Skip to content

crypto/mlkem: Encapsulate returns shared-key slices with extra secret capacity #80343

Description

@SphereDonout

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions