summaryrefslogtreecommitdiffhomepage
path: root/tka/builder.go
blob: 1e7b130151876d67fce4133f297f2278fb361f89 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build !ts_omit_tailnetlock

package tka

import (
	"fmt"
	"os"

	"tailscale.com/types/tkatype"
)

// Types implementing Signer can sign update messages.
type Signer interface {
	// SignAUM returns signatures for the AUM encoded by the given AUMSigHash.
	SignAUM(tkatype.AUMSigHash) ([]tkatype.Signature, error)
}

// UpdateBuilder implements a builder for changes to the tailnet
// key authority.
//
// Finalize must be called to compute the update messages, which
// must then be applied to all Authority objects using Inform().
type UpdateBuilder struct {
	a      *Authority
	signer Signer

	state  State
	parent AUMHash

	out []AUM
}

func (b *UpdateBuilder) mkUpdate(update AUM) error {
	prevHash := make([]byte, len(b.parent))
	copy(prevHash, b.parent[:])
	update.PrevAUMHash = prevHash

	if b.signer != nil {
		sigs, err := b.signer.SignAUM(update.SigHash())
		if err != nil {
			return fmt.Errorf("signing failed: %v", err)
		}
		update.Signatures = append(update.Signatures, sigs...)
	}
	if err := update.StaticValidate(); err != nil {
		return fmt.Errorf("generated update was invalid: %v", err)
	}
	state, err := b.state.applyVerifiedAUM(update)
	if err != nil {
		return fmt.Errorf("update cannot be applied: %v", err)
	}

	b.state = state
	b.parent = update.Hash()
	b.out = append(b.out, update)
	return nil
}

// AddKey adds a new key to the authority.
func (b *UpdateBuilder) AddKey(key Key) error {
	keyID, err := key.ID()
	if err != nil {
		return err
	}

	if _, err := b.state.GetKey(keyID); err == nil {
		return fmt.Errorf("cannot add key %v: already exists", key)
	}

	if len(b.state.Keys) >= maxKeys {
		return fmt.Errorf("cannot add key %v: maximum number of keys reached", key)
	}

	return b.mkUpdate(AUM{MessageKind: AUMAddKey, Key: &key})
}

// RemoveKey removes a key from the authority.
func (b *UpdateBuilder) RemoveKey(keyID tkatype.KeyID) error {
	if _, err := b.state.GetKey(keyID); err != nil {
		return fmt.Errorf("failed reading key %x: %v", keyID, err)
	}
	return b.mkUpdate(AUM{MessageKind: AUMRemoveKey, KeyID: keyID})
}

// SetKeyVote updates the number of votes of an existing key.
func (b *UpdateBuilder) SetKeyVote(keyID tkatype.KeyID, votes uint) error {
	if _, err := b.state.GetKey(keyID); err != nil {
		return fmt.Errorf("failed reading key %x: %v", keyID, err)
	}
	return b.mkUpdate(AUM{MessageKind: AUMUpdateKey, Votes: &votes, KeyID: keyID})
}

// SetKeyMeta updates key-value metadata stored against an existing key.
//
// TODO(tom): Provide an API to update specific values rather than the whole
// map.
func (b *UpdateBuilder) SetKeyMeta(keyID tkatype.KeyID, meta map[string]string) error {
	if _, err := b.state.GetKey(keyID); err != nil {
		return fmt.Errorf("failed reading key %x: %v", keyID, err)
	}
	return b.mkUpdate(AUM{MessageKind: AUMUpdateKey, Meta: meta, KeyID: keyID})
}

func (b *UpdateBuilder) generateCheckpoint() error {
	// Compute the checkpoint state.
	state := b.a.state
	for i, update := range b.out {
		var err error
		if state, err = state.applyVerifiedAUM(update); err != nil {
			return fmt.Errorf("applying update %d: %v", i, err)
		}
	}

	// Checkpoints can't specify a parent AUM.
	state.LastAUMHash = nil
	return b.mkUpdate(AUM{MessageKind: AUMCheckpoint, State: &state})
}

// checkpointEvery sets how often a checkpoint AUM should be generated.
const checkpointEvery = 50

// Finalize returns the set of update message to actuate the update.
func (b *UpdateBuilder) Finalize(storage Chonk) ([]AUM, error) {
	var (
		needCheckpoint bool    = true
		cursor         AUMHash = b.a.Head()
	)
	for i := len(b.out); i < checkpointEvery; i++ {
		aum, err := storage.AUM(cursor)
		if err != nil {
			if err == os.ErrNotExist {
				// The available chain is shorter than the interval to checkpoint at.
				needCheckpoint = false
				break
			}
			return nil, fmt.Errorf("reading AUM (%v): %v", cursor, err)
		}

		if aum.MessageKind == AUMCheckpoint {
			needCheckpoint = false
			break
		}

		parent, hasParent := aum.Parent()
		if !hasParent {
			// We've hit the genesis update, so the chain is shorter than the interval to checkpoint at.
			needCheckpoint = false
			break
		}
		cursor = parent
	}

	if needCheckpoint {
		if err := b.generateCheckpoint(); err != nil {
			return nil, fmt.Errorf("generating checkpoint: %v", err)
		}
	}

	// Check no AUMs were applied in the meantime
	if len(b.out) > 0 {
		if parent, _ := b.out[0].Parent(); parent != b.a.Head() {
			return nil, fmt.Errorf("updates no longer apply to head: based on %x but head is %x", parent, b.a.Head())
		}
	}
	return b.out, nil
}

// NewUpdater returns a builder you can use to make changes to
// the tailnet key authority.
//
// The provided signer function, if non-nil, is called with each update
// to compute and apply signatures.
//
// Updates are specified by calling methods on the returned UpdatedBuilder.
// Call Finalize() when you are done to obtain the specific update messages
// which actuate the changes.
func (a *Authority) NewUpdater(signer Signer) *UpdateBuilder {
	return &UpdateBuilder{
		a:      a,
		signer: signer,
		parent: a.Head(),
		state:  a.state,
	}
}