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.
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:
A package-local regression test can reproduce the issue by forcing
seqNumtomath.MaxUint64, callingnextNonce, incrementing once more throughSealorOpen, and observing that the next nonce sequence wraps and starts repeating from the initial nonce stream.What did you see happen?
Sender.SealandRecipient.Openderive nonces directly from auint64sequence number and then increment that counter with no wrap check.After 2^64 successful operations on the same HPKE context,
seqNumwraps to zero andnextNonce()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).Sealafter 2^64 calls(*Recipient).Openafter 2^64 successful callsThe one-shot top-level helpers
hpke.Sealandhpke.Openinstantiate 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,
SealandOpenshould start returning an error instead of wrappingseqNumand 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.