kem

package
v1.26.12 Latest Latest
Warning

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

Go to latest
Published: May 11, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

Documentation

Overview

Package kem implements post-quantum key encapsulation for the Lux node peer handshake. ML-KEM-768 (FIPS 203, NIST PQ Cat 3) is the default session KEM; ML-KEM-1024 (NIST PQ Cat 5) is mandatory for high-value validator and DKG channels.

The session shared secret is derived through one FIPS 203 encapsulate / decapsulate dance and then bound to the handshake transcript via cSHAKE256 with customization "LUX_NODE_AEAD_V1". The bound key is the AEAD session key used by every subsequent frame on the connection.

One and only one KEM family is admitted on a strict-PQ profile: ML-KEM. Classical X25519 / ECDH are present in the wire enum solely as forbidden markers so audit tooling can name a misconfiguration precisely; a strict-PQ peer with ForbidClassicalKEM=true refuses to handshake against any peer offering KeyExchangeX25519Unsafe.

KeyExchangeID is a type alias of config.KeyExchangeID; the canonical wire bytes (0x01 = ML-KEM-768, 0x02 = ML-KEM-1024, 0x90 = X25519Unsafe) live in the consensus config package and are shared with protocol/auth. Closes the "three disjoint KEM ID registries" drift named in the spec/code audit under Bug 3.

Index

Constants

View Source
const (

	// TranscriptHashSize is the byte width of the bound handshake
	// transcript hash returned by HashTranscript and stored in KEMSession.
	// 48 bytes = 384 bits matches HashSuiteSHA3NIST in consensus/config and
	// the chain-wide profile ProfileHash width.
	TranscriptHashSize = 48

	// SharedSecretSize is the FIPS 203 ML-KEM shared-secret width in
	// bytes. ML-KEM-768 and ML-KEM-1024 both emit 32 bytes (256 bits).
	SharedSecretSize = 32

	// AEADKeySize is the byte width of the derived AEAD session key.
	// 32 bytes targets ChaCha20-Poly1305 or AES-256-GCM.
	AEADKeySize = 32
)

Customization strings for the cSHAKE256 derivations performed by this package. Pinned at "_V1"; bumping the tag invalidates every prior derived key (which is the correct behaviour for a hard-fork of the encoding).

LUX_NODE_AEAD_V1 — peer-to-peer session AEAD key LUX_NODE_DKG_V1 — DKG channel AEAD key (high-value, ML-KEM-1024 only) LUX_NODE_TRANSCRIPT_V1 — TupleHash256 customization for handshake transcript

View Source
const (
	// KeyExchangeNone — alias for config.KeyExchangeInvalid (0x00). Rejected
	// by every strict-PQ peer.
	KeyExchangeNone = config.KeyExchangeInvalid

	// KeyExchangeMLKEM768 — FIPS 203 ML-KEM-768 (NIST PQ Cat 3, byte 0x01).
	// The production default for peer-to-peer session keys on the Lux
	// primary network. 1184-byte public key, 1088-byte ciphertext, 32-byte
	// shared secret.
	KeyExchangeMLKEM768 = config.KeyExchangeMLKEM768

	// KeyExchangeMLKEM1024 — FIPS 203 ML-KEM-1024 (NIST PQ Cat 5, byte 0x02).
	// Mandatory for high-value validator channels and Pulsar DKG sessions.
	// 1568-byte public key, 1568-byte ciphertext, 32-byte shared secret.
	KeyExchangeMLKEM1024 = config.KeyExchangeMLKEM1024

	// KeyExchangeX25519Unsafe — classical X25519 (byte 0x90). Carried as an
	// explicit forbidden marker; a strict-PQ peer refuses to handshake
	// against any peer offering this byte.
	KeyExchangeX25519Unsafe = config.KeyExchangeX25519Unsafe
)
View Source
const DKGChannelScheme = KeyExchangeMLKEM1024

