FiloSottile/age · error
invalid tagpq recipient public key: %v
Error message
invalid tagpq recipient public key: %v
What it means
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.
Source
Thrown at tag/tag.go:90
return nil, fmt.Errorf("invalid tag recipient public key size %d", len(publicKey))
}
p, err := nistec.NewP256Point().SetBytes(publicKey)
if err != nil {
return nil, fmt.Errorf("invalid tag recipient public key: %v", err)
}
k, err := hpke.DHKEM(ecdh.P256()).NewPublicKey(p.Bytes())
if err != nil {
return nil, fmt.Errorf("invalid tag recipient public key: %v", err)
}
return &Recipient{k}, nil
}
// NewHybridRecipient returns a new hybrid P-256 + ML-KEM-768 [Recipient] from
// raw concatenated public keys.
func NewHybridRecipient(publicKey []byte) (*Recipient, error) {
k, err := hpke.MLKEM768P256().NewPublicKey(publicKey)
if err != nil {
return nil, fmt.Errorf("invalid tagpq recipient public key: %v", err)
}
return &Recipient{k}, nil
}
// Hybrid reports whether r is a hybrid P-256 + ML-KEM-768 recipient.
func (r *Recipient) Hybrid() bool {
return r.pk.KEM().ID() == hpke.MLKEM768P256().ID()
}
func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
s, _, err := r.WrapWithLabels(fileKey)
return s, err
}
// Tag computes the 4-byte tag for the given ciphertext enc.
//
// This is a low-level method exposed for use by plugins that implement
// identities compatible with tagged recipients.View on GitHub (pinned to b74dce4cdb)
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.
Example fix
// before
r, err := tag.NewHybridRecipient(append(p256Pub, mlkemPub...)) // wrong order/size
// after
if len(mlkemPub) != 1184 || len(p256Pub) != 65 {
return fmt.Errorf("bad key sizes: mlkem=%d p256=%d", len(mlkemPub), len(p256Pub))
}
r, err := tag.NewHybridRecipient(append(slices.Clip(mlkemPub), p256Pub...)) Defensive patterns
Strategy: validation
Validate before calling
const hybridKeySize = 1184 + 65 // mlkem.EncapsulationKeySize768 + uncompressedPointSize
func validHybridKey(b []byte) bool {
return len(b) == hybridKeySize && b[1184] == 0x04 // uncompressed P-256 point
} Type guard
func isHybridKeySize(b []byte) bool { return len(b) == 1184+65 } Try / catch
r, err := tag.NewHybridRecipient(pubKey)
if err != nil {
return fmt.Errorf("rejecting tagpq key (%d bytes): %w", len(pubKey), err)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- invalid ciphertext size
- failed to set up HPKE sender: %v
- failed to create hybrid public key: %v
- failed to encrypt file key: %v
- ArmoredWriter already closed
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/597db6c58ae51f06.
Report an issue: GitHub.