FiloSottile/age · error
invalid ciphertext size
Error message
invalid ciphertext size
What it means
Tag computes the 4-byte correlation tag over an HPKE ciphertext (enc). For a hybrid recipient the enc must be exactly mlkem.CiphertextSize768 (1088) + uncompressedPointSize (65) = 1153 bytes. Any other length is rejected because the HKDF input would be structurally wrong.
Source
Thrown at tag/tag.go:116
}
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.
func (r *Recipient) Tag(enc []byte) ([]byte, error) {
label, tagRecipient := "age-encryption.org/p256tag", r.Bytes()
if r.Hybrid() {
label = "age-encryption.org/mlkem768p256tag"
// In hybrid mode, the tag is computed over just the P-256 part.
tagRecipient = tagRecipient[mlkem.EncapsulationKeySize768:]
if len(enc) != mlkem.CiphertextSize768+uncompressedPointSize {
return nil, fmt.Errorf("invalid ciphertext size")
}
} else if len(enc) != uncompressedPointSize {
return nil, fmt.Errorf("invalid ciphertext size")
}
rh := sha256.Sum256(tagRecipient)
tag, err := hkdf.Extract(sha256.New, append(slices.Clip(enc), rh[:4]...), []byte(label))
if err != nil {
return nil, fmt.Errorf("failed to compute tag: %v", err)
}
return tag[:4], nil
}
// WrapWithLabels implements [age.RecipientWithLabels], returning a single
// "postquantum" label if r is a hybrid P-256 + ML-KEM-768 recipient. This
// ensures a hybrid Recipient can't be mixed with other recipients that would
// defeat its post-quantum security.
//
// To unsafely bypass this restriction, wrap Recipient in an [age.Recipient]View on GitHub (pinned to b74dce4cdb)
Solutions
- Ensure enc is the full raw HPKE encapsulation key: 1088-byte ML-KEM-768 ciphertext plus 65-byte uncompressed P-256 point (1153 bytes total).
- Pass the exact second stanza argument after base64-raw decoding, without truncation or re-slicing.
- Confirm the recipient is actually hybrid before applying hybrid length expectations (use r.Hybrid()).
- Regenerate the stanza via WrapWithLabels if the enc value came from external storage and may be corrupted.
Example fix
// before
if len(enc) != 65 { return errors.New("bad enc") }
tag, err := r.Tag(enc)
// after
want := 65
if r.Hybrid() { want = 1088 + 65 }
if len(enc) != want { return fmt.Errorf("bad enc size %d, want %d", len(enc), want) }
tag, err := r.Tag(enc) Defensive patterns
Strategy: validation
Validate before calling
const hybridEncSize = 1088 + 65 // mlkem.CiphertextSize768 + uncompressedPointSize
func validHybridEnc(enc []byte) bool { return len(enc) == hybridEncSize } Type guard
func isHybridEnc(enc []byte, r *tag.Recipient) bool { return r.Hybrid() && len(enc) == 1088+65 } Try / catch
tagBytes, err := r.Tag(enc)
if err != nil {
return fmt.Errorf("tag computation failed (hybrid enc len=%d): %w", len(enc), err)
} Prevention
- Hybrid enc must be exactly 1153 bytes (1088 + 65).
- Decode the full stanza base64 argument; never slice it.
- Check r.Hybrid() to pick the expected length.
When it happens
Trigger: Calling Recipient.Tag(enc) with an enc slice whose length differs from 1153 bytes while the recipient is hybrid — e.g. passing only the P-256 part, a truncated/stanza-mangled enc, or an enc produced for a classic recipient.
Common situations: Plugin authors implementing compatible identities re-deriving the tag from a stanza's second argument and getting a decoded value of the wrong length due to base64 corruption or manual slicing; mixing classic and hybrid stanzas.
Related errors
- invalid tagpq recipient public key: %v
- 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/fe55d307df86e704.
Report an issue: GitHub.