tka,types/key: remove dependency for tailcfg & types/ packages on tka

Following the pattern elsewhere, we create a new tka-specific types package for the types
that need to couple between the serialized structure types, and tka.

Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
Tom DNetto
2022-08-04 11:45:19 -07:00
committed by Tom
parent a9f6cd41fd
commit f50043f6cb
18 changed files with 139 additions and 77 deletions

View File

@@ -12,15 +12,12 @@ import (
"github.com/fxamacker/cbor/v2"
"golang.org/x/crypto/blake2s"
"tailscale.com/types/tkatype"
)
// AUMHash represents the BLAKE2s digest of an Authority Update Message (AUM).
type AUMHash [blake2s.Size]byte
// AUMSigHash represents the BLAKE2s digest of an Authority Update
// Message (AUM), sans any signatures.
type AUMSigHash [blake2s.Size]byte
// AUMKind describes valid AUM types.
type AUMKind uint8
@@ -100,7 +97,7 @@ type AUM struct {
// KeyID references a public key which is part of the key authority.
// This field is used for RemoveKey and UpdateKey AUMs.
KeyID KeyID `cbor:"4,keyasint,omitempty"`
KeyID tkatype.KeyID `cbor:"4,keyasint,omitempty"`
// State describes the full state of the key authority.
// This field is used for Checkpoint AUMs.
@@ -118,7 +115,7 @@ type AUM struct {
// Signatures lists the signatures over this AUM.
// CBOR key 23 is the last key which can be encoded as a single byte.
Signatures []Signature `cbor:"23,keyasint,omitempty"`
Signatures []tkatype.Signature `cbor:"23,keyasint,omitempty"`
}
// StaticValidate returns a nil error if the AUM is well-formed.
@@ -230,7 +227,7 @@ func (a *AUM) Hash() AUMHash {
// This is identical to Hash() except the Signatures are not
// serialized. Without this, the hash used for signatures
// would be circularly dependent on the signatures.
func (a AUM) SigHash() AUMSigHash {
func (a AUM) SigHash() tkatype.AUMSigHash {
dupe := a
dupe.Signatures = nil
return blake2s.Sum256(dupe.Serialize())
@@ -250,7 +247,7 @@ func (a *AUM) sign25519(priv ed25519.PrivateKey) {
key := Key{Kind: Key25519, Public: priv.Public().(ed25519.PublicKey)}
sigHash := a.SigHash()
a.Signatures = append(a.Signatures, Signature{
a.Signatures = append(a.Signatures, tkatype.Signature{
KeyID: key.ID(),
Signature: ed25519.Sign(priv, sigHash[:]),
})

View File

@@ -11,6 +11,7 @@ import (
"github.com/fxamacker/cbor/v2"
"github.com/google/go-cmp/cmp"
"golang.org/x/crypto/blake2s"
"tailscale.com/types/tkatype"
)
func TestSerialization(t *testing.T) {
@@ -137,7 +138,7 @@ func TestSerialization(t *testing.T) {
},
{
"Signature",
AUM{MessageKind: AUMAddKey, Signatures: []Signature{{KeyID: []byte{1}}}},
AUM{MessageKind: AUMAddKey, Signatures: []tkatype.Signature{{KeyID: []byte{1}}}},
[]byte{
0xa3, // major type 5 (map), 3 items
0x01, // |- major type 0 (int), value 1 (first key, MessageKind)
@@ -198,7 +199,7 @@ func TestAUMWeight(t *testing.T) {
{
"Key unknown",
AUM{
Signatures: []Signature{{KeyID: fakeKeyID[:]}},
Signatures: []tkatype.Signature{{KeyID: fakeKeyID[:]}},
},
State{},
0,
@@ -206,7 +207,7 @@ func TestAUMWeight(t *testing.T) {
{
"Unary key",
AUM{
Signatures: []Signature{{KeyID: key.ID()}},
Signatures: []tkatype.Signature{{KeyID: key.ID()}},
},
State{
Keys: []Key{key},
@@ -216,7 +217,7 @@ func TestAUMWeight(t *testing.T) {
{
"Multiple keys",
AUM{
Signatures: []Signature{{KeyID: key.ID()}, {KeyID: key2.ID()}},
Signatures: []tkatype.Signature{{KeyID: key.ID()}, {KeyID: key2.ID()}},
},
State{
Keys: []Key{key, key2},
@@ -226,7 +227,7 @@ func TestAUMWeight(t *testing.T) {
{
"Double use",
AUM{
Signatures: []Signature{{KeyID: key.ID()}, {KeyID: key.ID()}},
Signatures: []tkatype.Signature{{KeyID: key.ID()}, {KeyID: key.ID()}},
},
State{
Keys: []Key{key},
@@ -255,7 +256,7 @@ func TestAUMHashes(t *testing.T) {
sigHash1 := aum.SigHash()
aumHash1 := aum.Hash()
aum.Signatures = []Signature{{KeyID: []byte{1, 2, 3, 4}}}
aum.Signatures = []tkatype.Signature{{KeyID: []byte{1, 2, 3, 4}}}
sigHash2 := aum.SigHash()
aumHash2 := aum.Hash()
if len(aum.Signatures) != 1 {

View File

@@ -6,11 +6,13 @@ package tka
import (
"fmt"
"tailscale.com/types/tkatype"
)
// Types implementing Signer can sign update messages.
type Signer interface {
SignAUM(*AUM) error
SignAUM(tkatype.AUMSigHash) ([]tkatype.Signature, error)
}
// UpdateBuilder implements a builder for changes to the tailnet
@@ -34,9 +36,11 @@ func (b *UpdateBuilder) mkUpdate(update AUM) error {
update.PrevAUMHash = prevHash
if b.signer != nil {
if err := b.signer.SignAUM(&update); err != 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)
@@ -61,7 +65,7 @@ func (b *UpdateBuilder) AddKey(key Key) error {
}
// RemoveKey removes a key from the authority.
func (b *UpdateBuilder) RemoveKey(keyID KeyID) error {
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)
}
@@ -69,7 +73,7 @@ func (b *UpdateBuilder) RemoveKey(keyID KeyID) error {
}
// SetKeyVote updates the number of votes of an existing key.
func (b *UpdateBuilder) SetKeyVote(keyID KeyID, votes uint) error {
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)
}
@@ -80,7 +84,7 @@ func (b *UpdateBuilder) SetKeyVote(keyID KeyID, votes uint) error {
//
// TODO(tom): Provide an API to update specific values rather than the whole
// map.
func (b *UpdateBuilder) SetKeyMeta(keyID KeyID, meta map[string]string) error {
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)
}

View File

@@ -9,13 +9,19 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/types/tkatype"
)
type signer25519 ed25519.PrivateKey
func (s signer25519) SignAUM(update *AUM) error {
update.sign25519(ed25519.PrivateKey(s))
return nil
func (s signer25519) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, error) {
priv := ed25519.PrivateKey(s)
key := Key{Kind: Key25519, Public: priv.Public().(ed25519.PublicKey)}
return []tkatype.Signature{{
KeyID: key.ID(),
Signature: ed25519.Sign(priv, sigHash[:]),
}}, nil
}
func TestAuthorityBuilderAddKey(t *testing.T) {

View File

@@ -15,6 +15,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/types/tkatype"
)
// chaintest_test.go implements test helpers for concisely describing
@@ -265,7 +266,7 @@ func (c *testChain) makeAUM(v *testchainNode) AUM {
sigHash := aum.SigHash()
for _, key := range c.SignAllKeys {
aum.Signatures = append(aum.Signatures, Signature{
aum.Signatures = append(aum.Signatures, tkatype.Signature{
KeyID: c.Key[key].ID(),
Signature: ed25519.Sign(c.KeyPrivs[key], sigHash[:]),
})
@@ -274,7 +275,7 @@ func (c *testChain) makeAUM(v *testchainNode) AUM {
// If the aum was specified as being signed by some key, then
// sign it using that key.
if key := v.SignedWith; key != "" {
aum.Signatures = append(aum.Signatures, Signature{
aum.Signatures = append(aum.Signatures, tkatype.Signature{
KeyID: c.Key[key].ID(),
Signature: ed25519.Sign(c.KeyPrivs[key], sigHash[:]),
})

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"github.com/hdevalence/ed25519consensus"
"tailscale.com/types/tkatype"
)
// KeyKind describes the different varieties of a Key.
@@ -73,12 +74,12 @@ func (k Key) Clone() Key {
return out
}
func (k Key) ID() KeyID {
func (k Key) ID() tkatype.KeyID {
switch k.Kind {
// Because 25519 public keys are so short, we just use the 32-byte
// public as their 'key ID'.
case Key25519:
return KeyID(k.Public)
return tkatype.KeyID(k.Public)
default:
panic("unsupported key kind")
}
@@ -112,21 +113,9 @@ func (k Key) StaticValidate() error {
return nil
}
// KeyID references a verification key stored in the key authority.
//
// For 25519 keys: The 32-byte public key.
type KeyID []byte
// Signature describes a signature over an AUM, which can be verified
// using the key referenced by KeyID.
type Signature struct {
KeyID KeyID `cbor:"1,keyasint"`
Signature []byte `cbor:"2,keyasint"`
}
// Verify returns a nil error if the signature is valid over the
// provided AUM BLAKE2s digest, using the given key.
func (s *Signature) Verify(aumDigest AUMSigHash, key Key) error {
func signatureVerify(s *tkatype.Signature, aumDigest tkatype.AUMSigHash, key Key) error {
// NOTE(tom): Even if we can compute the public from the KeyID,
// its possible for the KeyID to be attacker-controlled
// so we should use the public contained in the state machine.

View File

@@ -10,6 +10,8 @@ import (
"encoding/binary"
"math/rand"
"testing"
"tailscale.com/types/tkatype"
)
// returns a random source based on the test name + extraSeed.
@@ -41,24 +43,24 @@ func TestVerify25519(t *testing.T) {
MessageKind: AUMRemoveKey,
KeyID: []byte{1, 2, 3, 4},
// Signatures is set to crap so we are sure its ignored in the sigHash computation.
Signatures: []Signature{{KeyID: []byte{45, 42}}},
Signatures: []tkatype.Signature{{KeyID: []byte{45, 42}}},
}
sigHash := aum.SigHash()
aum.Signatures = []Signature{
aum.Signatures = []tkatype.Signature{
{
KeyID: key.ID(),
Signature: ed25519.Sign(priv, sigHash[:]),
},
}
if err := aum.Signatures[0].Verify(aum.SigHash(), key); err != nil {
if err := signatureVerify(&aum.Signatures[0], aum.SigHash(), key); err != nil {
t.Errorf("signature verification failed: %v", err)
}
// Make sure it fails with a different public key.
pub2, _ := testingKey25519(t, 2)
key2 := Key{Kind: Key25519, Public: pub2}
if err := aum.Signatures[0].Verify(aum.SigHash(), key2); err == nil {
if err := signatureVerify(&aum.Signatures[0], aum.SigHash(), key2); err == nil {
t.Error("signature verification with different key did not fail")
}
}

View File

@@ -204,7 +204,7 @@ func TestScenarioHelpers(t *testing.T) {
if _, ok := n.AUMs["L3"]; !ok {
t.Errorf("node n is missing %s", "L3")
}
if err := n.AUMs["L3"].Signatures[0].Verify(n.AUMs["L3"].SigHash(), *s.defaultKey); err != nil {
if err := signatureVerify(&n.AUMs["L3"].Signatures[0], n.AUMs["L3"].SigHash(), *s.defaultKey); err != nil {
t.Errorf("chained AUM was not signed: %v", err)
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/fxamacker/cbor/v2"
"github.com/hdevalence/ed25519consensus"
"golang.org/x/crypto/blake2s"
"tailscale.com/types/tkatype"
)
// SigKind describes valid NodeKeySignature types.
@@ -67,7 +68,7 @@ func (s NodeKeySignature) sigHash() [blake2s.Size]byte {
}
// Serialize returns the given NKS in a serialized format.
func (s *NodeKeySignature) Serialize() []byte {
func (s *NodeKeySignature) Serialize() tkatype.MarshaledSignature {
out := bytes.NewBuffer(make([]byte, 0, 128)) // 64byte sig + 32byte keyID + 32byte headroom
encoder, err := cbor.CTAP2EncOptions().EncMode()
if err != nil {

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"golang.org/x/crypto/argon2"
"tailscale.com/types/tkatype"
)
// ErrNoSuchKey is returned if the key referenced by a KeyID does not exist.
@@ -40,7 +41,7 @@ type State struct {
}
// GetKey returns the trusted key with the specified KeyID.
func (s State) GetKey(key KeyID) (Key, error) {
func (s State) GetKey(key tkatype.KeyID) (Key, error) {
for _, k := range s.Keys {
if bytes.Equal(k.ID(), key) {
return k, nil

View File

@@ -13,6 +13,7 @@ import (
"sort"
"github.com/fxamacker/cbor/v2"
"tailscale.com/types/tkatype"
)
// Authority is a Tailnet Key Authority. This type is the main coupling
@@ -416,7 +417,7 @@ func aumVerify(aum AUM, state State, isGenesisAUM bool) error {
if err != nil {
return fmt.Errorf("bad keyID on signature %d: %v", i, err)
}
if err := sig.Verify(sigHash, key); err != nil {
if err := signatureVerify(&sig, sigHash, key); err != nil {
return fmt.Errorf("signature %d: %v", i, err)
}
}
@@ -485,9 +486,11 @@ func Create(storage Chonk, state State, signer Signer) (*Authority, AUM, error)
// This serves as an easy way to validate the given state.
return nil, AUM{}, fmt.Errorf("invalid state: %v", err)
}
if err := signer.SignAUM(&genesis); err != nil {
sigs, err := signer.SignAUM(genesis.SigHash())
if err != nil {
return nil, AUM{}, fmt.Errorf("signing failed: %v", err)
}
genesis.Signatures = append(genesis.Signatures, sigs...)
a, err := Bootstrap(storage, genesis)
return a, genesis, err
@@ -591,7 +594,7 @@ func (a *Authority) Inform(updates []AUM) error {
// VerifySignature returns true if the provided nodeKeySignature is signed
// correctly by a trusted key.
func (a *Authority) VerifySignature(nodeKeySignature []byte) error {
func (a *Authority) VerifySignature(nodeKeySignature tkatype.MarshaledSignature) error {
var decoded NodeKeySignature
if err := cbor.Unmarshal(nodeKeySignature, &decoded); err != nil {
return fmt.Errorf("unmarshal: %v", err)

View File

@@ -9,6 +9,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/types/tkatype"
)
func TestComputeChainCandidates(t *testing.T) {
@@ -232,7 +233,7 @@ func TestOpenAuthority(t *testing.T) {
i2, i2H := fakeAUM(t, 2, &i1H)
i3, i3H := fakeAUM(t, 5, &i2H)
l2, l2H := fakeAUM(t, AUM{MessageKind: AUMNoOp, KeyID: []byte{7}, Signatures: []Signature{{KeyID: key.ID()}}}, &i3H)
l2, l2H := fakeAUM(t, AUM{MessageKind: AUMNoOp, KeyID: []byte{7}, Signatures: []tkatype.Signature{{KeyID: key.ID()}}}, &i3H)
l3, l3H := fakeAUM(t, 4, &i3H)
g2, g2H := fakeAUM(t, 8, nil)