Просмотр исходного кода

tka,types/key: implement direct node-key signatures

Signed-off-by: Tom DNetto <[email protected]>
Tom DNetto 3 лет назад
Родитель
Сommit
8cfd775885
5 измененных файлов с 191 добавлено и 0 удалено
  1. 99 0
      tka/sig.go
  2. 34 0
      tka/sig_test.go
  3. 17 0
      tka/tka.go
  4. 27 0
      types/key/node.go
  5. 14 0
      types/key/node_test.go

+ 99 - 0
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)
+	}
+}

+ 34 - 0
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)
+	}
+}

+ 17 - 0
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)
+}

+ 27 - 0
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

+ 14 - 0
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() {