DKGChannelScheme is the only KeyExchangeID admissible on a validator-to- validator DKG channel. Pinned at KeyExchangeMLKEM1024 (NIST PQ Cat 5) so the Pulsar / Pulsar-M Pedersen DKG over R_q runs under the strongest FIPS 203 parameter set available.

Anyone constructing a DKG session in pulsar / pulsar-m MUST go through NewDKGKEMSession (or assert this constant explicitly) so a future refactor that silently flips the byte to ML-KEM-768 fails the strict-PQ profile gate rather than silently downgrading.

Variables

View Source
var (
	// ErrUnsupportedScheme is returned when the requested KeyExchangeID is
	// not a supported ML-KEM variant. Includes None and any forbidden
	// classical marker.
	ErrUnsupportedScheme = errors.New("kem: unsupported scheme")

	// ErrClassicalKEMForbidden is returned when a strict-PQ caller receives
	// or attempts to negotiate an explicit classical KEM marker
	// (X25519Unsafe / P256Unsafe / P384Unsafe).
	ErrClassicalKEMForbidden = errors.New("kem: classical KEM forbidden in strict-PQ mode")

	// ErrBadCiphertextSize is returned when the encapsulated ciphertext is
	// not the scheme's canonical CiphertextSize.
	ErrBadCiphertextSize = errors.New("kem: ciphertext size does not match scheme")

	// ErrBadPublicKeySize is returned when the peer's encapsulation key is
	// not the scheme's canonical PublicKeySize.
	ErrBadPublicKeySize = errors.New("kem: public key size does not match scheme")

	// ErrBadPrivateKeySize is returned when the caller-supplied
	// decapsulation key is not the scheme's canonical PrivateKeySize.
	ErrBadPrivateKeySize = errors.New("kem: private key size does not match scheme")

	// ErrEmptyTranscript is returned when transcript bytes are nil or
	// empty. The whole point of transcript binding is that the AEAD key
	// depends on every prior handshake message; a zero-length transcript
	// is a contract violation.
	ErrEmptyTranscript = errors.New("kem: transcript is empty")
)

Errors returned by KEM session APIs. Each names exactly which contract was violated so callers can produce a precise diagnostic.

View Source
var ErrDKGSchemeMismatch = errors.New("kem: DKG channels MUST use ML-KEM-1024")

ErrDKGSchemeMismatch is returned by NewDKGKEMSession when the caller supplies a scheme other than DKGChannelScheme. A strict-PQ profile that declares HighValueKEM=ML-KEM-1024 but observes a DKG attempt under ML-KEM-768 MUST surface this error to the operator — the scheme mismatch is a configuration drift, not a tactical preference.

View Source
var ErrUnsupportedAEAD = errors.New("kem: unsupported AEAD")

ErrUnsupportedAEAD is returned by NewAEAD when an unknown / forbidden AEAD byte is requested.

Functions

func AssertDKGCompliance

func AssertDKGCompliance(sessionScheme, profileHighValueKEM KeyExchangeID) error

AssertDKGCompliance reports nil iff sessionScheme and profileHighValueKEM are both ML-KEM-1024. The strict-PQ profile cross-axis check: a profile that declares HighValueKEM=ML-KEM-1024 MUST observe ML-KEM-1024 on the wire; anything else is a configuration drift that audit tooling rejects at boot.

This is the single dispatch point for the "DKG channel scheme matches profile high-value KEM" predicate; pulsar / pulsar-m callers route through it instead of comparing the bytes locally.

func CiphertextSize

func CiphertextSize(scheme KeyExchangeID) (int, error)

CiphertextSize returns the canonical KEM ciphertext byte width for scheme.

func GenerateKEMKeypair

func GenerateKEMKeypair(scheme KeyExchangeID, randSrc io.Reader) (pub, priv []byte, err error)

GenerateKEMKeypair returns a fresh (encapsulation-key, decapsulation-key) pair for scheme. randSrc may be nil, in which case crypto/rand.Reader is used. Returns ErrUnsupportedScheme for anything outside the supported ML-KEM family.

Sizes:

MLKEM768   pub=1184 priv=2400 bytes
MLKEM1024  pub=1568 priv=3168 bytes

