Skip to content

crypto/hpke: Sender and Recipient reuse AEAD nonces after sequence counter wrap #80342

Description

@SphereDonout

Go version

go1.26.0 darwin/arm64

What did you do?

I inspected the crypto/hpke sequence-number and nonce-derivation logic for stateful Sender.Seal and Recipient.Open.
Current implementation:

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
}

func (r *Recipient) Open(aad, ciphertext []byte) ([]byte, error) {
	if r.aead == nil {
		return nil, errors.New("export-only instantiation")
	}
	plaintext, err := r.aead.Open(nil, r.nextNonce(), ciphertext, aad)
	if err != nil {
		return nil, err
	}
	r.seqNum++
	return plaintext, nil
}

func (ctx *context) nextNonce() []byte {
	nonce := make([]byte, ctx.aead.NonceSize())
	byteorder.BEPutUint64(nonce[len(nonce)-8:], ctx.seqNum)
	for i := range ctx.baseNonce {
		nonce[i] ^= ctx.baseNonce[i]
	}
	return nonce
}

A package-local regression test can reproduce the issue by forcing seqNum to math.MaxUint64, calling nextNonce, incrementing once more through Seal or Open, and observing that the next nonce sequence wraps and starts repeating from the initial nonce stream.

What did you see happen?

Sender.Seal and Recipient.Open derive nonces directly from a uint64 sequence number and then increment that counter with no wrap check.

After 2^64 successful operations on the same HPKE context, seqNum wraps to zero and nextNonce() repeats the same nonce stream under the same AEAD key.

That means the stateful HPKE context can eventually reuse AEAD nonces. For nonce-based AEADs, nonce reuse under the same key breaks the security assumptions of the construction and can undermine confidentiality and integrity.
This affects the stateful context methods:

  • (*Sender).Seal after 2^64 calls
  • (*Recipient).Open after 2^64 successful calls

The one-shot top-level helpers hpke.Seal and hpke.Open instantiate a fresh context each time, so this issue is about long-lived sender/recipient contexts.

What did you expect to see?

I expected crypto/hpke to reject further use of a context before sequence wrap can cause nonce reuse.

For example, once the maximum sequence number is exhausted, Seal and Open should start returning an error instead of wrapping seqNum and reusing nonces.

More generally, a long-lived HPKE context should preserve the invariant that the same (key, nonce) pair is never reused across successful AEAD operations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DocumentationIssues describing a change to documentation.NeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions