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

  1. 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).
  2. Check key ordering: the ML-KEM-768 key must come first, the P-256 key second.
  3. If starting from a compressed P-256 point, decompress it to uncompressed form (or use NewClassicRecipient for classic tag recipients).
  4. If the input is a Bech32 string, use ParseRecipient instead of manual decoding; its error message names the malformed recipient.
  5. 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

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


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