func HashTranscript

func HashTranscript(msgs ...[]byte) [TranscriptHashSize]byte

HashTranscript returns the canonical cSHAKE256 / TupleHash256 commitment to msgs in the order they appear. Customization is "LUX_NODE_TRANSCRIPT_V1" so two different handshake protocols cannot produce the same hash from the same byte content.

The encoding is SP 800-185 TupleHash256: each input is left-encoded with its bit-length prefix, the right-encoded total output bit-length is appended, and the result is fed to cSHAKE256 with the function-name "TupleHash" and customisation "LUX_NODE_TRANSCRIPT_V1". This matches the helper used by ChainSecurityProfile.ComputeHash, so the same transcript hash byte-encodes identically across the node and consensus.

func NISTCategory added in v1.26.12

func NISTCategory(scheme KeyExchangeID) uint8

NISTCategory reports the NIST PQ security category for scheme. Cat 3 targets AES-192 / SHA-384 strength; Cat 5 targets AES-256 / SHA-512. Returns 0 for invalid and forbidden markers.

Free function for the same reason as SharedSecretBits.

func NewAEAD

func NewAEAD(id AEADID, key []byte) (cipher.AEAD, error)

NewAEAD constructs a cipher.AEAD bound to key. Caller produces key via KEMSession.DeriveAEADKey or KEMSession.DeriveDKGAEADKey. Returns ErrUnsupportedAEAD for any byte other than AEADChaCha20Poly1305.

This is the single dispatch point that maps an AEADID byte to a concrete cipher; transport code routes through it instead of importing chacha20poly1305 directly so a future AEAD addition (e.g. AES-256-GCM at 0x02) lands in one place.

func PublicKeySize

func PublicKeySize(scheme KeyExchangeID) (int, error)

PublicKeySize returns the canonical encapsulation-key byte width for scheme. Useful for fixed-width framing on the wire.

func SharedSecretBits added in v1.26.12

func SharedSecretBits(scheme KeyExchangeID) uint16

SharedSecretBits reports the shared-secret width emitted by scheme in bits. Both ML-KEM-768 and ML-KEM-1024 emit 256-bit secrets per FIPS 203. Returns 0 for invalid and forbidden markers.

Free function rather than a method because KeyExchangeID is a type alias of config.KeyExchangeID; methods can only be declared in the package that defines the underlying type.

Types

type AEADID

type AEADID uint8

AEADID is the wire byte identifying the authenticated cipher used on post-handshake frames. Only FIPS-approved primitives are admitted; the production default is ChaCha20-Poly1305 (RFC 8439) because the Lux strict-PQ profile pairs lattice KEM with a stream cipher that is not vulnerable to the same lattice attack surface.

Numbering:

0x00 — None (rejected)
0x01 — ChaCha20-Poly1305 (RFC 8439, FIPS 140 module-approved)
const (
	AEADNone             AEADID = 0x00
	AEADChaCha20Poly1305 AEADID = 0x01
)

func (AEADID) String

func (a AEADID) String() string

String returns the canonical wire name.

type ActiveProfile

type ActiveProfile struct {
	// Name is the canonical profile name for log lines and audit reports.
	// Production strict-PQ deployments use "LUX_STRICT_E2E_PQ".
	Name string

	// NodeIdentity is the wire name of the per-node identity signature
	// scheme. Strict-PQ: "ML-DSA-65".
	NodeIdentity string

	// SessionKEM is the KEM byte for peer-to-peer session keys. Strict-PQ
	// default: KeyExchangeMLKEM768.
	SessionKEM KeyExchangeID

	// DKGKEM is the KEM byte for validator-to-validator DKG channels.
	// Strict-PQ mandates KeyExchangeMLKEM1024.
	DKGKEM KeyExchangeID

	// HashSuite is the wire name of the transcript / commitment hash
	// family. Strict-PQ: "SHA3_NIST".
	HashSuite string

	// PostQuantumE2E reports whether the profile asserts end-to-end PQ
	// (KEM, identity, threshold finality all PQ).
	PostQuantumE2E bool

	// ForbidClassicalKEM is the strict-PQ refusal switch: when true, any
	// peer offering an IsForbiddenInPQMode() KEM is refused at handshake.
	ForbidClassicalKEM bool
}

ActiveProfile is the small subset of strict-PQ profile fields the network layer needs at boot to log the active posture and refuse peers that disagree. The full ChainSecurityProfile lives in consensus/config and is the authoritative source; this struct is the network-layer projection of that profile.

func LuxStrictE2EPQ

func LuxStrictE2EPQ() ActiveProfile

LuxStrictE2EPQ returns the canonical Lux strict-PQ profile projection for the network layer. Mirrors the consensus-config profile of the same shape; values here are pinned so a node that boots without an explicit profile still advertises the strict posture by default.

func (ActiveProfile) Banner

func (p ActiveProfile) Banner() string

Banner returns the multi-line operator-readable banner that node startup MUST emit on boot under a strict-PQ profile. Fixed format so log scrapers can match deterministically.

SECURITY PROFILE: LUX_STRICT_E2E_PQ
NODE IDENTITY:    ML-DSA-65
SESSION KEM:      ML-KEM-768
DKG KEM:          ML-KEM-1024
HASH SUITE:       SHA3_NIST
POST-QUANTUM E2E: true

func (ActiveProfile) Validate

func (p ActiveProfile) Validate() error

Validate refuses an ActiveProfile whose axes are internally inconsistent before the network layer trusts it. Mirrors the strict-PQ portion of ChainSecurityProfile.Validate.

type KEMSession

type KEMSession struct {
	// SchemeID is the FIPS 203 ML-KEM scheme this session was negotiated
	// under. Strict-PQ peers admit only MLKEM768 (default) and MLKEM1024
	// (high-value / DKG).
	SchemeID KeyExchangeID

	// SharedSecret is the 256-bit raw KEM output. Treat as confidential;
	// derive subordinate keys through cSHAKE256 (see DeriveAEADKey), do
	// not use directly as an AEAD key.
	SharedSecret [SharedSecretSize]byte

	// TranscriptHash is the 384-bit cSHAKE256 / TupleHash256 commitment to
	// the handshake transcript at the point of KEM completion. The hash
	// is BOUND into DeriveAEADKey so a flipped transcript byte produces a
	// distinct AEAD key — that is, an attacker who downgrades a single
	// handshake field gets a key the honest peer never derived.
	TranscriptHash [TranscriptHashSize]byte
}

KEMSession is the result of one successful ML-KEM encapsulate / decapsulate. It carries the scheme byte, the raw 256-bit shared secret, and the 384-bit running transcript hash (cSHAKE256 / TupleHash256 over every handshake message produced before the KEM round, in canonical order). The AEAD key is derived from SharedSecret + TranscriptHash via cSHAKE256 customisation "LUX_NODE_AEAD_V1".

Field invariants enforced by package APIs:

  • SchemeID ∈ {MLKEM768, MLKEM1024}; any other scheme returns ErrUnsupportedScheme at construction time.
  • SharedSecret is the raw FIPS 203 ML-KEM.Decapsulate / Encapsulate output. Length is always exactly SharedSecretSize.
  • TranscriptHash is the 48-byte SHA3-384 (cSHAKE256) commitment over the handshake transcript. Two distinct transcripts MUST produce two distinct TranscriptHash values.

KEMSession does not zero its SharedSecret on drop because Go does not provide a deterministic destructor; callers that need defensive zeroing must do it explicitly after deriving the AEAD key.

func InitiateDKGKEMSession

func InitiateDKGKEMSession(
	scheme KeyExchangeID,
	peerKEMPub []byte,
	transcript []byte,
) (*KEMSession, []byte, error)

InitiateDKGKEMSession is the DKG-channel-flavoured wrapper around InitiateKEMSession. The scheme axis is fixed at ML-KEM-1024; supplying any other scheme returns ErrDKGSchemeMismatch before any KEM work runs.

