Go version
go1.26.0 darwin/arm64
What did you do?
I checked the capacity of the shared-key slice returned by crypto/mlkem encapsulation.
Minimal reproducer:
package main
import (
"crypto/mlkem"
"fmt"
)
func main() {
dk768, err := mlkem.GenerateKey768()
if err != nil {
panic(err)
}
k768, _ := dk768.EncapsulationKey().Encapsulate()
fmt.Printf("ML-KEM-768 shared key: len=%d cap=%d\n", len(k768), cap(k768))
fmt.Printf("ML-KEM-768 extra reachable bytes: %d\n", len(k768[:cap(k768)][mlkem.SharedKeySize:]))
dk1024, err := mlkem.GenerateKey1024()
if err != nil {
panic(err)
}
k1024, _ := dk1024.EncapsulationKey().Encapsulate()
fmt.Printf("ML-KEM-1024 shared key: len=%d cap=%d\n", len(k1024), cap(k1024))
fmt.Printf("ML-KEM-1024 extra reachable bytes: %d\n", len(k1024[:cap(k1024)][mlkem.SharedKeySize:]))
}
The source-level root cause is in the internal encapsulation helpers. Both helpers compute G = K || r, then return K as G[:SharedKeySize] without a full slice expression or clone:
G := g.Sum(nil)
K, r := G[:SharedKeySize], G[SharedKeySize:]
c = pkeEncrypt(cc, &ek.encryptionKey, m, r)
return K, c
The same pattern exists for ML-KEM-1024:
G := g.Sum(nil)
K, r := G[:SharedKeySize], G[SharedKeySize:]
c = pkeEncrypt1024(cc, &ek.encryptionKey1024, m, r)
return K, c
What did you see happen?
The returned shared key has length 32, but its backing array can still expose the remaining bytes of G.
Expected representative output:
ML-KEM-768 shared key: len=32 cap=64
ML-KEM-768 extra reachable bytes: 32
ML-KEM-1024 shared key: len=32 cap=64
ML-KEM-1024 extra reachable bytes: 32
A caller can reslice the returned value and read the adjacent encapsulation randomness r:
expanded := sharedKey[:cap(sharedKey)]
extra := expanded[mlkem.SharedKeySize:]
This does not expose the decapsulation private key, and the caller already receives the shared key itself. However, it does expose internal encapsulation randomness that is not part of the documented returned shared key. It also increases accidental leakage risk in caller code that incorrectly serializes, logs, hashes, or zeroes based on cap(sharedKey) instead of len(sharedKey).
What did you expect to see?
I expected the returned shared-key slice to have no extra capacity beyond its documented length:
len(sharedKey) == mlkem.SharedKeySize
cap(sharedKey) == mlkem.SharedKeySize
The fix appears small: return a capacity-clipped slice or clone the first 32 bytes before returning.
For example:
return G[:SharedKeySize:SharedKeySize], c
or:
K := bytes.Clone(G[:SharedKeySize])
return K, c
Go version
go1.26.0 darwin/arm64
What did you do?
I checked the capacity of the shared-key slice returned by crypto/mlkem encapsulation.
Minimal reproducer:
The source-level root cause is in the internal encapsulation helpers. Both helpers compute
G = K || r, then returnKasG[:SharedKeySize]without a full slice expression or clone:The same pattern exists for ML-KEM-1024:
What did you see happen?
The returned shared key has length
32, but its backing array can still expose the remaining bytes ofG.Expected representative output:
A caller can reslice the returned value and read the adjacent encapsulation randomness
r:This does not expose the decapsulation private key, and the caller already receives the shared key itself. However, it does expose internal encapsulation randomness that is not part of the documented returned shared key. It also increases accidental leakage risk in caller code that incorrectly serializes, logs, hashes, or zeroes based on
cap(sharedKey)instead oflen(sharedKey).What did you expect to see?
I expected the returned shared-key slice to have no extra capacity beyond its documented length:
The fix appears small: return a capacity-clipped slice or clone the first
32bytes before returning.For example:
or: