mirror of https://github.com/tailscale/tailscale/
tka: implement AUM and Key types
This is the first in a series of PRs implementing the internals for the Tailnet Key Authority. This PR implements the AUM and Key types, which are used by pretty much everything else. Future PRs: - The State type & related machinery - The Tailchonk (storage) type & implementation - The Authority type and sync implementation Signed-off-by: Tom DNetto <tom@tailscale.com>pull/5014/head
parent
e6572a0f08
commit
1cfd96cdc2
@ -0,0 +1,256 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tka
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Valid AUM types. Do NOT reorder.
|
||||
const (
|
||||
AUMInvalid AUMKind = iota
|
||||
// An AddKey AUM describes a new key trusted by the TKA.
|
||||
//
|
||||
// Only the Key optional field may be set.
|
||||
AUMAddKey
|
||||
// A RemoveKey AUM describes hte removal of a key trusted by TKA.
|
||||
//
|
||||
// Only the KeyID optional field may be set.
|
||||
AUMRemoveKey
|
||||
// A DisableNL AUM describes the disablement of TKA.
|
||||
//
|
||||
// Only the DisablementSecret optional field may be set.
|
||||
AUMDisableNL
|
||||
// A NoOp AUM carries no information and is used in tests.
|
||||
AUMNoOp
|
||||
// A UpdateKey AUM updates the metadata or votes of an existing key.
|
||||
//
|
||||
// Only KeyID, along with either/or Meta or Votes optional fields
|
||||
// may be set.
|
||||
AUMUpdateKey
|
||||
// A Checkpoint AUM specifies the full state of the TKA.
|
||||
//
|
||||
// Only the State optional field may be set.
|
||||
AUMCheckpoint
|
||||
)
|
||||
|
||||
func (k AUMKind) String() string {
|
||||
switch k {
|
||||
case AUMInvalid:
|
||||
return "invalid"
|
||||
case AUMAddKey:
|
||||
return "add-key"
|
||||
case AUMRemoveKey:
|
||||
return "remove-key"
|
||||
case AUMDisableNL:
|
||||
return "disable-nl"
|
||||
case AUMNoOp:
|
||||
return "no-op"
|
||||
case AUMCheckpoint:
|
||||
return "checkpoint"
|
||||
case AUMUpdateKey:
|
||||
return "update-key"
|
||||
default:
|
||||
return fmt.Sprintf("AUM?<%d>", int(k))
|
||||
}
|
||||
}
|
||||
|
||||
// AUM describes an Authority Update Message.
|
||||
//
|
||||
// The rules for adding new types of AUMs (MessageKind):
|
||||
// - CBOR key IDs must never be changed.
|
||||
// - New AUM types must not change semantics that are manipulated by other
|
||||
// AUM types.
|
||||
// - The serialization of existing data cannot change (in other words, if
|
||||
// an existing serialization test in aum_test.go fails, you need to try a
|
||||
// different approach).
|
||||
//
|
||||
// The rules for adding new fields are as follows:
|
||||
// - Must all be optional.
|
||||
// - An unset value must not result in serialization overhead. This is
|
||||
// necessary so the serialization of older AUMs stays the same.
|
||||
// - New processing semantics of the new fields must be compatible with the
|
||||
// behavior of old clients (which will ignore the field).
|
||||
// - No floats!
|
||||
type AUM struct {
|
||||
MessageKind AUMKind `cbor:"1,keyasint"`
|
||||
PrevAUMHash []byte `cbor:"2,keyasint"`
|
||||
|
||||
// Key encodes a public key to be added to the key authority.
|
||||
// This field is used for AddKey AUMs.
|
||||
Key *Key `cbor:"3,keyasint,omitempty"`
|
||||
|
||||
// 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"`
|
||||
|
||||
// State describes the full state of the key authority.
|
||||
// This field is used for Checkpoint AUMs.
|
||||
// TODO(tom): Use type *State once a future PR brings in that type.
|
||||
State interface{} `cbor:"5,keyasint,omitempty"`
|
||||
|
||||
// DisablementSecret is used to transmit a secret for disabling
|
||||
// the TKA.
|
||||
// This field is used for DisableNL AUMs.
|
||||
DisablementSecret []byte `cbor:"6,keyasint,omitempty"`
|
||||
|
||||
// Votes and Meta describe properties of a key in the key authority.
|
||||
// These fields are used for UpdateKey AUMs.
|
||||
Votes *uint `cbor:"7,keyasint,omitempty"`
|
||||
Meta map[string]string `cbor:"8,keyasint,omitempty"`
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// StaticValidate returns a nil error if the AUM is well-formed.
|
||||
func (a *AUM) StaticValidate() error {
|
||||
if a.Key != nil {
|
||||
if err := a.Key.StaticValidate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a.PrevAUMHash != nil && len(a.PrevAUMHash) == 0 {
|
||||
return errors.New("absent parent must be represented by a nil slice")
|
||||
}
|
||||
for i, sig := range a.Signatures {
|
||||
if len(sig.KeyID) == 0 || len(sig.Signature) != ed25519.SignatureSize {
|
||||
return fmt.Errorf("signature %d has missing keyID or malformed signature", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(tom): Validate State once a future PR brings in that type.
|
||||
|
||||
switch a.MessageKind {
|
||||
case AUMAddKey:
|
||||
if a.Key == nil {
|
||||
return errors.New("AddKey AUMs must contain a key")
|
||||
}
|
||||
if a.KeyID != nil || a.DisablementSecret != nil || a.State != nil || a.Votes != nil || a.Meta != nil {
|
||||
return errors.New("AddKey AUMs may only specify a Key")
|
||||
}
|
||||
case AUMRemoveKey:
|
||||
if len(a.KeyID) == 0 {
|
||||
return errors.New("RemoveKey AUMs must specify a key ID")
|
||||
}
|
||||
if a.Key != nil || a.DisablementSecret != nil || a.State != nil || a.Votes != nil || a.Meta != nil {
|
||||
return errors.New("RemoveKey AUMs may only specify a KeyID")
|
||||
}
|
||||
case AUMUpdateKey:
|
||||
if len(a.KeyID) == 0 {
|
||||
return errors.New("UpdateKey AUMs must specify a key ID")
|
||||
}
|
||||
if a.Meta == nil && a.Votes == nil {
|
||||
return errors.New("UpdateKey AUMs must contain an update to votes or key metadata")
|
||||
}
|
||||
if a.Key != nil || a.DisablementSecret != nil || a.State != nil {
|
||||
return errors.New("UpdateKey AUMs may only specify KeyID, Votes, and Meta")
|
||||
}
|
||||
case AUMCheckpoint:
|
||||
if a.State == nil {
|
||||
return errors.New("Checkpoint AUMs must specify the state")
|
||||
}
|
||||
if a.KeyID != nil || a.DisablementSecret != nil || a.Key != nil || a.Votes != nil || a.Meta != nil {
|
||||
return errors.New("Checkpoint AUMs may only specify State")
|
||||
}
|
||||
case AUMDisableNL:
|
||||
if len(a.DisablementSecret) == 0 {
|
||||
return errors.New("DisableNL AUMs must specify a disablement secret")
|
||||
}
|
||||
if a.KeyID != nil || a.State != nil || a.Key != nil || a.Votes != nil || a.Meta != nil {
|
||||
return errors.New("DisableNL AUMs may only a disablement secret")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serialize returns the given AUM in a serialized format.
|
||||
func (a *AUM) Serialize() []byte {
|
||||
// Why CBOR and not something like JSON?
|
||||
//
|
||||
// The main function of an AUM is to carry signed data. Signatures are
|
||||
// over digests, so the serialized representation must be deterministic.
|
||||
// Further, experience with other attempts (JWS/JWT,SAML,X509 etc) has
|
||||
// taught us that even subtle behaviors such as how you handle invalid
|
||||
// or unrecognized fields + any invariants in subsequent re-serialization
|
||||
// can easily lead to security-relevant logic bugs. Its certainly possible
|
||||
// to invent a workable scheme by massaging a JSON parsing library, though
|
||||
// profoundly unwise.
|
||||
//
|
||||
// CBOR is one of the few encoding schemes that are appropriate for use
|
||||
// with signatures and has security-conscious parsing + serialization
|
||||
// rules baked into the spec. We use the CTAP2 mode, which is well
|
||||
// understood + widely-implemented, and already proven for use in signing
|
||||
// assertions through its use by FIDO2 devices.
|
||||
out := bytes.NewBuffer(make([]byte, 0, 128))
|
||||
encoder, err := cbor.CTAP2EncOptions().EncMode()
|
||||
if err != nil {
|
||||
// Deterministic validation of encoding options, should
|
||||
// never fail.
|
||||
panic(err)
|
||||
}
|
||||
if err := encoder.NewEncoder(out).Encode(a); err != nil {
|
||||
// Writing to a bytes.Buffer should never fail.
|
||||
panic(err)
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// Hash returns a cryptographic digest of all AUM contents.
|
||||
func (a *AUM) Hash() AUMHash {
|
||||
return blake2s.Sum256(a.Serialize())
|
||||
}
|
||||
|
||||
// SigHash returns the cryptographic digest which a signature
|
||||
// is over.
|
||||
//
|
||||
// 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 {
|
||||
dupe := a
|
||||
dupe.Signatures = nil
|
||||
return blake2s.Sum256(dupe.Serialize())
|
||||
}
|
||||
|
||||
// Parent returns the parent's AUM hash and true, or a
|
||||
// zero value and false if there was no parent.
|
||||
func (a *AUM) Parent() (h AUMHash, ok bool) {
|
||||
if len(a.PrevAUMHash) > 0 {
|
||||
copy(h[:], a.PrevAUMHash)
|
||||
return h, true
|
||||
}
|
||||
return h, false
|
||||
}
|
||||
|
||||
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{
|
||||
KeyID: key.ID(),
|
||||
Signature: ed25519.Sign(priv, sigHash[:]),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(tom): Implement Weight() once a future PR brings in the State type.
|
@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tka
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestSerialization(t *testing.T) {
|
||||
uint2 := uint(2)
|
||||
|
||||
tcs := []struct {
|
||||
Name string
|
||||
AUM AUM
|
||||
Expect []byte
|
||||
}{
|
||||
{
|
||||
"AddKey",
|
||||
AUM{MessageKind: AUMAddKey, Key: &Key{}},
|
||||
[]byte{
|
||||
0xa3, // major type 5 (map), 3 items
|
||||
0x01, // |- major type 0 (int), value 1 (first key, MessageKind)
|
||||
0x01, // |- major type 0 (int), value 1 (first value, AUMAddKey)
|
||||
0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash)
|
||||
0xf6, // |- major type 7 (val), value null (second value, nil)
|
||||
0x03, // |- major type 0 (int), value 3 (third key, Key)
|
||||
0xa3, // |- major type 5 (map), 3 items (type Key)
|
||||
0x01, // |- major type 0 (int), value 1 (first key, Kind)
|
||||
0x00, // |- major type 0 (int), value 0 (first value)
|
||||
0x02, // |- major type 0 (int), value 2 (second key, Votes)
|
||||
0x00, // |- major type 0 (int), value 0 (first value)
|
||||
0x03, // |- major type 0 (int), value 3 (third key, Public)
|
||||
0xf6, // |- major type 7 (val), value null (third value, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
"RemoveKey",
|
||||
AUM{MessageKind: AUMRemoveKey, KeyID: []byte{1, 2}},
|
||||
[]byte{
|
||||
0xa3, // major type 5 (map), 3 items
|
||||
0x01, // |- major type 0 (int), value 1 (first key, MessageKind)
|
||||
0x02, // |- major type 0 (int), value 2 (first value, AUMRemoveKey)
|
||||
0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash)
|
||||
0xf6, // |- major type 7 (val), value null (second value, nil)
|
||||
0x04, // |- major type 0 (int), value 4 (third key, KeyID)
|
||||
0x42, // |- major type 2 (byte string), 2 items
|
||||
0x01, // |- major type 0 (int), value 1 (byte 1)
|
||||
0x02, // |- major type 0 (int), value 2 (byte 2)
|
||||
},
|
||||
},
|
||||
{
|
||||
"UpdateKey",
|
||||
AUM{MessageKind: AUMUpdateKey, Votes: &uint2, KeyID: []byte{1, 2}, Meta: map[string]string{"a": "b"}},
|
||||
[]byte{
|
||||
0xa5, // major type 5 (map), 5 items
|
||||
0x01, // |- major type 0 (int), value 1 (first key, MessageKind)
|
||||
0x05, // |- major type 0 (int), value 2 (first value, AUMUpdateKey)
|
||||
0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash)
|
||||
0xf6, // |- major type 7 (val), value null (second value, nil)
|
||||
0x04, // |- major type 0 (int), value 4 (third key, KeyID)
|
||||
0x42, // |- major type 2 (byte string), 2 items
|
||||
0x01, // |- major type 0 (int), value 1 (byte 1)
|
||||
0x02, // |- major type 0 (int), value 2 (byte 2)
|
||||
0x07, // |- major type 0 (int), value 7 (fourth key, Votes)
|
||||
0x02, // |- major type 0 (int), value 2 (forth value, 2)
|
||||
0x08, // |- major type 0 (int), value 8 (fifth key, Meta)
|
||||
0xa1, // |- major type 5 (map), 1 item (map[string]string type)
|
||||
0x61, // |- major type 3 (text string), value 1 (first key, one byte long)
|
||||
0x61, // |- byte 'a'
|
||||
0x61, // |- major type 3 (text string), value 1 (first value, one byte long)
|
||||
0x62, // |- byte 'b'
|
||||
},
|
||||
},
|
||||
{
|
||||
"DisableNL",
|
||||
AUM{MessageKind: AUMDisableNL, PrevAUMHash: []byte{1, 2}, DisablementSecret: []byte{3, 4}},
|
||||
[]byte{
|
||||
0xa3, // major type 5 (map), 3 items
|
||||
0x01, // |- major type 0 (int), value 1 (first key, MessageKind)
|
||||
0x03, // |- major type 0 (int), value 3 (first value, AUMDisableNL)
|
||||
0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash)
|
||||
0x42, // |- major type 2 (byte string), 2 items (second value)
|
||||
0x01, // |- major type 0 (int), value 1 (byte 1)
|
||||
0x02, // |- major type 0 (int), value 2 (byte 2)
|
||||
0x06, // |- major type 0 (int), value 6 (third key, DisablementSecret)
|
||||
0x42, // |- major type 2 (byte string), 2 items (third value)
|
||||
0x03, // |- major type 0 (int), value 3 (byte 3)
|
||||
0x04, // |- major type 0 (int), value 4 (byte 4)
|
||||
},
|
||||
},
|
||||
// TODO(tom): Uncomment once a future PR brings in the State type.
|
||||
// {
|
||||
// "Checkpoint",
|
||||
// AUM{MessageKind: AUMCheckpoint, PrevAUMHash: []byte{1, 2}, State: &State{
|
||||
// LastAUMHash: []byte{3, 4},
|
||||
// Keys: []Key{
|
||||
// {Kind: Key25519, Public: []byte{5, 6}},
|
||||
// },
|
||||
// }},
|
||||
// []byte{
|
||||
// 0xa3, // major type 5 (map), 3 items
|
||||
// 0x01, // |- major type 0 (int), value 1 (first key, MessageKind)
|
||||
// 0x06, // |- major type 0 (int), value 6 (first value, AUMCheckpoint)
|
||||
// 0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash)
|
||||
// 0x42, // |- major type 2 (byte string), 2 items (second value)
|
||||
// 0x01, // |- major type 0 (int), value 1 (byte 1)
|
||||
// 0x02, // |- major type 0 (int), value 2 (byte 2)
|
||||
// 0x05, // |- major type 0 (int), value 5 (third key, State)
|
||||
// 0xa3, // |- major type 5 (map), 3 items (third value, State type)
|
||||
// 0x01, // |- major type 0 (int), value 1 (first key, LastAUMHash)
|
||||
// 0x42, // |- major type 2 (byte string), 2 items (first value)
|
||||
// 0x03, // |- major type 0 (int), value 3 (byte 3)
|
||||
// 0x04, // |- major type 0 (int), value 4 (byte 4)
|
||||
// 0x02, // |- major type 0 (int), value 2 (second key, DisablementSecrets)
|
||||
// 0xf6, // |- major type 7 (val), value null (second value, nil)
|
||||
// 0x03, // |- major type 0 (int), value 3 (third key, Keys)
|
||||
// 0x81, // |- major type 4 (array), value 1 (one item in array)
|
||||
// 0xa3, // |- major type 5 (map), 3 items (Key type)
|
||||
// 0x01, // |- major type 0 (int), value 1 (first key, Kind)
|
||||
// 0x01, // |- major type 0 (int), value 1 (first value, Key25519)
|
||||
// 0x02, // |- major type 0 (int), value 2 (second key, Votes)
|
||||
// 0x00, // |- major type 0 (int), value 0 (second value, 0)
|
||||
// 0x03, // |- major type 0 (int), value 3 (third key, Public)
|
||||
// 0x42, // |- major type 2 (byte string), 2 items (third value)
|
||||
// 0x05, // |- major type 0 (int), value 5 (byte 5)
|
||||
// 0x06, // |- major type 0 (int), value 6 (byte 6)
|
||||
// },
|
||||
// },
|
||||
{
|
||||
"Signature",
|
||||
AUM{MessageKind: AUMAddKey, Signatures: []Signature{{KeyID: []byte{1}}}},
|
||||
[]byte{
|
||||
0xa3, // major type 5 (map), 3 items
|
||||
0x01, // |- major type 0 (int), value 1 (first key, MessageKind)
|
||||
0x01, // |- major type 0 (int), value 1 (first value, AUMAddKey)
|
||||
0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash)
|
||||
0xf6, // |- major type 7 (val), value null (second value, nil)
|
||||
0x17, // |- major type 0 (int), value 22 (third key, Signatures)
|
||||
0x81, // |- major type 4 (array), value 1 (one item in array)
|
||||
0xa2, // |- major type 5 (map), 2 items (Signature type)
|
||||
0x01, // |- major type 0 (int), value 1 (first key, KeyID)
|
||||
0x41, // |- major type 2 (byte string), 1 item
|
||||
0x01, // |- major type 0 (int), value 1 (byte 1)
|
||||
0x02, // |- major type 0 (int), value 2 (second key, Signature)
|
||||
0xf6, // |- major type 7 (val), value null (second value, nil)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
data := tc.AUM.Serialize()
|
||||
if diff := cmp.Diff(tc.Expect, data); diff != "" {
|
||||
t.Errorf("serialization differs (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
var decodedAUM AUM
|
||||
if err := cbor.Unmarshal(data, &decodedAUM); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.AUM, decodedAUM); diff != "" {
|
||||
t.Errorf("unmarshalled version differs (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAUMHashes(t *testing.T) {
|
||||
// .Hash(): a hash over everything.
|
||||
// .SigHash(): a hash over everything except the signatures.
|
||||
// The signatures are over a hash of the AUM, so
|
||||
// using SigHash() breaks this circularity.
|
||||
|
||||
aum := AUM{MessageKind: AUMAddKey, Key: &Key{Kind: Key25519}}
|
||||
sigHash1 := aum.SigHash()
|
||||
aumHash1 := aum.Hash()
|
||||
|
||||
aum.Signatures = []Signature{{KeyID: []byte{1, 2, 3, 4}}}
|
||||
sigHash2 := aum.SigHash()
|
||||
aumHash2 := aum.Hash()
|
||||
if len(aum.Signatures) != 1 {
|
||||
t.Error("signature was removed by one of the hash functions")
|
||||
}
|
||||
|
||||
if !bytes.Equal(sigHash1[:], sigHash1[:]) {
|
||||
t.Errorf("signature hash dependent on signatures!\n\t1 = %x\n\t2 = %x", sigHash1, sigHash2)
|
||||
}
|
||||
if bytes.Equal(aumHash1[:], aumHash2[:]) {
|
||||
t.Error("aum hash didnt change")
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tka
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hdevalence/ed25519consensus"
|
||||
)
|
||||
|
||||
// KeyKind describes the different varieties of a Key.
|
||||
type KeyKind uint8
|
||||
|
||||
// Valid KeyKind values.
|
||||
const (
|
||||
KeyInvalid KeyKind = iota
|
||||
Key25519
|
||||
)
|
||||
|
||||
func (k KeyKind) String() string {
|
||||
switch k {
|
||||
case KeyInvalid:
|
||||
return "invalid"
|
||||
case Key25519:
|
||||
return "25519"
|
||||
default:
|
||||
return fmt.Sprintf("Key?<%d>", int(k))
|
||||
}
|
||||
}
|
||||
|
||||
// Key describes the public components of a key known to network-lock.
|
||||
type Key struct {
|
||||
Kind KeyKind `cbor:"1,keyasint"`
|
||||
|
||||
// Votes describes the weight applied to signatures using this key.
|
||||
// Weighting is used to deterministically resolve branches in the AUM
|
||||
// chain (i.e. forks, where two AUMs exist with the same parent).
|
||||
Votes uint `cbor:"2,keyasint"`
|
||||
|
||||
// Public encodes the public key of the key. For 25519 keys,
|
||||
// this is simply the point on the curve representing the public
|
||||
// key.
|
||||
Public []byte `cbor:"3,keyasint"`
|
||||
|
||||
// Meta describes arbitrary metadata about the key. This could be
|
||||
// used to store the name of the key, for instance.
|
||||
Meta map[string]string `cbor:"12,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
func (k Key) ID() 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)
|
||||
default:
|
||||
panic("unsupported key kind")
|
||||
}
|
||||
}
|
||||
|
||||
const maxMetaBytes = 512
|
||||
|
||||
func (k Key) StaticValidate() error {
|
||||
if k.Votes > 4096 {
|
||||
return fmt.Errorf("excessive key weight: %d > 4096", k.Votes)
|
||||
}
|
||||
|
||||
// We have an arbitrary upper limit on the amount
|
||||
// of metadata that can be associated with a key, so
|
||||
// people don't start using it as a key-value store and
|
||||
// causing pathological cases due to the number + size of
|
||||
// AUMs.
|
||||
var metaBytes uint
|
||||
for k, v := range k.Meta {
|
||||
metaBytes += uint(len(k) + len(v))
|
||||
}
|
||||
if metaBytes > maxMetaBytes {
|
||||
return fmt.Errorf("key metadata too big (%d > %d)", metaBytes, maxMetaBytes)
|
||||
}
|
||||
|
||||
switch k.Kind {
|
||||
case Key25519:
|
||||
default:
|
||||
return fmt.Errorf("unrecognized key kind: %v", k.Kind)
|
||||
}
|
||||
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 {
|
||||
// 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.
|
||||
switch key.Kind {
|
||||
case Key25519:
|
||||
if ed25519consensus.Verify(ed25519.PublicKey(key.Public), aumDigest[:], s.Signature) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid signature")
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unhandled key type: %v", key.Kind)
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tka
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// returns a random source based on the test name + extraSeed.
|
||||
func testingRand(t *testing.T, extraSeed int64) *rand.Rand {
|
||||
var seed int64
|
||||
if err := binary.Read(bytes.NewBuffer([]byte(t.Name())), binary.LittleEndian, &seed); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return rand.New(rand.NewSource(seed + extraSeed))
|
||||
}
|
||||
|
||||
// generates a 25519 private key based on the seed + test name.
|
||||
func testingKey25519(t *testing.T, seed int64) (ed25519.PublicKey, ed25519.PrivateKey) {
|
||||
pub, priv, err := ed25519.GenerateKey(testingRand(t, seed))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pub, priv
|
||||
}
|
||||
|
||||
func TestVerify25519(t *testing.T) {
|
||||
pub, priv := testingKey25519(t, 1)
|
||||
key := Key{
|
||||
Kind: Key25519,
|
||||
Public: pub,
|
||||
}
|
||||
|
||||
aum := AUM{
|
||||
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}}},
|
||||
}
|
||||
sigHash := aum.SigHash()
|
||||
aum.Signatures = []Signature{
|
||||
{
|
||||
KeyID: key.ID(),
|
||||
Signature: ed25519.Sign(priv, sigHash[:]),
|
||||
},
|
||||
}
|
||||
|
||||
if err := aum.Signatures[0].Verify(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 {
|
||||
t.Error("signature verification with different key did not fail")
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tka (WIP) implements the Tailnet Key Authority.
|
||||
package tka
|
Loading…
Reference in New Issue