mlkem768incr

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package mlkem768incr implements the INCREMENTAL ML-KEM-768 key encapsulation used by Signal's Sparse Post-Quantum Ratchet (SPQR), as a pure-Go port (no cgo) of the libcrux-ml-kem 0.0.8 `incremental` API that SPQR v1.5.1 depends on. Standard ML-KEM-768 transports the whole encapsulation key at once; the incremental variant splits it into a small header (pk1) and a chunkable encapsulation key (pk2), and encapsulates in two phases (encapsulate1 emits the u-ciphertext from the header alone; encapsulate2 emits the v-ciphertext once the pk2 chunk arrives). This lets the bulky encapsulation key be transported in pieces across SPQR epochs.

The package is built in two layers, each verified independently (ADR 0003):

  • The FIPS-203 ML-KEM-768 PKE/KEM core — modeled on Go stdlib's FIPS-203 implementation, and verified against (1) the stdlib crypto/mlkem package for end-to-end shared-secret equality and (2) the NIST/ACVP ML-KEM-768 known-answer tests, which pin the intermediate byte encodings (catching a compensating-error pair that an end-to-end-only check could mask).

  • The libcrux incremental split/serialization layer (pk1/pk2 framing, two-phase encapsulation, the EncapsState byte layout including the issue-1275 endianness handling, and decapsulate_compressed_key) — verified byte-for-byte against known-answer vectors generated from libcrux-ml-kem 0.0.8 itself (the exact revision SPQR v1.5.1 pins).

Byte-exactness with libcrux is the deliverable: SPQR serializes these KEM bytes into its own wire/state, so any divergence breaks mainline interop.

Index

Constants

View Source
const (
	// PublicKey1Size is the header: ρ (32) ‖ H(ek) (32).
	PublicKey1Size = 64
	// PublicKey2Size is the chunked encapsulation key: ByteEncode₁₂(t̂).
	PublicKey2Size = k * encodingSize12 // 1152
	// DecapsulationKeySize is the standard expanded dk: ByteEncode₁₂(ŝ) ‖ ek ‖
	// H(ek) ‖ z.
	DecapsulationKeySize = k*encodingSize12 + EncapsulationKeySize768 + 32 + 32 // 2400
	// Ciphertext1Size is Compress₁₀(u).
	Ciphertext1Size = k * encodingSize10 // 960
	// Ciphertext2Size is Compress₄(v).
	Ciphertext2Size = encodingSize4 // 128

	// EncapsStateSize is the saved phase-1 state: r̂ (k polys) ‖ e₂ (1 poly) ‖
	// m (32). = 3*512 + 512 + 32 = 2080.
	EncapsStateSize = k*rawPolyI16Size + rawPolyI16Size + messageSize
)

Incremental ML-KEM-768 wire sizes (libcrux 0.0.8, K=3).

View Source
const (
	SharedKeySize           = 32
	SeedSize                = 64                               // d ‖ z
	CiphertextSize768       = k*encodingSize10 + encodingSize4 // 1088
	EncapsulationKeySize768 = k*encodingSize12 + 32            // 1184

)

Standard ML-KEM-768 byte sizes (FIPS 203).

Variables

This section is empty.

Functions

func DecapsulateCompressedKey

func DecapsulateCompressedKey(dk, ct1, ct2 []byte) ([]byte, error)

DecapsulateCompressedKey reconstructs the standard ciphertext ct1 ‖ ct2 and runs the ordinary FIPS-203 ML-KEM decaps (with constant-time implicit rejection) against the 2400-byte expanded decapsulation key. libcrux decapsulate_compressed_key.

func Encapsulate2

func Encapsulate2(state, pk2 []byte) ([]byte, error)

Encapsulate2 finishes encapsulation: it parses the EncapsState (applying the libcrux issue-1275 endianness fix if needed), decodes t̂ from pk2, computes v = NTT⁻¹(t̂ᵀ◦r̂) + e₂ + Decompress₁(m), and returns ct2 = Compress₄(v).

func FixEncapsStateEndianness

func FixEncapsStateEndianness(state []byte) ([]byte, error)

