golang/go · error
export-only instantiation
Error message
export-only instantiation
What it means
Returned by Sender.Seal when the HPKE context was instantiated with the export-only AEAD (AEAD ID 0xFFFF per RFC 9180 §7.3). An export-only context computes the shared secret and exporter secret but intentionally leaves context.aead nil, so it can derive secrets via Export() but cannot perform authenticated encryption. Seal/Open explicitly guard against this by checking s.aead == nil.
Source
Thrown at src/crypto/hpke/hpke.go:173
sharedSecret, err := k.decap(enc)
if err != nil {
return nil, err
}
context, err := newContext(sharedSecret, k.KEM().ID(), kdf, aead, info)
if err != nil {
return nil, err
}
return &Recipient{context}, nil
}
// Seal encrypts the provided plaintext, optionally binding to the additional
// public data aad.
//
// Seal uses incrementing counters for each call, and Open on the receiving side
// must be called in the same order as Seal.
func (s *Sender) Seal(aad, plaintext []byte) ([]byte, error) {
if s.aead == nil {
return nil, errors.New("export-only instantiation")
}
ciphertext := s.aead.Seal(nil, s.nextNonce(), plaintext, aad)
s.seqNum++
return ciphertext, nil
}
// Seal instantiates a single-use HPKE sending HPKE context like [NewSender],
// and then encrypts the provided plaintext like [Sender.Seal] (with no aad).
// Seal returns the concatenation of the encapsulated key and the ciphertext.
func Seal(pk PublicKey, kdf KDF, aead AEAD, info, plaintext []byte) ([]byte, error) {
enc, s, err := NewSender(pk, kdf, aead, info)
if err != nil {
return nil, err
}
ct, err := s.Seal(nil, plaintext)
if err != nil {
return nil, err
}View on GitHub (pinned to b6b368adc5)
Solutions
- Pass a real AEAD to NewSender, e.g. AES-128-GCM (hpke.AES128GCM) or ChaCha20Poly1305, instead of the export-only AEAD.
- If you only need derived secrets, call Sender.Export(exporterContext, length) rather than Seal().
- Verify the AEAD before constructing the Sender: check aead.ID() != 0xFFFF before calling NewSender.
Example fix
// before aead := hpke.ExportOnly() enc, s, err := hpke.NewSender(pk, kdf, aead, info) ct, err := s.Seal(nil, plaintext) // returns "export-only instantiation" // after aead := hpke.AES128GCM() enc, s, err := hpke.NewSender(pk, kdf, aead, info) ct, err := s.Seal(nil, plaintext)
Defensive patterns
Strategy: validation
Validate before calling
// Reject export-only AEAD before constructing the sender.
func newEncryptingSender(pk hpke.PublicKey, kdf hpke.KDF, aead hpke.AEAD, info []byte) (enc []byte, s *hpke.Sender, err error) {
if aead == nil || aead.ID() == 0xFFFF {
return nil, nil, errors.New("aead is export-only; cannot Seal")
}
return hpke.NewSender(pk, kdf, aead, info)
} Try / catch
// Treat as a programmer error, not a recoverable runtime condition.
ct, err := s.Seal(aad, plaintext)
if err != nil {
if err.Error() == "export-only instantiation" {
log.Fatal("sender built with export-only AEAD; pass AES128GCM/ChaCha20Poly1305")
}
return err
} Prevention
- Centralize ciphersuite selection in one helper that never returns the export-only AEAD for encryption contexts.
- Add a unit test that asserts Seal succeeds on every (KEM,KDF,AEAD) tuple your app uses.
- Treat AEAD ID 0xFFFF as a derivation-only mode in your mental model and docs.
When it happens
Trigger: Calling (*Sender).Seal(aad, plaintext) on a Sender produced by NewSender with the export-only AEAD (e.g. hpke.ExportOnly() or a custom AEAD whose aead() returns a nil cipher.AEAD). Also reachable if a user constructs a context-like value whose aead field is nil.
Common situations: Developers copy the ciphersuite triple (KEM, KDF, AEAD) from a spec table and pick the 0xFFFF export-only AEAD ID by mistake. Or they intend to use Export() for key derivation but call Seal() expecting encryption to work. Sometimes seen when porting RFC 9180 test vectors that exercise the export-only mode.
Related errors
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/581868fa638007be.
Report an issue: GitHub.