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

  1. Pass a real AEAD to NewSender, e.g. AES-128-GCM (hpke.AES128GCM) or ChaCha20Poly1305, instead of the export-only AEAD.
  2. If you only need derived secrets, call Sender.Export(exporterContext, length) rather than Seal().
  3. 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

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.