The returned KEMSession is otherwise byte-identical to what InitiateKEMSession(MLKEM1024, ...) returns; downstream code may call DeriveDKGAEADKey on it to bind the AEAD key under the "LUX_NODE_DKG_V1" customisation.

func InitiateKEMSession

func InitiateKEMSession(
	scheme KeyExchangeID,
	peerKEMPub []byte,
	transcript []byte,
) (*KEMSession, []byte, error)

InitiateKEMSession performs the initiator side of a one-shot ML-KEM session establishment. peerKEMPub is the responder's encapsulation key (already received on the wire); transcript is the bound running hash over every handshake message produced before this call.

Returns the resulting KEMSession plus the encapsulation ciphertext to send to the responder. The caller is responsible for binding the ciphertext into the post-KEM transcript before deriving subordinate keys (the cSHAKE256 customisation inside DeriveAEADKey hard-binds the transcript bytes the caller passes here).

func RespondDKGKEMSession

func RespondDKGKEMSession(
	scheme KeyExchangeID,
	ourKEMSec []byte,
	peerCiphertext []byte,
	transcript []byte,
) (*KEMSession, error)

RespondDKGKEMSession is the responder-side analogue of InitiateDKGKEMSession. Same scheme-pin rule applies.

func RespondKEMSession

func RespondKEMSession(
	scheme KeyExchangeID,
	ourKEMSec []byte,
	peerCiphertext []byte,
	transcript []byte,
) (*KEMSession, error)

RespondKEMSession performs the responder side: decapsulate the initiator's ciphertext under the responder's decapsulation key and produce the shared session.

transcript MUST be byte-identical to the transcript the initiator used. If the two sides disagree by even one byte, their derived AEAD keys diverge and the first encrypted frame fails authentication.

func (*KEMSession) DeriveAEADKey

func (s *KEMSession) DeriveAEADKey() [AEADKeySize]byte

DeriveAEADKey returns the 256-bit AEAD session key bound to this KEM session's shared secret AND its transcript hash. Uses cSHAKE256 with customisation "LUX_NODE_AEAD_V1"; the function name is "KEMDerive".

Two sessions with the same SharedSecret but different TranscriptHash produce two different keys. Two sessions with the same TranscriptHash but different SharedSecret also produce two different keys. This is the property the strict-PQ profile depends on for downgrade resistance.

func (*KEMSession) DeriveDKGAEADKey

func (s *KEMSession) DeriveDKGAEADKey() ([AEADKeySize]byte, error)

DeriveDKGAEADKey returns the 256-bit AEAD session key bound to this KEM session under the DKG-channel customisation "LUX_NODE_DKG_V1". A DKG channel session is required by the strict-PQ profile to negotiate ML-KEM-1024; this function refuses to derive a key on any other scheme so a misconfigured caller fails loud at key derivation rather than silently downgrading.

Returns ErrUnsupportedScheme if the session was negotiated under anything other than KeyExchangeMLKEM1024.

type KeyExchangeID

type KeyExchangeID = config.KeyExchangeID

KeyExchangeID is the wire byte that identifies the key-exchange / KEM scheme a peer offers for session-key establishment. Type-aliased to config.KeyExchangeID so the wire format and predicates (String, IsPostQuantum, IsForbiddenInPQMode) are shared with the consensus security profile.

Canonical numbering:

0x00 — Invalid / unspecified (rejected by every strict-PQ verifier)
0x01 — ML-KEM-768  (FIPS 203 Cat 3; production default)
0x02 — ML-KEM-1024 (FIPS 203 Cat 5; DKG / high-value channels)
0x90 — X25519Unsafe (classical; explicit forbidden marker in strict-PQ)

The forbidden markers are NEVER produced by a strict-PQ node; they exist solely so a strict-PQ peer that receives one can refuse with a precise diagnostic instead of dropping silently.

ML-KEM-512 (NIST Cat 1) is intentionally not exported — it sits below the strict-PQ floor; the node does not negotiate Cat 1 sessions.

Jump to

Keyboard shortcuts

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