FiloSottile/age · error
invalid tag recipient public key size %d
Error message
invalid tag recipient public key size %d
What it means
NewClassicRecipient requires a raw compressed P-256 public key of exactly 33 bytes (compressedPointSize = 1 + 32). If len(publicKey) differs, this error reports the actual size. It guards against keys in the wrong point encoding (e.g. uncompressed 65-byte keys) or truncated/garbage input.
Source
Thrown at tag/tag.go:72
return r, nil
case "tagpq":
r, err := NewHybridRecipient(k)
if err != nil {
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
}
return r, nil
default:
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
}
}
const compressedPointSize = 1 + 32
const uncompressedPointSize = 1 + 32 + 32
// NewClassicRecipient returns a new P-256 [Recipient] from a raw public key.
func NewClassicRecipient(publicKey []byte) (*Recipient, error) {
if len(publicKey) != compressedPointSize {
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)View on GitHub (pinned to b74dce4cdb)
Solutions
- Convert the key to compressed SEC1 form: use elliptic.MarshalCompressed(curve, x, y) or point.Bytes() on a nistec P256Point.
- If you have an uncompressed 65-byte key, drop the 0x04 prefix and compress: keep the 0x02/0x03 prefix plus x-coordinate.
- Verify the byte slice is not truncated or padded by checking its origin/export format.
- Alternatively parse the Bech32 recipient string via tag.ParseRecipient, which handles the expected encoding.
Example fix
// before r, err := tag.NewClassicRecipient(uncompressedPub) // 65 bytes // after x, y := elliptic.Unmarshal(elliptic.P256(), uncompressedPub) compressed := elliptic.MarshalCompressed(elliptic.P256(), x, y) // 33 bytes r, err := tag.NewClassicRecipient(compressed)
Defensive patterns
Strategy: validation
Validate before calling
const compressedPointSize = 33
if len(publicKey) != compressedPointSize {
return fmt.Errorf("need 33-byte compressed P-256 key, got %d bytes", len(publicKey))
}
r, err := tag.NewClassicRecipient(publicKey) Type guard
func isCompressedP256Key(b []byte) bool {
return len(b) == 33 && (b[0] == 0x02 || b[0] == 0x03)
} Prevention
- Always export P-256 keys in compressed SEC1 form (33 bytes, 0x02/0x03 prefix).
- Convert uncompressed keys with elliptic.MarshalCompressed before calling the API.
- Add a length+prefix assertion at the boundary where keys enter your code.
When it happens
Trigger: Calling tag.NewClassicRecipient(publicKey) with a byte slice whose length is not 33 — e.g. a 65-byte uncompressed SEC1 point, a raw 32-byte x-coordinate, or an empty/nil slice.
Common situations: Loading keys exported by tools that emit uncompressed points; passing x509/ecdsa keys without conversion; slicing errors when extracting the key from a larger buffer.
Related errors
- invalid tag recipient public key: %v
- failed to decrypt and authenticate final chunk: %w
- failed to decrypt and authenticate chunk at offset %d: %w
- wrong ecdh Curve
- failed to create hybrid public key: %v
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/90b9843a3d798669.
Report an issue: GitHub.