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

  1. Convert the key to compressed SEC1 form: use elliptic.MarshalCompressed(curve, x, y) or point.Bytes() on a nistec P256Point.
  2. If you have an uncompressed 65-byte key, drop the 0x04 prefix and compress: keep the 0x02/0x03 prefix plus x-coordinate.
  3. Verify the byte slice is not truncated or padded by checking its origin/export format.
  4. 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

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


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