diff --git a/tka/sig.go b/tka/sig.go new file mode 100644 index 000000000..f6bb20bd3 --- /dev/null +++ b/tka/sig.go @@ -0,0 +1,99 @@ +// 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" + "github.com/hdevalence/ed25519consensus" + "golang.org/x/crypto/blake2s" +) + +// SigKind describes valid NodeKeySignature types. +type SigKind uint8 + +const ( + SigInvalid SigKind = iota + // SigDirect describes a signature over a specific node key, using + // the keyID specified. + SigDirect +) + +func (s SigKind) String() string { + switch s { + case SigInvalid: + return "invalid" + case SigDirect: + return "direct" + default: + return fmt.Sprintf("Sig?<%d>", int(s)) + } +} + +// NodeKeySignature encapsulates a signature that authorizes a specific +// node key, based on verification from keys in the tailnet key authority. +type NodeKeySignature struct { + // SigKind identifies the variety of signature. + SigKind SigKind `cbor:"1,keyasint"` + // Pubkey identifies the public key which is being certified. + Pubkey []byte `cbor:"2,keyasint"` + + // KeyID identifies which key in the tailnet key authority should + // be used to verify this signature. Only set for SigDirect and + // SigCredential signature kinds. + KeyID []byte `cbor:"3,keyasint,omitempty"` + + // Signature is the packed (R, S) ed25519 signature over the rest + // of the structure. + Signature []byte `cbor:"4,keyasint,omitempty"` +} + +// sigHash returns the cryptographic digest which a signature +// is over. +// +// This is a hash of the serialized structure, sans the signature. +// Without this exclusion, the hash used for the signature +// would be circularly dependent on the signature. +func (s NodeKeySignature) sigHash() [blake2s.Size]byte { + dupe := s + dupe.Signature = nil + return blake2s.Sum256(dupe.Serialize()) +} + +// Serialize returns the given NKS in a serialized format. +func (s *NodeKeySignature) Serialize() []byte { + out := bytes.NewBuffer(make([]byte, 0, 128)) // 64byte sig + 32byte keyID + 32byte headroom + encoder, err := cbor.CTAP2EncOptions().EncMode() + if err != nil { + // Deterministic validation of encoding options, should + // never fail. + panic(err) + } + if err := encoder.NewEncoder(out).Encode(s); err != nil { + // Writing to a bytes.Buffer should never fail. + panic(err) + } + return out.Bytes() +} + +// verifySignature checks that the NodeKeySignature is authentic and certified +// by the given verificationKey. +func (s *NodeKeySignature) verifySignature(verificationKey Key) error { + sigHash := s.sigHash() + switch verificationKey.Kind { + case Key25519: + if ed25519consensus.Verify(ed25519.PublicKey(verificationKey.Public), sigHash[:], s.Signature) { + return nil + } + return errors.New("invalid signature") + + default: + return fmt.Errorf("unhandled key type: %v", verificationKey.Kind) + } +} diff --git a/tka/sig_test.go b/tka/sig_test.go new file mode 100644 index 000000000..1a071f088 --- /dev/null +++ b/tka/sig_test.go @@ -0,0 +1,34 @@ +// 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" + "testing" +) + +func TestSigDirect(t *testing.T) { + nodeKeyPub := []byte{1, 2, 3, 4} + + // Verification key (the key used to sign) + pub, priv := testingKey25519(t, 1) + key := Key{Kind: Key25519, Public: pub, Votes: 2} + + sig := NodeKeySignature{ + SigKind: SigDirect, + KeyID: key.ID(), + Pubkey: nodeKeyPub, + } + sigHash := sig.sigHash() + sig.Signature = ed25519.Sign(priv, sigHash[:]) + + if sig.sigHash() != sigHash { + t.Errorf("sigHash changed after signing: %x != %x", sig.sigHash(), sigHash) + } + + if err := sig.verifySignature(key); err != nil { + t.Fatalf("verifySignature() failed: %v", err) + } +} diff --git a/tka/tka.go b/tka/tka.go index c876e17b1..b126a9a57 100644 --- a/tka/tka.go +++ b/tka/tka.go @@ -11,6 +11,8 @@ import ( "fmt" "os" "sort" + + "github.com/fxamacker/cbor/v2" ) // Authority is a Tailnet Key Authority. This type is the main coupling @@ -586,3 +588,18 @@ func (a *Authority) Inform(updates []AUM) error { a.state = c.state return nil } + +// VerifySignature returns true if the provided nodeKeySignature is signed +// correctly by a trusted key. +func (a *Authority) VerifySignature(nodeKeySignature []byte) error { + var decoded NodeKeySignature + if err := cbor.Unmarshal(nodeKeySignature, &decoded); err != nil { + return fmt.Errorf("unmarshal: %v", err) + } + key, err := a.state.GetKey(decoded.KeyID) + if err != nil { + return fmt.Errorf("key: %v", err) + } + + return decoded.verifySignature(key) +} diff --git a/types/key/node.go b/types/key/node.go index af812c918..b71245711 100644 --- a/types/key/node.go +++ b/types/key/node.go @@ -10,6 +10,7 @@ import ( "crypto/subtle" "encoding/hex" "errors" + "fmt" "go4.org/mem" "golang.org/x/crypto/curve25519" @@ -34,6 +35,10 @@ const ( // changed. nodePublicHexPrefix = "nodekey:" + // nodePublicBinaryPrefix is the prefix used to identify a + // binary-encoded node public key. + nodePublicBinaryPrefix = "np" + // NodePublicRawLen is the length in bytes of a NodePublic, when // serialized with AppendTo, Raw32 or WriteRawWithoutAllocating. NodePublicRawLen = 32 @@ -297,6 +302,28 @@ func (k *NodePublic) UnmarshalText(b []byte) error { return parseHex(k.k[:], mem.B(b), mem.S(nodePublicHexPrefix)) } +// MarshalBinary implements encoding.BinaryMarshaler. +func (k NodePublic) MarshalBinary() (data []byte, err error) { + b := make([]byte, len(nodePublicBinaryPrefix)+NodePublicRawLen) + copy(b[:len(nodePublicBinaryPrefix)], nodePublicBinaryPrefix) + copy(b[len(nodePublicBinaryPrefix):], k.k[:]) + return b, nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (k *NodePublic) UnmarshalBinary(in []byte) error { + data := mem.B(in) + if !mem.HasPrefix(data, mem.S(nodePublicBinaryPrefix)) { + return fmt.Errorf("missing/incorrect type prefix %s", nodePublicBinaryPrefix) + } + if want, got := len(nodePublicBinaryPrefix)+NodePublicRawLen, data.Len(); want != got { + return fmt.Errorf("incorrect len for NodePublic (%d != %d)", got, want) + } + + data.SliceFrom(len(nodePublicBinaryPrefix)).Copy(k.k[:]) + return nil +} + // WireGuardGoString prints k in the same format used by wireguard-go. func (k NodePublic) WireGuardGoString() string { // This implementation deliberately matches the overly complicated diff --git a/types/key/node_test.go b/types/key/node_test.go index 1f9042d8f..321edf458 100644 --- a/types/key/node_test.go +++ b/types/key/node_test.go @@ -30,6 +30,20 @@ func TestNodeKey(t *testing.T) { if full, got := string(bs), ":"+p.UntypedHexString(); !strings.HasSuffix(full, got) { t.Fatalf("NodePublic.UntypedHexString is not a suffix of the typed serialization, got %q want suffix of %q", got, full) } + bs, err = p.MarshalBinary() + if err != nil { + t.Fatal(err) + } + if got, want := bs, append([]byte(nodePublicBinaryPrefix), p.k[:]...); !bytes.Equal(got, want) { + t.Fatalf("Binary-encoded NodePublic = %x, want %x", got, want) + } + var decoded NodePublic + if err := decoded.UnmarshalBinary(bs); err != nil { + t.Fatalf("NodePublic.UnmarshalBinary(%x) failed: %v", bs, err) + } + if decoded != p { + t.Errorf("unmarshaled and original NodePublic differ:\noriginal = %v\ndecoded = %v", p, decoded) + } z := NodePublic{} if !z.IsZero() {