ipn/ipnlocal: discard node keys that have been rotated out

A non-signing node can be allowed to re-sign its new node keys following
key renewal/rotation (e.g. via `tailscale up --force-reauth`). To be
able to do this, node's TLK is written into WrappingPubkey field of the
initial SigDirect signature, signed by a signing node.

The intended use of this field implies that, for each WrappingPubkey, we
typically expect to have at most one active node with a signature
tracing back to that key. Multiple valid signatures referring to the
same WrappingPubkey can occur if a client's state has been cloned, but
it's something we explicitly discourage and don't support:
https://tailscale.com/s/clone

This change propagates rotation details (wrapping public key, a list
of previous node keys that have been rotated out) to netmap processing,
and adds tracking of obsolete node keys that, when found, will get
filtered out.

Updates tailscale/corp#19764

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov
2024-05-09 07:23:03 +01:00
committed by Anton Tolchanov
parent 42cfbf427c
commit 01847e0123
6 changed files with 464 additions and 56 deletions
+75
View File
@@ -304,3 +304,78 @@ func (s *NodeKeySignature) verifySignature(nodeKey key.NodePublic, verificationK
return fmt.Errorf("unhandled signature type: %v", s.SigKind)
}
}
// RotationDetails holds additional information about a nodeKeySignature
// of kind SigRotation.
type RotationDetails struct {
// PrevNodeKeys is a list of node keys which have been rotated out.
PrevNodeKeys []key.NodePublic
// WrappingPubkey is the public key which has been authorized to sign
// this rotating signature.
WrappingPubkey []byte
}
// rotationDetails returns the RotationDetails for a SigRotation signature.
func (s *NodeKeySignature) rotationDetails() (*RotationDetails, error) {
if s.SigKind != SigRotation {
return nil, nil
}
sri := &RotationDetails{}
nested := s.Nested
for nested != nil {
if len(nested.Pubkey) > 0 {
var nestedPub key.NodePublic
if err := nestedPub.UnmarshalBinary(nested.Pubkey); err != nil {
return nil, fmt.Errorf("nested pubkey: %v", err)
}
sri.PrevNodeKeys = append(sri.PrevNodeKeys, nestedPub)
}
if nested.SigKind != SigRotation {
break
}
nested = nested.Nested
}
sri.WrappingPubkey = nested.WrappingPubkey
return sri, nil
}
// ResignNKS re-signs a node-key signature for a new node-key.
//
// This only matters on network-locked tailnets, because node-key signatures are
// how other nodes know that a node-key is authentic. When the node-key is
// rotated then the existing signature becomes invalid, so this function is
// responsible for generating a new wrapping signature to certify the new node-key.
//
// The signature itself is a SigRotation signature, which embeds the old signature
// and certifies the new node-key as a replacement for the old by signing the new
// signature with RotationPubkey (which is the node's own network-lock key).
func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.MarshaledSignature) (tkatype.MarshaledSignature, error) {
var oldSig NodeKeySignature
if err := oldSig.Unserialize(oldNKS); err != nil {
return nil, fmt.Errorf("decoding NKS: %w", err)
}
nk, err := nodeKey.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("marshalling node-key: %w", err)
}
if bytes.Equal(nk, oldSig.Pubkey) {
// The old signature is valid for the node-key we are using, so just
// use it verbatim.
return oldNKS, nil
}
newSig := NodeKeySignature{
SigKind: SigRotation,
Pubkey: nk,
Nested: &oldSig,
}
if newSig.Signature, err = priv.SignNKS(newSig.SigHash()); err != nil {
return nil, fmt.Errorf("signing NKS: %w", err)
}
return newSig.Serialize(), nil
}
+141
View File
@@ -5,6 +5,7 @@ package tka
import (
"crypto/ed25519"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
@@ -298,3 +299,143 @@ func TestSigSerializeUnserialize(t *testing.T) {
t.Errorf("unmarshalled version differs (-want, +got):\n%s", diff)
}
}
func TestNodeKeySignatureRotationDetails(t *testing.T) {
// Trusted network lock key
pub, priv := testingKey25519(t, 1)
k := Key{Kind: Key25519, Public: pub, Votes: 2}
// 'credential' key (the one being delegated to)
cPub, cPriv := testingKey25519(t, 2)
n1, n2, n3 := key.NewNode(), key.NewNode(), key.NewNode()
n1pub, _ := n1.Public().MarshalBinary()
n2pub, _ := n2.Public().MarshalBinary()
n3pub, _ := n3.Public().MarshalBinary()
tests := []struct {
name string
nodeKey key.NodePublic
sigFn func() NodeKeySignature
want *RotationDetails
}{
{
name: "SigDirect",
nodeKey: n1.Public(),
sigFn: func() NodeKeySignature {
s := NodeKeySignature{
SigKind: SigDirect,
KeyID: pub,
Pubkey: n1pub,
}
sigHash := s.SigHash()
s.Signature = ed25519.Sign(priv, sigHash[:])
return s
},
want: nil,
},
{
name: "SigWrappedCredential",
nodeKey: n1.Public(),
sigFn: func() NodeKeySignature {
nestedSig := NodeKeySignature{
SigKind: SigCredential,
KeyID: pub,
WrappingPubkey: cPub,
}
sigHash := nestedSig.SigHash()
nestedSig.Signature = ed25519.Sign(priv, sigHash[:])
sig := NodeKeySignature{
SigKind: SigRotation,
Pubkey: n1pub,
Nested: &nestedSig,
}
sigHash = sig.SigHash()
sig.Signature = ed25519.Sign(cPriv, sigHash[:])
return sig
},
want: &RotationDetails{
WrappingPubkey: cPub,
},
},
{
name: "SigRotation",
nodeKey: n2.Public(),
sigFn: func() NodeKeySignature {
nestedSig := NodeKeySignature{
SigKind: SigDirect,
Pubkey: n1pub,
KeyID: pub,
WrappingPubkey: cPub,
}
sigHash := nestedSig.SigHash()
nestedSig.Signature = ed25519.Sign(priv, sigHash[:])
sig := NodeKeySignature{
SigKind: SigRotation,
Pubkey: n2pub,
Nested: &nestedSig,
}
sigHash = sig.SigHash()
sig.Signature = ed25519.Sign(cPriv, sigHash[:])
return sig
},
want: &RotationDetails{
WrappingPubkey: cPub,
PrevNodeKeys: []key.NodePublic{n1.Public()},
},
},
{
name: "SigRotationNestedTwice",
nodeKey: n3.Public(),
sigFn: func() NodeKeySignature {
initialSig := NodeKeySignature{
SigKind: SigDirect,
Pubkey: n1pub,
KeyID: pub,
WrappingPubkey: cPub,
}
sigHash := initialSig.SigHash()
initialSig.Signature = ed25519.Sign(priv, sigHash[:])
prevRotation := NodeKeySignature{
SigKind: SigRotation,
Pubkey: n2pub,
Nested: &initialSig,
}
sigHash = prevRotation.SigHash()
prevRotation.Signature = ed25519.Sign(cPriv, sigHash[:])
sig := NodeKeySignature{
SigKind: SigRotation,
Pubkey: n3pub,
Nested: &prevRotation,
}
sigHash = sig.SigHash()
sig.Signature = ed25519.Sign(cPriv, sigHash[:])
return sig
},
want: &RotationDetails{
WrappingPubkey: cPub,
PrevNodeKeys: []key.NodePublic{n2.Public(), n1.Public()},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sig := tt.sigFn()
if err := sig.verifySignature(tt.nodeKey, k); err != nil {
t.Fatalf("verifySignature(node) failed: %v", err)
}
got, err := sig.rotationDetails()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("rotationDetails() = %v, want %v", got, tt.want)
}
})
}
}
+16 -5
View File
@@ -668,25 +668,36 @@ func (a *Authority) Inform(storage Chonk, updates []AUM) error {
// NodeKeyAuthorized checks if the provided nodeKeySignature authorizes
// the given node key.
func (a *Authority) NodeKeyAuthorized(nodeKey key.NodePublic, nodeKeySignature tkatype.MarshaledSignature) error {
_, err := a.NodeKeyAuthorizedWithDetails(nodeKey, nodeKeySignature)
return err
}
// NodeKeyAuthorized checks if the provided nodeKeySignature authorizes
// the given node key, and returns RotationDetails if the signature is
// a valid rotation signature.
func (a *Authority) NodeKeyAuthorizedWithDetails(nodeKey key.NodePublic, nodeKeySignature tkatype.MarshaledSignature) (*RotationDetails, error) {
var decoded NodeKeySignature
if err := decoded.Unserialize(nodeKeySignature); err != nil {
return fmt.Errorf("unserialize: %v", err)
return nil, fmt.Errorf("unserialize: %v", err)
}
if decoded.SigKind == SigCredential {
return errors.New("credential signatures cannot authorize nodes on their own")
return nil, errors.New("credential signatures cannot authorize nodes on their own")
}
kID, err := decoded.authorizingKeyID()
if err != nil {
return err
return nil, err
}
key, err := a.state.GetKey(kID)
if err != nil {
return fmt.Errorf("key: %v", err)
return nil, fmt.Errorf("key: %v", err)
}
return decoded.verifySignature(nodeKey, key)
if err := decoded.verifySignature(nodeKey, key); err != nil {
return nil, err
}
return decoded.rotationDetails()
}
// KeyTrusted returns true if the given keyID is trusted by the tailnet