mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
tka,types/key: implement direct node-key signatures
Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
parent
c13fab2a67
commit
8cfd775885
99
tka/sig.go
Normal file
99
tka/sig.go
Normal file
@ -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
tka/sig_test.go
Normal file
34
tka/sig_test.go
Normal file
@ -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
tka/tka.go
17
tka/tka.go
@ -11,6 +11,8 @@
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/fxamacker/cbor/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authority is a Tailnet Key Authority. This type is the main coupling
|
// 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
|
a.state = c.state
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"go4.org/mem"
|
"go4.org/mem"
|
||||||
"golang.org/x/crypto/curve25519"
|
"golang.org/x/crypto/curve25519"
|
||||||
@ -34,6 +35,10 @@
|
|||||||
// changed.
|
// changed.
|
||||||
nodePublicHexPrefix = "nodekey:"
|
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
|
// NodePublicRawLen is the length in bytes of a NodePublic, when
|
||||||
// serialized with AppendTo, Raw32 or WriteRawWithoutAllocating.
|
// serialized with AppendTo, Raw32 or WriteRawWithoutAllocating.
|
||||||
NodePublicRawLen = 32
|
NodePublicRawLen = 32
|
||||||
@ -297,6 +302,28 @@ func (k *NodePublic) UnmarshalText(b []byte) error {
|
|||||||
return parseHex(k.k[:], mem.B(b), mem.S(nodePublicHexPrefix))
|
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.
|
// WireGuardGoString prints k in the same format used by wireguard-go.
|
||||||
func (k NodePublic) WireGuardGoString() string {
|
func (k NodePublic) WireGuardGoString() string {
|
||||||
// This implementation deliberately matches the overly complicated
|
// This implementation deliberately matches the overly complicated
|
||||||
|
@ -30,6 +30,20 @@ func TestNodeKey(t *testing.T) {
|
|||||||
if full, got := string(bs), ":"+p.UntypedHexString(); !strings.HasSuffix(full, got) {
|
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)
|
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{}
|
z := NodePublic{}
|
||||||
if !z.IsZero() {
|
if !z.IsZero() {
|
||||||
|
Loading…
Reference in New Issue
Block a user