FixEncapsStateEndianness detects and repairs an EncapsState whose polynomial int16s were serialized with swapped endianness by a libcrux SIMD backend (cryspen/libcrux#1275). It inspects the e₂ region (state[1536:2048]), whose coefficients are CBD η2 samples — all in [-2,2]. Read as little-endian int16, a correct encoding shows only {0, 1, 2, -1, -2}; a byte-swapped one shows the swapped images {0x0100, 0x0200, 0xFEFF}. On the first decisive coefficient we either keep the state (correct) or swap every int16 pair in state[0:len-32] (the trailing 32 random bytes have no endianness and are never swapped). A state of all-ambiguous {0,-1} values, or any unexpected value, is left as-is (matching SPQR's keep-and-warn fallback).

func ValidatePublicKeyParts

func ValidatePublicKeyParts(pk1, pk2 []byte) error

ValidatePublicKeyParts checks that a header (pk1) and chunked encaps key (pk2) are consistent: it reconstructs the standard public key (t̂ ‖ ρ), verifies H(ek) matches the header's hash, and checks t̂ is in the valid domain (the ByteDecode₁₂ modulus check). libcrux validate_pk_bytes.

Types

type DecapsulationKey768

type DecapsulationKey768 struct {
	// contains filtered or unexported fields
}

DecapsulationKey768 is an expanded FIPS-203 ML-KEM-768 decapsulation key. It holds the seed (d‖z), ρ, H(ek), and the expanded encryption/decryption keys. The key material is secret; see String for redaction.

func NewDecapsulationKey768

func NewDecapsulationKey768(seed []byte) (*DecapsulationKey768, error)

NewDecapsulationKey768 expands a decapsulation key from a 64-byte seed (d‖z), per FIPS 203. The seed must be uniformly random and kept secret.

func (*DecapsulationKey768) Bytes

func (dk *DecapsulationKey768) Bytes() []byte

Bytes returns the 64-byte seed form (d‖z) of the decapsulation key (secret).

func (*DecapsulationKey768) Decapsulate

func (dk *DecapsulationKey768) Decapsulate(ciphertext []byte) ([]byte, error)

Decapsulate implements ML-KEM.Decaps (FIPS 203, Algorithm 18) with implicit rejection, constant-time. ciphertext must be CiphertextSize768 bytes.

func (*DecapsulationKey768) EncapsulationKey

func (dk *DecapsulationKey768) EncapsulationKey() *EncapsulationKey768

EncapsulationKey returns the public encapsulation key.

func (DecapsulationKey768) Format

func (dk DecapsulationKey768) Format(f fmt.State, _ rune)

Format implements fmt.Formatter, redacting the secret key material under every verb. Without this, %#v dumps the seed (d), implicit-rejection secret (z), and secret vector ŝ as a raw struct, and %x dumps their bytes. Value receiver so a value copy redacts too. The embedded encryptionKey is public (t̂/Â) but is redacted along with the rest for a single uniform secret-key rendering; use EncapsulationKey() to print the public key. (FIPS-203 secret-leak convention; see curve.PrivateKey.Format.)

func (DecapsulationKey768) String

func (dk DecapsulationKey768) String() string

String redacts the secret key material so a decapsulation key never leaks into logs. Value receiver (matching curve.PrivateKey) so a value copy — not just a pointer — also redacts under %v/%s.

type EncapsulationKey768

type EncapsulationKey768 struct {
	// contains filtered or unexported fields
}

EncapsulationKey768 is an expanded FIPS-203 ML-KEM-768 encapsulation key.

func NewEncapsulationKey768

func NewEncapsulationKey768(b []byte) (*EncapsulationKey768, error)

NewEncapsulationKey768 parses the standard 1184-byte encapsulation key, checking the modulus (FIPS 203 §7.2 step 2 via polyByteDecode).

func (*EncapsulationKey768) Bytes

func (ek *EncapsulationKey768) Bytes() []byte

Bytes returns the standard 1184-byte ML-KEM-768 encapsulation key encoding.

func (*EncapsulationKey768) EncapsulateInternal

func (ek *EncapsulationKey768) EncapsulateInternal(m *[messageBytes]byte) (sharedKey, ciphertext []byte)

EncapsulateInternal is the derandomized ML-KEM.Encaps (FIPS 203, Algorithm 17) with the 32-byte message m supplied (for KATs). Encapsulate (production) draws m from a CSPRNG.

type EncapsulationResult

type EncapsulationResult struct {
	Ciphertext1  []byte // Compress₁₀(u) (Ciphertext1Size)
	EncapsState  []byte // saved (r̂ ‖ e₂ ‖ m) (EncapsStateSize)
	SharedSecret []byte // K = G(m‖H(ek))[:32]
}

EncapsulationResult holds the phase-1 encapsulation outputs.

func Encapsulate1Internal

func Encapsulate1Internal(pk1 []byte, m *[messageBytes]byte) (*EncapsulationResult, error)

Encapsulate1Internal is the derandomized phase-1 encapsulation with the 32-byte message m supplied (for KATs); production callers draw m from a CSPRNG. pk1 is the 64-byte header.

K = G(m‖H(ek))[:32]; the rest is K-PKE.Encrypt's r/e-sampling and the u half:

r̂[i]  = NTT(CBD_η1(PRF(r, i))),    i∈[0,k)
e₁[i] = CBD_η2(PRF(r, k+i)),       i∈[0,k)
e₂    = CBD_η2(PRF(r, 2k))
u     = NTT⁻¹(Âᵀ◦r̂) + e₁;  ct1 = Compress₁₀(u)

(r̂, e₂, m) are stashed in the EncapsState for phase 2.

type IncrementalKey

type IncrementalKey struct {
	PK1 []byte // header (PublicKey1Size)
	PK2 []byte // chunked encaps key (PublicKey2Size)
	DK  []byte // expanded decapsulation key (DecapsulationKeySize)
}

IncrementalKey is a freshly generated incremental ML-KEM-768 key, split into its header, chunked encapsulation key, and (standard, expanded) decapsulation key. Mirrors SPQR's generate() output (incremental_mlkem768.rs).

func GenerateIncrementalKey

func GenerateIncrementalKey(seed []byte) (*IncrementalKey, error)

GenerateIncrementalKey expands an incremental ML-KEM-768 key from a 64-byte seed (d‖z), per FIPS 203 KeyGen, and serializes it into the incremental split. This is libcrux's KeyPairCompressedBytes::from_seed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL