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

  1. 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).
  2. Pass the exact second stanza argument after base64-raw decoding, without truncation or re-slicing.
  3. Confirm the recipient is actually hybrid before applying hybrid length expectations (use r.Hybrid()).
  4. 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

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


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/fe55d307df86e704. Report an issue: GitHub.