tailscale/tailscale · error
nested signatures must nest a signature
Error message
nested signatures must nest a signature
What it means
Thrown by verifySignature() when the signature declares SigKind==SigRotation but Nested is nil. A rotation signature must embed the signature it rotates, both to locate the verifying (wrapping) public key and to recurse the verification chain. This is the same structural defect as the authorizingKeyID() check, but hit on the verification path.
Source
Thrown at tka/sig.go:262
// verifySignature checks that the NodeKeySignature is authentic & certified
// 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 {
if s.SigKind != SigCredential {
nodeBytes, err := nodeKey.MarshalBinary()
if err != nil {
return fmt.Errorf("marshalling pubkey: %v", err)
}
if !bytes.Equal(nodeBytes, s.Pubkey) {
return errors.New("signature does not authorize nodeKey")
}
}
sigHash := s.SigHash()
switch s.SigKind {
case SigRotation:
if s.Nested == nil {
return errors.New("nested signatures must nest a signature")
}
// Verify the signature using the nested rotation key.
verifyPub, ok := s.Nested.wrappingPublic()
if !ok {
return errors.New("missing rotation key")
}
if len(verifyPub) != ed25519.PublicKeySize {
return fmt.Errorf("bad rotation key length: %d", len(verifyPub))
}
if !ed25519.Verify(ed25519.PublicKey(verifyPub[:]), sigHash[:], s.Signature) {
return errors.New("invalid signature")
}
// Recurse to verify the signature on the nested structure.
var nestedPub key.NodePublic
// SigCredential signatures certify an indirection key rather than a node
// key, so there's no need to check the node key.View on GitHub (pinned to 6e0912f979)
Solutions
- Re-create the rotation signature properly by nesting the previous signature and setting WrappingPubkey
- Validate the structural invariant (SigRotation implies Nested != nil) before invoking verification, and reject/quarantine bad signatures
- Check the CBOR bytes actually carry the nested field if the signature came off the wire
Example fix
// before
sig := tka.NodeKeySignature{SigKind: tka.SigRotation, Pubkey: newNodeKey, Signature: sigBytes}
err := authority.NodeKeyAuthorized(newNodeKey, sig) // error: must nest a signature
// after
sig.Nested = &prevSig
sig.WrappingPubkey = oldPub
err = authority.NodeKeyAuthorized(newNodeKey, sig) Defensive patterns
Strategy: validation
Validate before calling
if sig.SigKind == tka.SigRotation && sig.Nested == nil {
return errors.New("rejecting malformed rotation signature")
}
// only then:
err := authority.NodeKeyAuthorized(nodeKey, sig) Type guard
func isVerifiableSignature(s *tka.NodeKeySignature) bool {
if s.SigKind != tka.SigRotation { return true }
return s.Nested != nil && len(s.Nested.WrappingPubkey) > 0 || (s.Nested != nil && s.Nested.SigKind == tka.SigRotation)
} Try / catch
if err := sig.VerifySignature(nodeKey, trustedKey); err != nil {
// malformed or unauthentic: drop the signature; do not retry with mutation
} Prevention
- Validate AUM/signature structure at the trust boundary (post-decode, pre-verify)
- Fuzz CBOR decode paths so truncated signatures fail early with your own error
- Never construct SigRotation literals without the nested payload in production code
When it happens
Trigger: Authority.NodeKeyAuthorized / verifySignature on a SigRotation NodeKeySignature with Nested==nil; malformed or truncated signature received from the network or read from storage; struct literals built in tests without the nested field.
Common situations: Deserialization bugs that drop the nested pointer; peers on an older/newer serialization version; hand-crafted signatures in unit tests that skip the nesting step.
Related errors
- invalid signature: rotation signature missing nested signatu
- signature does not authorize nodeKey
- missing rotation key
- invalid signature
- missing checkpoint state
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/8b1c819658ccdf87.
Report an issue: GitHub.