tka: implement credential signatures (key material delegation)
This will be needed to support preauth-keys with network lock in the future, so getting the core mechanics out of the way now. Signed-off-by: Tom DNetto <tom@tailscale.com>pull/5513/head
parent
490acdefb6
commit
be95aebabd
|
@ -147,7 +147,7 @@ func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeK
|
||||||
SigKind: tka.SigDirect,
|
SigKind: tka.SigDirect,
|
||||||
KeyID: signer.KeyID(),
|
KeyID: signer.KeyID(),
|
||||||
Pubkey: p,
|
Pubkey: p,
|
||||||
RotationPubkey: nodeInfo.RotationPubkey,
|
WrappingPubkey: nodeInfo.RotationPubkey,
|
||||||
}
|
}
|
||||||
sig.Signature, err = signer.SignNKS(sig.SigHash())
|
sig.Signature, err = signer.SignNKS(sig.SigHash())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
80
tka/sig.go
80
tka/sig.go
|
@ -33,6 +33,19 @@ const (
|
||||||
// SigRotation signature and sign it again with their rotation key. That
|
// SigRotation signature and sign it again with their rotation key. That
|
||||||
// way, SigRotation nesting should only be 2 deep in the common case.
|
// way, SigRotation nesting should only be 2 deep in the common case.
|
||||||
SigRotation
|
SigRotation
|
||||||
|
// SigCredential describes a signature over a specifi public key, signed
|
||||||
|
// by a key in the tailnet key authority referenced by the specified keyID.
|
||||||
|
// In effect, SigCredential delegates the ability to make a signature to
|
||||||
|
// a different public/private key pair.
|
||||||
|
//
|
||||||
|
// It is intended that a different public/private key pair be generated
|
||||||
|
// for each different SigCredential that is created. Implementors must
|
||||||
|
// take care that the private side is only known to the entity that needs
|
||||||
|
// to generate the wrapping SigRotation signature, and it is immediately
|
||||||
|
// discarded after use.
|
||||||
|
//
|
||||||
|
// SigCredential is expected to be nested in a SigRotation signature.
|
||||||
|
SigCredential
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s SigKind) String() string {
|
func (s SigKind) String() string {
|
||||||
|
@ -43,6 +56,8 @@ func (s SigKind) String() string {
|
||||||
return "direct"
|
return "direct"
|
||||||
case SigRotation:
|
case SigRotation:
|
||||||
return "rotation"
|
return "rotation"
|
||||||
|
case SigCredential:
|
||||||
|
return "credential"
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("Sig?<%d>", int(s))
|
return fmt.Sprintf("Sig?<%d>", int(s))
|
||||||
}
|
}
|
||||||
|
@ -53,8 +68,9 @@ func (s SigKind) String() string {
|
||||||
type NodeKeySignature struct {
|
type NodeKeySignature struct {
|
||||||
// SigKind identifies the variety of signature.
|
// SigKind identifies the variety of signature.
|
||||||
SigKind SigKind `cbor:"1,keyasint"`
|
SigKind SigKind `cbor:"1,keyasint"`
|
||||||
// Pubkey identifies the public key which is being authorized.
|
// Pubkey identifies the key.NodePublic which is being authorized.
|
||||||
Pubkey []byte `cbor:"2,keyasint"`
|
// SigCredential signatures do not use this field.
|
||||||
|
Pubkey []byte `cbor:"2,keyasint,omitempty"`
|
||||||
|
|
||||||
// KeyID identifies which key in the tailnet key authority should
|
// KeyID identifies which key in the tailnet key authority should
|
||||||
// be used to verify this signature. Only set for SigDirect and
|
// be used to verify this signature. Only set for SigDirect and
|
||||||
|
@ -69,19 +85,23 @@ type NodeKeySignature struct {
|
||||||
// used as Pubkey. Only used for SigRotation signatures.
|
// used as Pubkey. Only used for SigRotation signatures.
|
||||||
Nested *NodeKeySignature `cbor:"5,keyasint,omitempty"`
|
Nested *NodeKeySignature `cbor:"5,keyasint,omitempty"`
|
||||||
|
|
||||||
// RotationPubkey specifies the ed25519 public key which may sign a
|
// WrappingPubkey specifies the ed25519 public key which must be used
|
||||||
// SigRotation signature, which embeds this one.
|
// to sign a Signature which embeds this one.
|
||||||
//
|
//
|
||||||
// Intermediate SigRotation signatures may omit this value to use the
|
// For SigRotation signatures multiple levels deep, intermediate
|
||||||
// parent one.
|
// signatures may omit this value, in which case the parent WrappingPubkey
|
||||||
RotationPubkey []byte `cbor:"6,keyasint,omitempty"`
|
// is used.
|
||||||
|
//
|
||||||
|
// SigCredential signatures use this field to specify the public key
|
||||||
|
// they are certifying, following the usual semanticsfor WrappingPubkey.
|
||||||
|
WrappingPubkey []byte `cbor:"6,keyasint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// rotationPublic returns the public key which must sign a SigRotation
|
// wrappingPublic returns the public key which must sign a signature which
|
||||||
// signature that embeds this signature, if any.
|
// embeds this one, if any.
|
||||||
func (s NodeKeySignature) rotationPublic() (pub ed25519.PublicKey, ok bool) {
|
func (s NodeKeySignature) wrappingPublic() (pub ed25519.PublicKey, ok bool) {
|
||||||
if len(s.RotationPubkey) > 0 {
|
if len(s.WrappingPubkey) > 0 {
|
||||||
return ed25519.PublicKey(s.RotationPubkey), true
|
return ed25519.PublicKey(s.WrappingPubkey), true
|
||||||
}
|
}
|
||||||
|
|
||||||
switch s.SigKind {
|
switch s.SigKind {
|
||||||
|
@ -89,7 +109,7 @@ func (s NodeKeySignature) rotationPublic() (pub ed25519.PublicKey, ok bool) {
|
||||||
if s.Nested == nil {
|
if s.Nested == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return s.Nested.rotationPublic()
|
return s.Nested.wrappingPublic()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, false
|
return nil, false
|
||||||
|
@ -138,15 +158,18 @@ func (s *NodeKeySignature) Unserialize(data []byte) error {
|
||||||
return dec.Unmarshal(data, s)
|
return dec.Unmarshal(data, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifySignature checks that the NodeKeySignature is authentic, certified
|
// verifySignature checks that the NodeKeySignature is authentic & certified
|
||||||
// by the given verificationKey, and authorizes the given nodeKey.
|
// by the given verificationKey. Additionally, SigDirect and SigRotation
|
||||||
|
// signatures are checked to ensure they authorize the given nodeKey.
|
||||||
func (s *NodeKeySignature) verifySignature(nodeKey key.NodePublic, verificationKey Key) error {
|
func (s *NodeKeySignature) verifySignature(nodeKey key.NodePublic, verificationKey Key) error {
|
||||||
nodeBytes, err := nodeKey.MarshalBinary()
|
if s.SigKind != SigCredential {
|
||||||
if err != nil {
|
nodeBytes, err := nodeKey.MarshalBinary()
|
||||||
return fmt.Errorf("marshalling pubkey: %v", err)
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("marshalling pubkey: %v", err)
|
||||||
if !bytes.Equal(nodeBytes, s.Pubkey) {
|
}
|
||||||
return errors.New("signature does not authorize nodeKey")
|
if !bytes.Equal(nodeBytes, s.Pubkey) {
|
||||||
|
return errors.New("signature does not authorize nodeKey")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sigHash := s.SigHash()
|
sigHash := s.SigHash()
|
||||||
|
@ -157,7 +180,7 @@ func (s *NodeKeySignature) verifySignature(nodeKey key.NodePublic, verificationK
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature using the nested rotation key.
|
// Verify the signature using the nested rotation key.
|
||||||
verifyPub, ok := s.Nested.rotationPublic()
|
verifyPub, ok := s.Nested.wrappingPublic()
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("missing rotation key")
|
return errors.New("missing rotation key")
|
||||||
}
|
}
|
||||||
|
@ -167,15 +190,22 @@ func (s *NodeKeySignature) verifySignature(nodeKey key.NodePublic, verificationK
|
||||||
|
|
||||||
// Recurse to verify the signature on the nested structure.
|
// Recurse to verify the signature on the nested structure.
|
||||||
var nestedPub key.NodePublic
|
var nestedPub key.NodePublic
|
||||||
if err := nestedPub.UnmarshalBinary(s.Nested.Pubkey); err != nil {
|
// SigCredential signatures certify an indirection key rather than a node
|
||||||
return fmt.Errorf("nested pubkey: %v", err)
|
// key, so theres no need to check the node key.
|
||||||
|
if s.Nested.SigKind != SigCredential {
|
||||||
|
if err := nestedPub.UnmarshalBinary(s.Nested.Pubkey); err != nil {
|
||||||
|
return fmt.Errorf("nested pubkey: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := s.Nested.verifySignature(nestedPub, verificationKey); err != nil {
|
if err := s.Nested.verifySignature(nestedPub, verificationKey); err != nil {
|
||||||
return fmt.Errorf("nested: %v", err)
|
return fmt.Errorf("nested: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case SigDirect:
|
case SigDirect, SigCredential:
|
||||||
|
if s.Nested != nil {
|
||||||
|
return fmt.Errorf("invalid signature: signatures of type %v cannot nest another signature", s.SigKind)
|
||||||
|
}
|
||||||
switch verificationKey.Kind {
|
switch verificationKey.Kind {
|
||||||
case Key25519:
|
case Key25519:
|
||||||
if ed25519consensus.Verify(ed25519.PublicKey(verificationKey.Public), sigHash[:], s.Signature) {
|
if ed25519consensus.Verify(ed25519.PublicKey(verificationKey.Public), sigHash[:], s.Signature) {
|
||||||
|
|
|
@ -67,7 +67,7 @@ func TestSigNested(t *testing.T) {
|
||||||
SigKind: SigDirect,
|
SigKind: SigDirect,
|
||||||
KeyID: k.ID(),
|
KeyID: k.ID(),
|
||||||
Pubkey: oldPub,
|
Pubkey: oldPub,
|
||||||
RotationPubkey: rPub,
|
WrappingPubkey: rPub,
|
||||||
}
|
}
|
||||||
sigHash := nestedSig.SigHash()
|
sigHash := nestedSig.SigHash()
|
||||||
nestedSig.Signature = ed25519.Sign(priv, sigHash[:])
|
nestedSig.Signature = ed25519.Sign(priv, sigHash[:])
|
||||||
|
@ -110,6 +110,13 @@ func TestSigNested(t *testing.T) {
|
||||||
if err := sig.verifySignature(node.Public(), k); err == nil {
|
if err := sig.verifySignature(node.Public(), k); err == nil {
|
||||||
t.Error("verifySignature(node) succeeded with bad outer signature")
|
t.Error("verifySignature(node) succeeded with bad outer signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test verification fails if the outer signature is signed with a
|
||||||
|
// different public key to whats specified in WrappingPubkey
|
||||||
|
sig.Signature = ed25519.Sign(priv, sigHash[:])
|
||||||
|
if err := sig.verifySignature(node.Public(), k); err == nil {
|
||||||
|
t.Error("verifySignature(node) succeeded with different signature")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSigNested_DeepNesting(t *testing.T) {
|
func TestSigNested_DeepNesting(t *testing.T) {
|
||||||
|
@ -128,7 +135,7 @@ func TestSigNested_DeepNesting(t *testing.T) {
|
||||||
SigKind: SigDirect,
|
SigKind: SigDirect,
|
||||||
KeyID: k.ID(),
|
KeyID: k.ID(),
|
||||||
Pubkey: oldPub,
|
Pubkey: oldPub,
|
||||||
RotationPubkey: rPub,
|
WrappingPubkey: rPub,
|
||||||
}
|
}
|
||||||
sigHash := nestedSig.SigHash()
|
sigHash := nestedSig.SigHash()
|
||||||
nestedSig.Signature = ed25519.Sign(priv, sigHash[:])
|
nestedSig.Signature = ed25519.Sign(priv, sigHash[:])
|
||||||
|
@ -175,6 +182,91 @@ func TestSigNested_DeepNesting(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSigCredential(t *testing.T) {
|
||||||
|
// Network-lock key (the key used to sign the nested sig)
|
||||||
|
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)
|
||||||
|
// The node key being certified
|
||||||
|
node := key.NewNode()
|
||||||
|
nodeKeyPub, _ := node.Public().MarshalBinary()
|
||||||
|
|
||||||
|
// The signature certifying delegated trust to another
|
||||||
|
// public key.
|
||||||
|
nestedSig := NodeKeySignature{
|
||||||
|
SigKind: SigCredential,
|
||||||
|
KeyID: k.ID(),
|
||||||
|
WrappingPubkey: cPub,
|
||||||
|
}
|
||||||
|
sigHash := nestedSig.SigHash()
|
||||||
|
nestedSig.Signature = ed25519.Sign(priv, sigHash[:])
|
||||||
|
|
||||||
|
// The signature authorizing the node key, signed by the
|
||||||
|
// delegated key & embedding the original signature.
|
||||||
|
sig := NodeKeySignature{
|
||||||
|
SigKind: SigRotation,
|
||||||
|
KeyID: k.ID(),
|
||||||
|
Pubkey: nodeKeyPub,
|
||||||
|
Nested: &nestedSig,
|
||||||
|
}
|
||||||
|
sigHash = sig.SigHash()
|
||||||
|
sig.Signature = ed25519.Sign(cPriv, sigHash[:])
|
||||||
|
if err := sig.verifySignature(node.Public(), k); err != nil {
|
||||||
|
t.Fatalf("verifySignature(node) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test verification fails if the wrong verification key is provided
|
||||||
|
kBad := Key{Kind: Key25519, Public: []byte{1, 2, 3, 4}, Votes: 2}
|
||||||
|
if err := sig.verifySignature(node.Public(), kBad); err == nil {
|
||||||
|
t.Error("verifySignature() did not error for wrong verification key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test someone can't misuse our public API for verifying node-keys
|
||||||
|
a, _ := Open(newTestchain(t, "G1\nG1.template = genesis",
|
||||||
|
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||||
|
Keys: []Key{k},
|
||||||
|
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||||
|
}})).Chonk())
|
||||||
|
if err := a.NodeKeyAuthorized(node.Public(), nestedSig.Serialize()); err == nil {
|
||||||
|
t.Error("NodeKeyAuthorized(SigCredential, node) did not fail")
|
||||||
|
}
|
||||||
|
// but that they can use it properly (nested in a SigRotation)
|
||||||
|
if err := a.NodeKeyAuthorized(node.Public(), sig.Serialize()); err != nil {
|
||||||
|
t.Errorf("NodeKeyAuthorized(SigRotation{SigCredential}, node) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test verification fails if the inner signature is invalid
|
||||||
|
tmp := make([]byte, ed25519.SignatureSize)
|
||||||
|
copy(tmp, nestedSig.Signature)
|
||||||
|
copy(nestedSig.Signature, []byte{1, 2, 3, 4})
|
||||||
|
if err := sig.verifySignature(node.Public(), k); err == nil {
|
||||||
|
t.Error("verifySignature(node) succeeded with bad inner signature")
|
||||||
|
}
|
||||||
|
copy(nestedSig.Signature, tmp)
|
||||||
|
|
||||||
|
// Test verification fails if the outer signature is invalid
|
||||||
|
copy(tmp, sig.Signature)
|
||||||
|
copy(sig.Signature, []byte{1, 2, 3, 4})
|
||||||
|
if err := sig.verifySignature(node.Public(), k); err == nil {
|
||||||
|
t.Error("verifySignature(node) succeeded with bad outer signature")
|
||||||
|
}
|
||||||
|
copy(sig.Signature, tmp)
|
||||||
|
|
||||||
|
// Test verification fails if we attempt to check a different node-key
|
||||||
|
otherNode := key.NewNode()
|
||||||
|
if err := sig.verifySignature(otherNode.Public(), k); err == nil {
|
||||||
|
t.Error("verifySignature(otherNode) succeeded with different principal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test verification fails if the outer signature is signed with a
|
||||||
|
// different public key to whats specified in WrappingPubkey
|
||||||
|
sig.Signature = ed25519.Sign(priv, sigHash[:])
|
||||||
|
if err := sig.verifySignature(node.Public(), k); err == nil {
|
||||||
|
t.Error("verifySignature(node) succeeded with different signature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSigSerializeUnserialize(t *testing.T) {
|
func TestSigSerializeUnserialize(t *testing.T) {
|
||||||
nodeKeyPub := []byte{1, 2, 3, 4}
|
nodeKeyPub := []byte{1, 2, 3, 4}
|
||||||
pub, priv := testingKey25519(t, 1)
|
pub, priv := testingKey25519(t, 1)
|
||||||
|
|
|
@ -673,6 +673,10 @@ func (a *Authority) NodeKeyAuthorized(nodeKey key.NodePublic, nodeKeySignature t
|
||||||
if err := decoded.Unserialize(nodeKeySignature); err != nil {
|
if err := decoded.Unserialize(nodeKeySignature); err != nil {
|
||||||
return fmt.Errorf("unserialize: %v", err)
|
return fmt.Errorf("unserialize: %v", err)
|
||||||
}
|
}
|
||||||
|
if decoded.SigKind == SigCredential {
|
||||||
|
return errors.New("credential signatures cannot authorize nodes on their own")
|
||||||
|
}
|
||||||
|
|
||||||
key, err := a.state.GetKey(decoded.KeyID)
|
key, err := a.state.GetKey(decoded.KeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("key: %v", err)
|
return fmt.Errorf("key: %v", err)
|
||||||
|
|
Loading…
Reference in New Issue