{"record":{"id":"597db6c58ae51f06","repo":"FiloSottile/age","slug":"invalid-tagpq-recipient-public-key-v","errorCode":null,"errorMessage":"invalid tagpq recipient public key: %v","messagePattern":"invalid tagpq recipient public key: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tag/tag.go","lineNumber":90,"sourceCode":"\t\treturn nil, fmt.Errorf(\"invalid tag recipient public key size %d\", len(publicKey))\n\t}\n\tp, err := nistec.NewP256Point().SetBytes(publicKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid tag recipient public key: %v\", err)\n\t}\n\tk, err := hpke.DHKEM(ecdh.P256()).NewPublicKey(p.Bytes())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid tag recipient public key: %v\", err)\n\t}\n\treturn &Recipient{k}, nil\n}\n\n// NewHybridRecipient returns a new hybrid P-256 + ML-KEM-768 [Recipient] from\n// raw concatenated public keys.\nfunc NewHybridRecipient(publicKey []byte) (*Recipient, error) {\n\tk, err := hpke.MLKEM768P256().NewPublicKey(publicKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid tagpq recipient public key: %v\", err)\n\t}\n\treturn &Recipient{k}, nil\n}\n\n// Hybrid reports whether r is a hybrid P-256 + ML-KEM-768 recipient.\nfunc (r *Recipient) Hybrid() bool {\n\treturn r.pk.KEM().ID() == hpke.MLKEM768P256().ID()\n}\n\nfunc (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\ts, _, err := r.WrapWithLabels(fileKey)\n\treturn s, err\n}\n\n// Tag computes the 4-byte tag for the given ciphertext enc.\n//\n// This is a low-level method exposed for use by plugins that implement\n// identities compatible with tagged recipients.","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/FiloSottile/age/blob/b74dce4cdbe35b5e5f66c06d9612b72f89028758/tag/tag.go#L72-L108","documentation":"NewHybridRecipient builds a hybrid P-256 + ML-KEM-768 age recipient from raw concatenated public keys. It passes the bytes to hpke.MLKEM768P256().NewPublicKey, which requires exactly ML-KEM-768 encapsulation key size (1184 bytes) plus a valid uncompressed P-256 point (65 bytes), i.e. 1249 bytes total. If parsing fails, the error wraps the underlying HPKE parse failure.","triggerScenarios":"Calling NewHybridRecipient with a byte slice that is not 1184+65 bytes, that concatenates keys in the wrong order (P-256 first), that contains a compressed P-256 point instead of uncompressed, or that holds a non-canonical/out-of-curve P-256 point. Also produced indirectly when ParseRecipient parses a Bech32 'age1tagpq1...' recipient whose decoded payload is malformed.","commonSituations":"Truncating or mis-serializing keys when storing them in a database; concatenating a compressed (33-byte) P-256 key; reading a 'tag' (classic) key and feeding it to NewHybridRecipient; copy/paste or encoding errors in plugin code handling age1tagpq1 recipients.","solutions":["Verify publicKey is exactly 1249 bytes: 1184-byte ML-KEM-768 encapsulation key followed by a 65-byte uncompressed P-256 point (0x04 || X || Y).","Check key ordering: the ML-KEM-768 key must come first, the P-256 key second.","If starting from a compressed P-256 point, decompress it to uncompressed form (or use NewClassicRecipient for classic tag recipients).","If the input is a Bech32 string, use ParseRecipient instead of manual decoding; its error message names the malformed recipient.","Regenerate or re-export the key pair from the source plugin/hardware token to rule out corruption."],"exampleFix":"// before\nr, err := tag.NewHybridRecipient(append(p256Pub, mlkemPub...)) // wrong order/size\n// after\nif len(mlkemPub) != 1184 || len(p256Pub) != 65 {\n    return fmt.Errorf(\"bad key sizes: mlkem=%d p256=%d\", len(mlkemPub), len(p256Pub))\n}\nr, err := tag.NewHybridRecipient(append(slices.Clip(mlkemPub), p256Pub...))","handlingStrategy":"validation","validationCode":"const hybridKeySize = 1184 + 65 // mlkem.EncapsulationKeySize768 + uncompressedPointSize\nfunc validHybridKey(b []byte) bool {\n    return len(b) == hybridKeySize && b[1184] == 0x04 // uncompressed P-256 point\n}","typeGuard":"func isHybridKeySize(b []byte) bool { return len(b) == 1184+65 }","tryCatchPattern":"r, err := tag.NewHybridRecipient(pubKey)\nif err != nil {\n    return fmt.Errorf(\"rejecting tagpq key (%d bytes): %w\", len(pubKey), err)\n}","preventionTips":["Assert len(publicKey) == 1249 (1184 + 65) before calling NewHybridRecipient.","Store ML-KEM-768 key first, P-256 uncompressed point second.","Use ParseRecipient on Bech32 strings instead of manual decoding.","Reject compressed P-256 points (33 bytes) at input boundaries."],"tags":["go","age","hpke","public-key","post-quantum","key-parsing"],"backgroundTag":"invalid-public-key","analyzedSha":"b74dce4cdbe35b5e5f66c06d9612b72f89028758","analyzedAt":"2026-08-31T23:59:31.627Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}