{"record":{"id":"8b1c819658ccdf87","repo":"tailscale/tailscale","slug":"nested-signatures-must-nest-a-signature","errorCode":null,"errorMessage":"nested signatures must nest a signature","messagePattern":"nested signatures must nest a signature","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tka/sig.go","lineNumber":262,"sourceCode":"// verifySignature checks that the NodeKeySignature is authentic & certified\n// by the given verificationKey. Additionally, SigDirect and SigRotation\n// signatures are checked to ensure they authorize the given nodeKey.\nfunc (s *NodeKeySignature) verifySignature(nodeKey key.NodePublic, verificationKey Key) error {\n\tif s.SigKind != SigCredential {\n\t\tnodeBytes, err := nodeKey.MarshalBinary()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"marshalling pubkey: %v\", err)\n\t\t}\n\t\tif !bytes.Equal(nodeBytes, s.Pubkey) {\n\t\t\treturn errors.New(\"signature does not authorize nodeKey\")\n\t\t}\n\t}\n\n\tsigHash := s.SigHash()\n\tswitch s.SigKind {\n\tcase SigRotation:\n\t\tif s.Nested == nil {\n\t\t\treturn errors.New(\"nested signatures must nest a signature\")\n\t\t}\n\n\t\t// Verify the signature using the nested rotation key.\n\t\tverifyPub, ok := s.Nested.wrappingPublic()\n\t\tif !ok {\n\t\t\treturn errors.New(\"missing rotation key\")\n\t\t}\n\t\tif len(verifyPub) != ed25519.PublicKeySize {\n\t\t\treturn fmt.Errorf(\"bad rotation key length: %d\", len(verifyPub))\n\t\t}\n\t\tif !ed25519.Verify(ed25519.PublicKey(verifyPub[:]), sigHash[:], s.Signature) {\n\t\t\treturn errors.New(\"invalid signature\")\n\t\t}\n\n\t\t// Recurse to verify the signature on the nested structure.\n\t\tvar nestedPub key.NodePublic\n\t\t// SigCredential signatures certify an indirection key rather than a node\n\t\t// key, so there's no need to check the node key.","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/tka/sig.go#L244-L280","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nsig := tka.NodeKeySignature{SigKind: tka.SigRotation, Pubkey: newNodeKey, Signature: sigBytes}\nerr := authority.NodeKeyAuthorized(newNodeKey, sig) // error: must nest a signature\n\n// after\nsig.Nested = &prevSig\nsig.WrappingPubkey = oldPub\nerr = authority.NodeKeyAuthorized(newNodeKey, sig)","handlingStrategy":"validation","validationCode":"if sig.SigKind == tka.SigRotation && sig.Nested == nil {\n\treturn errors.New(\"rejecting malformed rotation signature\")\n}\n// only then:\nerr := authority.NodeKeyAuthorized(nodeKey, sig)","typeGuard":"func isVerifiableSignature(s *tka.NodeKeySignature) bool {\n\tif s.SigKind != tka.SigRotation { return true }\n\treturn s.Nested != nil && len(s.Nested.WrappingPubkey) > 0 || (s.Nested != nil && s.Nested.SigKind == tka.SigRotation)\n}","tryCatchPattern":"if err := sig.VerifySignature(nodeKey, trustedKey); err != nil {\n\t// malformed or unauthentic: drop the signature; do not retry with mutation\n}","preventionTips":["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"],"tags":["go","tailscale","tailnet-lock","signature","validation"],"backgroundTag":"malformed-signature","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}