mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
The live guard refused to start a chain whose K was below the number of validators that existed. But sampling is stake-weighted: a validator is drawn in proportion to what it staked, however many validators there are, and K only sets how many draws a poll makes. Tying K to the headcount gave a validator holding a billionth of the stake the same standing as one holding a fifth, and refused to boot a producing chain because a small staker had joined. The guard keeps what makes a committee safe — the overlap bound and at least single-fault tolerance — and stops counting heads. The tier floor stays a reported target: refusing to start does not add validators, it pins every node on whatever it last booted. Co-authored-by: Hanzo Dev <[email protected]>
1035 lines
41 KiB
Go
1035 lines
41 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
// Package config — PQMode is the single switch by which an operator selects
|
||
// what cryptographic witnesses ride alongside BLS-12-381 in a Quasar finality
|
||
// certificate. Each mode is a fully orthogonal point in the design space and
|
||
// is named for the sub-protocol that defines its post-quantum surface.
|
||
//
|
||
// The truth, in one table:
|
||
//
|
||
// mode classical threshold lattice per-validator PQ rollup DKG required FIPS-approvable?
|
||
// bls BLS-12-381 — — — — no (BLS not in FIPS 186)
|
||
// corona BLS-12-381 Corona / BLAKE3 — — trusted dealer no (BLAKE3 not in FIPS 202)
|
||
// pulsar BLS-12-381 Pulsar / SHA-3 (SP-185) — — Pedersen DKG over R_q + reshare partial (BLS still classical)
|
||
// quasar BLS-12-381 Pulsar / SHA-3 (SP-185) ML-DSA-65 (rolled) Groth16 / BN254-Z Pedersen DKG over R_q + reshare no (Groth16/BN254 classical, not FIPS)
|
||
// mldsa BLS-12-381 — ML-DSA-65 raw (SHAKE) — — yes (drop BLS or use FIPS-approved aggregate)
|
||
//
|
||
// Two production paths:
|
||
//
|
||
// BLS → Pulsar → Quasar public chains (open validator set, epoch rotation)
|
||
// BLS → Corona federation / bridge MPC (fixed dealer, no rotation)
|
||
//
|
||
// Quasar is the Lux-mesh default. Corona is for fixed federations only —
|
||
// its trusted-dealer DKG is unsuitable for an open public chain. MLDSA is the
|
||
// audit-grade fallback **and** the FIPS-approvable path: ML-DSA-65 (FIPS 204)
|
||
// + SHAKE256 (FIPS 202) is the only mode where every PQ component sits inside
|
||
// the NIST-approved algorithm set. See HIP-0077 §"FIPS profile" for the full
|
||
// drop-list (no Pulsar/Corona/Groth16/secp256k1) and the implementation
|
||
// switch needed.
|
||
//
|
||
// Pulsar vs Quasar in one sentence: Pulsar = BLS + one R-LWE threshold layer;
|
||
// Quasar = Pulsar + a second M-LWE layer (ML-DSA-65) compressed via Groth16
|
||
// onto Z-Chain. Same threshold layer, plus a defense-in-depth lattice that
|
||
// keeps cert size constant.
|
||
package config
|
||
|
||
import (
|
||
"fmt"
|
||
)
|
||
|
||
// PQMode selects the post-quantum signature stack the consensus engine layers
|
||
// over the BLS-12-381 fast path. See the package doc-comment for the full
|
||
// option matrix; each constant below pins one row.
|
||
type PQMode uint8
|
||
|
||
const (
|
||
// PQModeBLS — BLS aggregate only. 48-byte cert. Classical fast path,
|
||
// no PQ surface. Benchmarks and legacy testnets only; rejected on any
|
||
// production Lux mesh per HIP-0077.
|
||
PQModeBLS PQMode = iota
|
||
|
||
// PQModeNasua — BLS + the academic Nasua 2-round LWE threshold
|
||
// signature. Hash profile is **BLAKE3** (`primitives/hash.go` uses
|
||
// `github.com/zeebo/blake3` for every challenge / share / transcript
|
||
// binding). Trusted-dealer DKG, fixed federation, no proactive
|
||
// resharing. Suitable for bridge MPC and small fixed-membership
|
||
// federations; **not** an open public chain stance because it has no
|
||
// DKG that survives epoch rotation, and BLAKE3 is outside the NIST
|
||
// FIPS 202 approved hash family so this mode is **not** FIPS-approvable.
|
||
// Provided by `github.com/luxfi/nasua` (academic port).
|
||
PQModeNasua
|
||
|
||
// PQModePulsar — BLS + the Pulsar production fork of Corona. Same
|
||
// 2-round threshold algorithm as Corona, but Pulsar's canonical
|
||
// hash profile is **SHA-3 (cSHAKE256 / KMAC256 / TupleHash256,
|
||
// FIPS 202 + NIST SP 800-185)** and Pulsar adds the production
|
||
// lifecycle Corona lacks: Pedersen DKG over R_q with proper hiding,
|
||
// and proactive secret resharing for epoch validator rotation.
|
||
// Pulsar additionally ships a non-normative `Pulsar-BLAKE3` legacy
|
||
// suite for byte-equality regressions only.
|
||
// Suitable for open public chains with **same-committee** proactive
|
||
// resharing; cross-committee reshare in Pulsar v0.1 changes the
|
||
// master pubkey because the committee root is bound into the cSHAKE
|
||
// seed mix — cross-committee rotation is on the Pulsar v0.2 roadmap.
|
||
// Provided by `github.com/luxfi/pulsar`.
|
||
PQModePulsar
|
||
|
||
// PQModeQuasar — BLS + Pulsar + ML-DSA-65 rolled into a Groth16 proof
|
||
// anchored on Z-Chain. Two distinct lattice constructions backing
|
||
// each finality cert (Pulsar = R-LWE threshold; ML-DSA = MLWE per
|
||
// validator), both still constant-size in the cert. Strongest
|
||
// available; **Lux mesh default** per HIP-0077. Where the Z-Chain
|
||
// witness path is not yet wired in a given deployment, Quasar
|
||
// degrades to MLDSA semantics so the strong stance is preserved at
|
||
// higher bandwidth cost.
|
||
// Implemented by `lux/consensus/protocol/quasar`.
|
||
PQModeQuasar
|
||
|
||
// PQModeMLDSA — BLS + per-validator ML-DSA-65, no rollup, no
|
||
// threshold. Cert size: 48 + N*3309 B (linear in validator count).
|
||
// Audit-grade fallback when Quasar's Z-Chain Groth16 witness is
|
||
// unavailable, or when an external auditor needs every validator's
|
||
// raw signature in every cert. ML-DSA primitive comes from Pulsar's
|
||
// FIPS-204 implementation.
|
||
PQModeMLDSA
|
||
)
|
||
|
||
// String returns the canonical single-word name.
|
||
func (m PQMode) String() string {
|
||
switch m {
|
||
case PQModeBLS:
|
||
return "bls"
|
||
case PQModeNasua:
|
||
return "nasua"
|
||
case PQModePulsar:
|
||
return "pulsar"
|
||
case PQModeQuasar:
|
||
return "quasar"
|
||
case PQModeMLDSA:
|
||
return "mldsa"
|
||
default:
|
||
return fmt.Sprintf("pq-mode(%d)", uint8(m))
|
||
}
|
||
}
|
||
|
||
// PolicyID maps a PQMode to the canonical wire policy ID.
|
||
//
|
||
// Translation, kept here so we don't drag pkg/wire into config:
|
||
//
|
||
// bls -> 1 (PolicyQuorum)
|
||
// corona -> 5 (PolicyPQ, P+Q witness set, SHA-256 profile)
|
||
// pulsar -> 5 (PolicyPQ, P+Q witness set, SHA-3 profile)
|
||
// quasar -> 4 (PolicyQuantum, P+Q+Z witness set)
|
||
// mldsa -> 6 (PolicyPZ, P+Z witness set, per-validator ML-DSA)
|
||
//
|
||
// Corona and Pulsar share a wire PolicyID because the witness set on
|
||
// the wire is the same shape — the difference is the hash profile and
|
||
// DKG path, both negotiated out-of-band at network setup.
|
||
func (m PQMode) PolicyID() uint16 {
|
||
switch m {
|
||
case PQModeBLS:
|
||
return 1 // PolicyQuorum
|
||
case PQModeNasua, PQModePulsar:
|
||
return 5 // PolicyPQ
|
||
case PQModeQuasar:
|
||
return 4 // PolicyQuantum
|
||
case PQModeMLDSA:
|
||
return 6 // PolicyPZ
|
||
default:
|
||
return 1
|
||
}
|
||
}
|
||
|
||
// HashSuiteID is the wire byte that identifies which hash family a Quasar
|
||
// finality certificate was produced under. It is a separate axis from
|
||
// PolicyID: Pulsar and Corona share PolicyID 5 (P+Q witness set) but use
|
||
// different hash families (SHA-3 vs BLAKE3), so the policy byte alone is
|
||
// not enough for a receiver to know which kernel to instantiate.
|
||
//
|
||
// HashSuiteID is bound into the cert transcript so that flipping the byte
|
||
// post-signing changes the digest the threshold signature covers — a
|
||
// cross-suite confusion attack therefore fails on signature verification,
|
||
// not just on a string-equality check. See HIP-0077 §"Lux consensus PQ
|
||
// modes" and proofs/pulsar/hash-suite-separation.tex for the formal
|
||
// argument.
|
||
//
|
||
// **Numbering policy (NIST-aligned).** SHA3_NIST is the normative entry
|
||
// (0x01) so reviewers reading a cert envelope see the FIPS-aligned suite
|
||
// first. BLAKE3 keeps its own ID (0x02) but is marked LEGACY because it
|
||
// is outside the FIPS 202 family; non-normative for any NIST-track
|
||
// submission. SHAKE256 does NOT get its own ID — SHAKE256 is FIPS 202
|
||
// and therefore part of SHA3_NIST. Modes whose only PQ hash is SHAKE256
|
||
// (e.g. raw ML-DSA-65) advertise SHA3_NIST.
|
||
//
|
||
// The byte is open-ended: future hash families MUST claim the next free
|
||
// ID and pin it in pq_mode_test.go before any code references it. Never
|
||
// reuse a retired ID. The wire reserves 0x00 for "no hash-family
|
||
// commitment" so legacy BLS-only certs round-trip cleanly.
|
||
type HashSuiteID uint8
|
||
|
||
const (
|
||
// HashSuiteNone — no hash-family commitment. BLS-only modes whose
|
||
// threshold layer is absent do not pin a hash suite at the consensus
|
||
// layer; their kernels carry their own hash scoping internally.
|
||
HashSuiteNone HashSuiteID = 0x00
|
||
|
||
// HashSuiteSHA3NIST — the normative SP 800-185 hash family:
|
||
// SHAKE256 + cSHAKE256 + KMAC256 + TupleHash256 (FIPS 202 +
|
||
// NIST SP 800-185). The canonical Lux-mesh and Lux primary-network
|
||
// choice. Every NIST-track Pulsar variant (Pulsar.R production
|
||
// profile, Pulsar.M family) signs under this suite. ML-DSA-65 also
|
||
// rides here because SHAKE256 is FIPS 202 and not a separate family.
|
||
HashSuiteSHA3NIST HashSuiteID = 0x01
|
||
|
||
// HashSuiteBLAKE3Legacy — Corona academic profile and Pulsar's
|
||
// pre-pin legacy suite. BLAKE3 keyed XOF for every challenge / share
|
||
// / transcript binding (see github.com/luxfi/nasua
|
||
// primitives/hash.go and github.com/luxfi/corona/hash/blake3.go).
|
||
// BLAKE3 is outside FIPS 202 and therefore non-normative for any
|
||
// NIST submission; the byte exists so legacy / academic / federation-
|
||
// MPC deployments can still emit a verifiable cert.
|
||
HashSuiteBLAKE3Legacy HashSuiteID = 0x02
|
||
)
|
||
|
||
// String returns the canonical lower-case name of the hash suite. Empty
|
||
// string for HashSuiteNone so cert printers can omit the line cleanly.
|
||
func (h HashSuiteID) String() string {
|
||
switch h {
|
||
case HashSuiteNone:
|
||
return "none"
|
||
case HashSuiteSHA3NIST:
|
||
return "sha3-nist"
|
||
case HashSuiteBLAKE3Legacy:
|
||
return "blake3-legacy"
|
||
default:
|
||
return fmt.Sprintf("hash-suite(0x%02x)", uint8(h))
|
||
}
|
||
}
|
||
|
||
// IsNormative reports whether this hash suite is on the NIST-approved
|
||
// algorithm path (FIPS 202 family). Production Lux meshes MUST emit
|
||
// only normative suites; legacy/academic suites are bridge-MPC + audit
|
||
// fixtures only.
|
||
func (h HashSuiteID) IsNormative() bool {
|
||
return h == HashSuiteSHA3NIST
|
||
}
|
||
|
||
// HashSuiteID reports the wire HashSuiteID byte for this PQ mode. The
|
||
// byte is the receiver's only signal of which hash kernel to instantiate
|
||
// when verifying a cert built under this mode — Pulsar and Corona share
|
||
// PolicyID 5, so PolicyID alone cannot distinguish them.
|
||
//
|
||
// Closes HIP-0077 red-review F1 (silent finality forks between
|
||
// hash-profile islands sharing the same PolicyID).
|
||
//
|
||
// Mapping:
|
||
//
|
||
// bls -> HashSuiteNone (0x00) no hash-family commitment
|
||
// corona -> HashSuiteBLAKE3Legacy (0x02) academic profile, non-normative
|
||
// pulsar -> HashSuiteSHA3NIST (0x01) production profile (cSHAKE256 family)
|
||
// quasar -> HashSuiteSHA3NIST (0x01) same threshold-layer kernel as Pulsar
|
||
// mldsa -> HashSuiteSHA3NIST (0x01) ML-DSA-65 uses SHAKE256 (FIPS 202)
|
||
func (m PQMode) HashSuiteID() HashSuiteID {
|
||
switch m {
|
||
case PQModeBLS:
|
||
return HashSuiteNone
|
||
case PQModeNasua:
|
||
return HashSuiteBLAKE3Legacy
|
||
case PQModePulsar, PQModeQuasar, PQModeMLDSA:
|
||
return HashSuiteSHA3NIST
|
||
default:
|
||
return HashSuiteNone
|
||
}
|
||
}
|
||
|
||
// HashProfile reports the canonical hash-family name for the mode. This
|
||
// is the human-readable string form of HashSuiteID and stays consistent
|
||
// with it for every mode: "none" / "sha3-nist" / "blake3-legacy".
|
||
// Receivers SHOULD compare HashSuiteID bytes (cheap, exact); HashProfile
|
||
// exists for logs, error messages, and cross-language wire-debug only.
|
||
func (m PQMode) HashProfile() string {
|
||
return m.HashSuiteID().String()
|
||
}
|
||
|
||
// SigSchemeID is the wire byte that identifies which signature scheme
|
||
// produced the signature in this slot of a Quasar finality certificate.
|
||
// Orthogonal to PolicyID, HashSuiteID, and ProofSystemID.
|
||
//
|
||
// Bound into the cert transcript so a flipped byte breaks signature
|
||
// verification, not just type-equality.
|
||
//
|
||
// **Numbering blocks** (open-ended). The two ML-DSA-related blocks are
|
||
// kept distinct on purpose:
|
||
//
|
||
// - 0x40 block names the **raw single-party** primitive (ML-DSA-44/65/87
|
||
// per FIPS 204). Used when a per-validator signature appears in the
|
||
// cert without threshold aggregation (mldsa audit-grade fallback,
|
||
// identity attestations on Z-Chain, etc.).
|
||
//
|
||
// - 0x50 block names the **Pulsar-M threshold variant** at the same
|
||
// parameter sets. The verification *relation* is FIPS 204 ML-DSA.Verify
|
||
// for both blocks, but the producing protocol differs (single-party
|
||
// vs threshold) and the wire intentionally separates them so a
|
||
// receiver knows which kernel emitted the bytes.
|
||
//
|
||
// 0x00 — None / unspecified (BLS-only certs do not commit to a sig scheme)
|
||
// 0x10..0x1F — Classical (BLS-12-381 aggregate at 0x10)
|
||
// 0x20..0x2F — Corona (academic R-LWE)
|
||
// 0x30..0x3F — Pulsar.R (production R-LWE family)
|
||
// 0x40..0x4F — Raw ML-DSA per FIPS 204 (single-party):
|
||
// 0x41 = ML-DSA-44 (NIST PQ Cat 2)
|
||
// 0x42 = ML-DSA-65 (NIST PQ Cat 3)
|
||
// 0x43 = ML-DSA-87 (NIST PQ Cat 5)
|
||
// 0x50..0x5F — Pulsar-M threshold (M-LWE, FIPS 204-compatible):
|
||
// 0x51 = Pulsar-M-44 (NIST PQ Cat 2, devnet only)
|
||
// 0x52 = Pulsar-M-65 (NIST PQ Cat 3, **production default**)
|
||
// 0x53 = Pulsar-M-87 (NIST PQ Cat 5, high-value roots)
|
||
// 0x60.. — reserved for future variants
|
||
//
|
||
// New entries claim the next free integer in their block and MUST land in
|
||
// pq_mode_test.go before any code references them. Never reuse a retired ID.
|
||
type SigSchemeID uint8
|
||
|
||
const (
|
||
SigSchemeNone SigSchemeID = 0x00
|
||
SigSchemeBLS12381 SigSchemeID = 0x10
|
||
SigSchemeNasua SigSchemeID = 0x20
|
||
SigSchemePulsarR SigSchemeID = 0x30
|
||
|
||
// Raw single-party ML-DSA (FIPS 204).
|
||
SigSchemeMLDSA44 SigSchemeID = 0x41
|
||
SigSchemeMLDSA65 SigSchemeID = 0x42
|
||
SigSchemeMLDSA87 SigSchemeID = 0x43
|
||
|
||
// Pulsar-M threshold (M-LWE, output-interchangeable with FIPS 204).
|
||
SigSchemePulsar44 SigSchemeID = 0x51
|
||
SigSchemePulsar65 SigSchemeID = 0x52 // production default
|
||
SigSchemePulsar87 SigSchemeID = 0x53
|
||
)
|
||
|
||
// String returns the canonical wire name of the signature scheme.
|
||
func (s SigSchemeID) String() string {
|
||
switch s {
|
||
case SigSchemeNone:
|
||
return "none"
|
||
case SigSchemeBLS12381:
|
||
return "bls12-381"
|
||
case SigSchemeNasua:
|
||
return "nasua"
|
||
case SigSchemePulsarR:
|
||
return "pulsar-r"
|
||
case SigSchemeMLDSA44:
|
||
return "ml-dsa-44"
|
||
case SigSchemeMLDSA65:
|
||
return "ml-dsa-65"
|
||
case SigSchemeMLDSA87:
|
||
return "ml-dsa-87"
|
||
case SigSchemePulsar44:
|
||
return "pulsar-m-44"
|
||
case SigSchemePulsar65:
|
||
return "pulsar-m-65"
|
||
case SigSchemePulsar87:
|
||
return "pulsar-m-87"
|
||
default:
|
||
return fmt.Sprintf("sig-scheme(0x%02x)", uint8(s))
|
||
}
|
||
}
|
||
|
||
// IsPulsarM reports whether this scheme is in the Pulsar-M threshold
|
||
// family (0x50 block). Pulsar-M outputs verify under unmodified FIPS 204
|
||
// ML-DSA.Verify; the threshold protocol that produced them is what this
|
||
// flag separates from raw ML-DSA.
|
||
func (s SigSchemeID) IsPulsarM() bool {
|
||
return s == SigSchemePulsar44 ||
|
||
s == SigSchemePulsar65 ||
|
||
s == SigSchemePulsar87
|
||
}
|
||
|
||
// IsRawMLDSA reports whether this scheme is in the raw single-party
|
||
// ML-DSA block (0x40). These signatures are produced by FIPS 204
|
||
// ML-DSA.Sign (one private key, no threshold protocol); the same
|
||
// verifier accepts them as IsPulsarM outputs.
|
||
func (s SigSchemeID) IsRawMLDSA() bool {
|
||
return s == SigSchemeMLDSA44 ||
|
||
s == SigSchemeMLDSA65 ||
|
||
s == SigSchemeMLDSA87
|
||
}
|
||
|
||
// VerifiesUnderFIPS204 reports whether the unmodified FIPS 204 ML-DSA
|
||
// verifier will accept signatures emitted under this scheme. Both raw
|
||
// ML-DSA (0x40 block) and Pulsar-M (0x50 block) qualify; nothing else
|
||
// in the current enum does.
|
||
func (s SigSchemeID) VerifiesUnderFIPS204() bool {
|
||
return s.IsRawMLDSA() || s.IsPulsarM()
|
||
}
|
||
|
||
// SigSchemeID reports the default threshold-signature scheme for this
|
||
// PQ mode. The default is the production-grade choice; operators MAY
|
||
// override (e.g. Pulsar-M-87 for governance checkpoints) by passing an
|
||
// explicit SigSchemeID through the cert assembler.
|
||
//
|
||
// Mapping (HIP-0077 production defaults):
|
||
//
|
||
// bls -> SigSchemeNone BLS aggregate carried separately
|
||
// corona -> SigSchemeNasua federation MPC
|
||
// pulsar -> SigSchemePulsarR R-LWE production
|
||
// quasar -> SigSchemePulsarR threshold layer is Pulsar.R; Z-Chain witness separate
|
||
// mldsa -> SigSchemeMLDSA65 raw ML-DSA-65 (audit-grade); ops opt-in to Pulsar-M-65 explicitly
|
||
func (m PQMode) SigSchemeID() SigSchemeID {
|
||
switch m {
|
||
case PQModeBLS:
|
||
return SigSchemeNone
|
||
case PQModeNasua:
|
||
return SigSchemeNasua
|
||
case PQModePulsar, PQModeQuasar:
|
||
return SigSchemePulsarR
|
||
case PQModeMLDSA:
|
||
return SigSchemeMLDSA65
|
||
default:
|
||
return SigSchemeNone
|
||
}
|
||
}
|
||
|
||
// ProofPolicyID is the wire byte that identifies the **security class**
|
||
// (policy) the proof system implements. Orthogonal to ProofBackendID
|
||
// (which names the implementation). Strict-PQ deployments MUST accept
|
||
// only IDs whose `IsPostQuantum()` returns true; classical IDs are
|
||
// reserved as explicit forbidden markers so audit tooling can name a
|
||
// misconfiguration precisely.
|
||
//
|
||
// Numbering:
|
||
//
|
||
// 0x00 — None
|
||
// 0x10..0x1F — STARK / FRI policies (PQ-secure)
|
||
// 0x10 = STARK_FRI_SHA3_PQ canonical, NIST-aligned
|
||
// 0x11 = STARK_FRI_KECCAK_PQ Keccak Merkle (FIPS 202; non-NIST-recommended-default)
|
||
// 0x80..0xFF — Classical / forbidden in PQ mode
|
||
// 0x80 = GROTH16_BN254_CLASSICAL
|
||
// 0x81 = PLONK_KZG_CLASSICAL
|
||
// 0x82 = HALO_EC_CLASSICAL
|
||
// 0x83 = IPA_EC_CLASSICAL
|
||
//
|
||
// `ProofSystemID` is a deprecated alias for `ProofPolicyID` for one
|
||
// release; new code uses `ProofPolicyID`.
|
||
type ProofPolicyID uint8
|
||
|
||
// ProofSystemID is a deprecated type alias for ProofPolicyID. Existing
|
||
// callers continue to work; new code uses ProofPolicyID. Removed after
|
||
// the 2026-Aug-31 encoding-freeze gate.
|
||
type ProofSystemID = ProofPolicyID
|
||
|
||
const (
|
||
ProofPolicyNone ProofPolicyID = 0x00
|
||
ProofPolicySTARKFRISHA3PQ ProofPolicyID = 0x10 // canonical Z-Chain policy
|
||
ProofPolicySTARKFRIKeccak ProofPolicyID = 0x11 // FIPS 202 Keccak alternative
|
||
|
||
// Forbidden in strict-PQ mode. The wire keeps an ID for each so
|
||
// audit tooling can name a misconfiguration explicitly.
|
||
ProofPolicyGroth16BN254Forbid ProofPolicyID = 0x80
|
||
ProofPolicyPLONKKZGForbid ProofPolicyID = 0x81
|
||
ProofPolicyHaloECForbid ProofPolicyID = 0x82
|
||
ProofPolicyIPAECForbid ProofPolicyID = 0x83
|
||
|
||
// Legacy aliases — keep callers compiling through the rename.
|
||
// New code uses the ProofPolicy* names.
|
||
ProofSystemNone = ProofPolicyNone
|
||
ProofSystemSTARKFRISHA3PQ = ProofPolicySTARKFRISHA3PQ
|
||
ProofSystemSTARKFRIKeccakPQ = ProofPolicySTARKFRIKeccak
|
||
ProofSystemGroth16BN254Forbid = ProofPolicyGroth16BN254Forbid
|
||
ProofSystemKZGForbid = ProofPolicyPLONKKZGForbid
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (p ProofPolicyID) String() string {
|
||
switch p {
|
||
case ProofPolicyNone:
|
||
return "none"
|
||
case ProofPolicySTARKFRISHA3PQ:
|
||
return "stark-fri-sha3-pq"
|
||
case ProofPolicySTARKFRIKeccak:
|
||
return "stark-fri-keccak-pq"
|
||
case ProofPolicyGroth16BN254Forbid:
|
||
return "groth16-bn254-classical-forbidden-in-pq"
|
||
case ProofPolicyPLONKKZGForbid:
|
||
return "plonk-kzg-classical-forbidden-in-pq"
|
||
case ProofPolicyHaloECForbid:
|
||
return "halo-ec-classical-forbidden-in-pq"
|
||
case ProofPolicyIPAECForbid:
|
||
return "ipa-ec-classical-forbidden-in-pq"
|
||
default:
|
||
return fmt.Sprintf("proof-policy(0x%02x)", uint8(p))
|
||
}
|
||
}
|
||
|
||
// IsPostQuantum reports whether this policy is acceptable in strict-PQ
|
||
// mode. Strict-PQ producers MUST emit only IsPostQuantum=true policies;
|
||
// verifiers in strict-PQ mode MUST refuse anything else.
|
||
func (p ProofPolicyID) IsPostQuantum() bool {
|
||
switch p {
|
||
case ProofPolicySTARKFRISHA3PQ,
|
||
ProofPolicySTARKFRIKeccak:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// IsForbiddenInPQMode reports whether this proof policy carries the
|
||
// explicit forbidden marker. Used by audit tooling to detect a
|
||
// misconfiguration where a classical proof would otherwise sneak past
|
||
// a PQ-only deployment.
|
||
func (p ProofPolicyID) IsForbiddenInPQMode() bool {
|
||
return p == ProofPolicyGroth16BN254Forbid ||
|
||
p == ProofPolicyPLONKKZGForbid ||
|
||
p == ProofPolicyHaloECForbid ||
|
||
p == ProofPolicyIPAECForbid
|
||
}
|
||
|
||
// ProofBackendID is the wire byte that identifies which **implementation**
|
||
// produced a Z-Chain proof. Orthogonal to ProofPolicyID (which names the
|
||
// security class). This separation lets Z-Chain benchmark SP1, RISC Zero,
|
||
// P3Q, Stone (Cairo), and Stwo (Circle STARK) without changing chain
|
||
// semantics — they all satisfy the same ProofPolicyID, just with
|
||
// different underlying implementations.
|
||
//
|
||
// Numbering:
|
||
//
|
||
// 0x00 — None (no backend; e.g. policy-only declarations)
|
||
// 0x10..0x1F — STARK / FRI implementations (production)
|
||
// 0x20 = RISC0_SUCCINCT_STARK_PQ RISC Zero succinct receipt path (no Groth16 wrapper)
|
||
// 0x21 = SP1_COMPRESSED_STARK_PQ SP1 compressed STARK (no Groth16 wrapper)
|
||
// 0x22 = P3Q_STARK_FRI_SHA3_PQ Lux P3Q (Plonky3 fork, cSHAKE256 Merkle)
|
||
// 0x23 = STONE_CAIRO_STARK_PQ Cairo / Stone backend
|
||
// 0x24 = STWO_CIRCLE_STARK_PQ Stwo Circle STARK
|
||
// 0x30..0x3F — Direct full-node verification (no succinct proof). The
|
||
// "proof" is the certificate itself: each signer's
|
||
// unmodified FIPS 204/205 signature, a weighted-validator-set
|
||
// Merkle inclusion path, and a weight-threshold predicate.
|
||
// Soundness rests ONLY on the underlying FIPS verifier plus
|
||
// Merkle second-preimage resistance — there is no
|
||
// succinctness layer and therefore no STARK/SNARK trusted
|
||
// component to audit. A STARK compression backend over the
|
||
// SAME relation is a separate id in the 0x10/0x20 block.
|
||
// 0x30 = DIRECT_WEIGHTED_QUORUM_PQ per-signer FIPS verify + Merkle + weight predicate
|
||
// 0x70..0x7F — Dev-only / non-production (CI, testnet, audit fuzzing)
|
||
// 0x70 = RISC0_RAW_STARK_DEV
|
||
// 0x71 = SP1_CORE_STARK_DEV
|
||
// 0x80..0xFF — Forbidden in strict-PQ mode (mirror of ProofPolicyID
|
||
// forbidden block; backends that wrap classical wrappers
|
||
// carry the same explicit refusal markers).
|
||
type ProofBackendID uint8
|
||
|
||
const (
|
||
ProofBackendNone ProofBackendID = 0x00
|
||
ProofBackendRISC0SuccinctSTARK ProofBackendID = 0x20
|
||
ProofBackendSP1CompressedSTARK ProofBackendID = 0x21
|
||
ProofBackendP3QSTARKFRISHA3 ProofBackendID = 0x22
|
||
ProofBackendStoneCairoSTARK ProofBackendID = 0x23
|
||
ProofBackendStwoCircleSTARK ProofBackendID = 0x24
|
||
|
||
// ProofBackendDirectWeightedQuorum is the direct full-node-verifiable
|
||
// weighted quorum certificate backend (HIP-0079 §"full-node-verifiable
|
||
// mode"). Verification is direct: for each signer record, run unmodified
|
||
// FIPS 204 (ML-DSA) or FIPS 205 (SLH-DSA) Verify over the
|
||
// domain-separated consensus message, check the weighted-validator-set
|
||
// Merkle inclusion path, then assert the accumulated voting weight meets
|
||
// the quorum threshold. No succinct proof, no recursion, no trusted
|
||
// setup; the per-signer signatures ARE the proof. This is threshold
|
||
// CERTIFICATION, not threshold signing — no key material is shared,
|
||
// reconstructed, or combined. See protocol/quasar/quorum_cert.go.
|
||
ProofBackendDirectWeightedQuorum ProofBackendID = 0x30
|
||
|
||
// Dev-only — never produced in production strict-PQ deployments.
|
||
ProofBackendRISC0RawSTARKDev ProofBackendID = 0x70
|
||
ProofBackendSP1CoreSTARKDev ProofBackendID = 0x71
|
||
|
||
// Forbidden in strict-PQ mode. Mirrors ProofPolicyID forbidden block
|
||
// at the backend layer (e.g. a backend that wraps STARK in Groth16
|
||
// for cheaper EVM verification).
|
||
ProofBackendGroth16WrapForbid ProofBackendID = 0x80
|
||
ProofBackendKZGWrapForbid ProofBackendID = 0x81
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (b ProofBackendID) String() string {
|
||
switch b {
|
||
case ProofBackendNone:
|
||
return "none"
|
||
case ProofBackendRISC0SuccinctSTARK:
|
||
return "risc0-succinct-stark-pq"
|
||
case ProofBackendSP1CompressedSTARK:
|
||
return "sp1-compressed-stark-pq"
|
||
case ProofBackendP3QSTARKFRISHA3:
|
||
return "p3q-stark-fri-sha3-pq"
|
||
case ProofBackendStoneCairoSTARK:
|
||
return "stone-cairo-stark-pq"
|
||
case ProofBackendStwoCircleSTARK:
|
||
return "stwo-circle-stark-pq"
|
||
case ProofBackendDirectWeightedQuorum:
|
||
return "direct-weighted-quorum-pq"
|
||
case ProofBackendRISC0RawSTARKDev:
|
||
return "risc0-raw-stark-dev"
|
||
case ProofBackendSP1CoreSTARKDev:
|
||
return "sp1-core-stark-dev"
|
||
case ProofBackendGroth16WrapForbid:
|
||
return "groth16-wrap-classical-forbidden-in-pq"
|
||
case ProofBackendKZGWrapForbid:
|
||
return "kzg-wrap-classical-forbidden-in-pq"
|
||
default:
|
||
return fmt.Sprintf("proof-backend(0x%02x)", uint8(b))
|
||
}
|
||
}
|
||
|
||
// IsProductionPQ reports whether this backend is acceptable in strict-PQ
|
||
// production mode. Dev backends (0x70 block) and forbidden backends
|
||
// (0x80 block) return false.
|
||
//
|
||
// ProofBackendDirectWeightedQuorum qualifies: its soundness rests on
|
||
// unmodified FIPS 204/205 verification plus Merkle second-preimage
|
||
// resistance, which is strictly inside the NIST-approved algorithm set
|
||
// (no classical EC, no trusted setup). It is the most conservative
|
||
// production-PQ posture — there is no succinctness assumption to break.
|
||
func (b ProofBackendID) IsProductionPQ() bool {
|
||
switch b {
|
||
case ProofBackendRISC0SuccinctSTARK,
|
||
ProofBackendSP1CompressedSTARK,
|
||
ProofBackendP3QSTARKFRISHA3,
|
||
ProofBackendStoneCairoSTARK,
|
||
ProofBackendStwoCircleSTARK,
|
||
ProofBackendDirectWeightedQuorum:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// IsForbiddenInPQMode mirrors ProofPolicyID.IsForbiddenInPQMode at the
|
||
// backend layer.
|
||
func (b ProofBackendID) IsForbiddenInPQMode() bool {
|
||
return b == ProofBackendGroth16WrapForbid ||
|
||
b == ProofBackendKZGWrapForbid
|
||
}
|
||
|
||
// IdentitySchemeID is the wire byte that identifies which signature scheme
|
||
// a validator uses for **identity** (registration, key rotation,
|
||
// revocation, attestation) — distinct from the threshold-finality
|
||
// SigSchemeID. Identity signatures are single-party, FIPS 204 ML-DSA.
|
||
//
|
||
// Numbering — block 0x40 mirrors raw ML-DSA in SigSchemeID so the byte
|
||
// pattern is consistent across the wire.
|
||
//
|
||
// 0x00 — None (no identity scheme committed; legacy / anonymous)
|
||
// 0x41 — ML_DSA_44 (NIST PQ Cat 2; devnet only)
|
||
// 0x42 — ML_DSA_65 (NIST PQ Cat 3; **production identity default**)
|
||
// 0x43 — ML_DSA_87 (NIST PQ Cat 5; high-value root identities)
|
||
type IdentitySchemeID uint8
|
||
|
||
const (
|
||
IdentitySchemeNone IdentitySchemeID = 0x00
|
||
IdentitySchemeMLDSA44 IdentitySchemeID = 0x41
|
||
IdentitySchemeMLDSA65 IdentitySchemeID = 0x42 // production default
|
||
IdentitySchemeMLDSA87 IdentitySchemeID = 0x43
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (i IdentitySchemeID) String() string {
|
||
switch i {
|
||
case IdentitySchemeNone:
|
||
return "none"
|
||
case IdentitySchemeMLDSA44:
|
||
return "ml-dsa-44"
|
||
case IdentitySchemeMLDSA65:
|
||
return "ml-dsa-65"
|
||
case IdentitySchemeMLDSA87:
|
||
return "ml-dsa-87"
|
||
default:
|
||
return fmt.Sprintf("identity-scheme(0x%02x)", uint8(i))
|
||
}
|
||
}
|
||
|
||
// IsFIPS204 reports whether this identity scheme uses unmodified
|
||
// FIPS 204 ML-DSA verification. All defined non-None values do; the
|
||
// helper exists so audit tooling can assert this property explicitly.
|
||
func (i IdentitySchemeID) IsFIPS204() bool {
|
||
return i == IdentitySchemeMLDSA44 ||
|
||
i == IdentitySchemeMLDSA65 ||
|
||
i == IdentitySchemeMLDSA87
|
||
}
|
||
|
||
// Note: ProofFormatID and VerifierID are defined in security_profile.go
|
||
// because they are part of the locked-profile axis set, not the
|
||
// mode-selection enum group above. Look there for their numbering tables
|
||
// and behaviour predicates.
|
||
|
||
// DKGRequired reports whether an open public chain can run this mode.
|
||
// Modes whose threshold layer relies on a trusted dealer (Corona) cannot;
|
||
// modes with no threshold (BLS, MLDSA) are vacuously fine; modes with
|
||
// proper DKG (Pulsar, Quasar) are the production target.
|
||
func (m PQMode) DKGRequired() string {
|
||
switch m {
|
||
case PQModeBLS, PQModeMLDSA:
|
||
return "none"
|
||
case PQModeNasua:
|
||
return "trusted-dealer" // unsuitable for open public chains
|
||
case PQModePulsar, PQModeQuasar:
|
||
return "pedersen-dkg-over-rq"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// IsPostQuantum reports whether the mode carries any PQ witness on top of
|
||
// BLS. False only for PQModeBLS.
|
||
func (m PQMode) IsPostQuantum() bool {
|
||
return m != PQModeBLS
|
||
}
|
||
|
||
// SuitableForPublicChain reports whether the mode is appropriate for an
|
||
// open public chain (epoch rotation, no trusted dealer). False for
|
||
// PQModeNasua (trusted-dealer DKG only) and PQModeBLS (no PQ at all);
|
||
// true for everything else.
|
||
func (m PQMode) SuitableForPublicChain() bool {
|
||
switch m {
|
||
case PQModePulsar, PQModeQuasar, PQModeMLDSA:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// End-to-end PQ enums (HIP-0078 §"E2E PQ surface").
|
||
//
|
||
// These five axes are orthogonal to the threshold / finality axis above:
|
||
// they pin what the chain accepts at the **wallet**, **transaction
|
||
// authorisation**, **contract authorisation**, **key exchange**, and
|
||
// **recovery** layers respectively. Each axis carries its own forbidden
|
||
// marker block (0x90+) so an audit pipeline can name a classical leakage
|
||
// explicitly rather than detecting it by absence.
|
||
//
|
||
// Numbering convention shared across the five enums:
|
||
//
|
||
// 0x00 — Invalid / unspecified (rejected by every strict verifier)
|
||
// 0x01..0x0F — KEM family (reserved on KeyExchangeID; unused on schemes)
|
||
// 0x05..0x07 — SLH-DSA stateless backstop (RecoverySchemeID only)
|
||
// 0x10..0x2F — Multi-party / proof-carrying composite schemes
|
||
// 0x42..0x43 — ML-DSA-65 / ML-DSA-87 (FIPS 204 lattice signature)
|
||
// 0x90.. — Forbidden in strict-PQ mode (ECDSA / X25519 / classical BLS)
|
||
// 0xFF — None sentinel (RecoverySchemeID only — high-value M-DSA-87
|
||
// provides the backstop without a separate stateless scheme)
|
||
//
|
||
// Renumbering an existing entry breaks every state-root and envelope ever
|
||
// signed under it. Adding an entry only requires a new free integer in the
|
||
// appropriate block.
|
||
// =============================================================================
|
||
|
||
// WalletSchemeID identifies the signature scheme a wallet uses to sign
|
||
// outbound transactions. The wallet axis is independent of validator
|
||
// identity (IdentitySchemeID), threshold finality (SigSchemeID), and
|
||
// contract authorisation (ContractAuthID) — a single chain may pin
|
||
// different schemes per axis. Strict-PQ chains pin a lattice scheme here
|
||
// and refuse ECDSA wallets via ChainSecurityProfile.ForbidECDSAWallets.
|
||
type WalletSchemeID uint8
|
||
|
||
const (
|
||
WalletSchemeInvalid WalletSchemeID = 0x00
|
||
WalletSchemeMLDSA65 WalletSchemeID = 0x42 // FIPS 204 Cat 3 (production default)
|
||
WalletSchemeMLDSA87 WalletSchemeID = 0x43 // FIPS 204 Cat 5 (high-value)
|
||
WalletSchemeECDSAUnsafe WalletSchemeID = 0x90 // classical ECDSA; refused in strict-PQ
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (w WalletSchemeID) String() string {
|
||
switch w {
|
||
case WalletSchemeInvalid:
|
||
return "invalid"
|
||
case WalletSchemeMLDSA65:
|
||
return "ml-dsa-65"
|
||
case WalletSchemeMLDSA87:
|
||
return "ml-dsa-87"
|
||
case WalletSchemeECDSAUnsafe:
|
||
return "ecdsa-unsafe-classical-forbidden-in-pq"
|
||
default:
|
||
return fmt.Sprintf("wallet-scheme(0x%02x)", uint8(w))
|
||
}
|
||
}
|
||
|
||
// IsPostQuantum reports whether this wallet scheme is acceptable in
|
||
// strict-PQ mode. ML-DSA-65 and ML-DSA-87 qualify; classical ECDSA does
|
||
// not.
|
||
func (w WalletSchemeID) IsPostQuantum() bool {
|
||
return w == WalletSchemeMLDSA65 || w == WalletSchemeMLDSA87
|
||
}
|
||
|
||
// IsForbiddenInPQMode reports whether this wallet scheme carries the
|
||
// explicit forbidden marker. Used by audit tooling to detect a
|
||
// misconfiguration where a classical wallet would otherwise sneak past
|
||
// a PQ-only deployment.
|
||
func (w WalletSchemeID) IsForbiddenInPQMode() bool {
|
||
return w == WalletSchemeECDSAUnsafe
|
||
}
|
||
|
||
// TxSchemeID identifies the signature scheme a transaction's outer
|
||
// authorisation envelope was signed under. Distinct from WalletSchemeID
|
||
// (which names the wallet's identity key): tx auth may be delegated to
|
||
// a session key, a typed multisig, or a wallet's primary key — the
|
||
// scheme byte names the cryptographic primitive that produced the
|
||
// signature on the wire.
|
||
type TxSchemeID uint8
|
||
|
||
const (
|
||
TxSchemeInvalid TxSchemeID = 0x00
|
||
TxSchemeMLDSA65 TxSchemeID = 0x42
|
||
TxSchemeMLDSA87 TxSchemeID = 0x43
|
||
TxSchemeECDSAUnsafe TxSchemeID = 0x90
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (t TxSchemeID) String() string {
|
||
switch t {
|
||
case TxSchemeInvalid:
|
||
return "invalid"
|
||
case TxSchemeMLDSA65:
|
||
return "ml-dsa-65"
|
||
case TxSchemeMLDSA87:
|
||
return "ml-dsa-87"
|
||
case TxSchemeECDSAUnsafe:
|
||
return "ecdsa-unsafe-classical-forbidden-in-pq"
|
||
default:
|
||
return fmt.Sprintf("tx-scheme(0x%02x)", uint8(t))
|
||
}
|
||
}
|
||
|
||
// IsPostQuantum reports whether this tx scheme is acceptable in
|
||
// strict-PQ mode.
|
||
func (t TxSchemeID) IsPostQuantum() bool {
|
||
return t == TxSchemeMLDSA65 || t == TxSchemeMLDSA87
|
||
}
|
||
|
||
// IsForbiddenInPQMode reports whether this tx scheme carries the
|
||
// explicit forbidden marker.
|
||
func (t TxSchemeID) IsForbiddenInPQMode() bool {
|
||
return t == TxSchemeECDSAUnsafe
|
||
}
|
||
|
||
// ContractAuthID identifies the authorisation primitive a smart contract
|
||
// uses to gate a call. Broader than TxSchemeID: a contract may accept
|
||
// raw lattice signatures, multi-party threshold signatures, session-
|
||
// scoped capabilities, or Z-Chain validity proofs. The byte names the
|
||
// kind; the contract's authoriser policy names which kinds it accepts.
|
||
//
|
||
// The 0x10–0x2F block covers composite primitives (multi-party,
|
||
// proof-carrying); the 0x40 block mirrors raw FIPS 204 ML-DSA; the
|
||
// 0x90+ block names classical primitives that are explicitly forbidden
|
||
// in strict-PQ mode (ECDSA and classical BLS aggregates).
|
||
type ContractAuthID uint8
|
||
|
||
const (
|
||
ContractAuthInvalid ContractAuthID = 0x00
|
||
ContractAuthZChainProof ContractAuthID = 0x10 // Z-Chain validity proof
|
||
ContractAuthMultisigMLDSA ContractAuthID = 0x20 // threshold ML-DSA multisig
|
||
ContractAuthSessionPQ ContractAuthID = 0x21 // PQ session capability
|
||
ContractAuthMLDSA65 ContractAuthID = 0x42
|
||
ContractAuthMLDSA87 ContractAuthID = 0x43
|
||
ContractAuthECDSAUnsafe ContractAuthID = 0x90
|
||
ContractAuthBLSUnsafe ContractAuthID = 0x91 // classical BLS aggregate
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (c ContractAuthID) String() string {
|
||
switch c {
|
||
case ContractAuthInvalid:
|
||
return "invalid"
|
||
case ContractAuthZChainProof:
|
||
return "z-chain-proof"
|
||
case ContractAuthMultisigMLDSA:
|
||
return "multisig-ml-dsa"
|
||
case ContractAuthSessionPQ:
|
||
return "session-pq"
|
||
case ContractAuthMLDSA65:
|
||
return "ml-dsa-65"
|
||
case ContractAuthMLDSA87:
|
||
return "ml-dsa-87"
|
||
case ContractAuthECDSAUnsafe:
|
||
return "ecdsa-unsafe-classical-forbidden-in-pq"
|
||
case ContractAuthBLSUnsafe:
|
||
return "bls-unsafe-classical-forbidden-in-pq"
|
||
default:
|
||
return fmt.Sprintf("contract-auth(0x%02x)", uint8(c))
|
||
}
|
||
}
|
||
|
||
// IsPostQuantum reports whether this contract-auth primitive is
|
||
// acceptable in strict-PQ mode. Lattice signatures, lattice multisigs,
|
||
// PQ session capabilities, and Z-Chain proofs qualify; classical ECDSA
|
||
// and classical BLS aggregates do not.
|
||
func (c ContractAuthID) IsPostQuantum() bool {
|
||
switch c {
|
||
case ContractAuthZChainProof,
|
||
ContractAuthMultisigMLDSA,
|
||
ContractAuthSessionPQ,
|
||
ContractAuthMLDSA65,
|
||
ContractAuthMLDSA87:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// IsForbiddenInPQMode reports whether this contract-auth primitive
|
||
// carries the explicit forbidden marker.
|
||
func (c ContractAuthID) IsForbiddenInPQMode() bool {
|
||
return c == ContractAuthECDSAUnsafe || c == ContractAuthBLSUnsafe
|
||
}
|
||
|
||
// KeyExchangeID identifies the KEM (Key Encapsulation Mechanism) used
|
||
// for session key establishment, encrypted-state-transition envelopes,
|
||
// and operator-to-operator key wrapping. Distinct from every signature
|
||
// axis: a KEM provides confidentiality, not authenticity. Strict-PQ
|
||
// chains pin a lattice KEM (ML-KEM, FIPS 203) and refuse classical
|
||
// X25519 via ChainSecurityProfile.ForbidClassicalKEM.
|
||
//
|
||
// F103 KEM semantics — chain-wide vs TLS-layer:
|
||
//
|
||
// The profile field names the post-quantum KEM component that MUST be
|
||
// present on the wire. A peer-to-peer TLS handshake is permitted to
|
||
// negotiate the IANA-registered hybrid X25519MLKEM768 (0x11ec) as the
|
||
// curve preference: the hybrid is STRICTLY STRONGER than pure
|
||
// ML-KEM-768 (an attacker must break BOTH X25519 AND ML-KEM to derive
|
||
// the session key) and is the format real-world TLS 1.3 stacks
|
||
// implement today. The profile's KeyExchangeMLKEM768 value therefore
|
||
// reads as "the ML-KEM-768 component is present", not "X25519 is
|
||
// forbidden in this layer". ForbidClassicalKEM remains the gate that
|
||
// refuses a pure-classical curve (e.g. an X25519-only handshake) at
|
||
// the application layer; it does NOT refuse a hybrid that includes
|
||
// ML-KEM-768.
|
||
//
|
||
// Forks that pin a strictly-pure ML-KEM-768 TLS posture do so by
|
||
// overriding the chain's KeyExchangeID, NOT by adding a separate
|
||
// "pure vs hybrid" enum byte: every additional axis is an additional
|
||
// downstream verifier obligation, and the audit signed off on the
|
||
// single-byte KeyExchangeID surface.
|
||
type KeyExchangeID uint8
|
||
|
||
const (
|
||
KeyExchangeInvalid KeyExchangeID = 0x00
|
||
KeyExchangeMLKEM768 KeyExchangeID = 0x01 // FIPS 203 Cat 3 (production default; hybrid X25519MLKEM768 also acceptable at TLS layer)
|
||
KeyExchangeMLKEM1024 KeyExchangeID = 0x02 // FIPS 203 Cat 5 (high-value)
|
||
KeyExchangeX25519Unsafe KeyExchangeID = 0x90 // pure classical X25519; refused in strict-PQ via ForbidClassicalKEM
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (k KeyExchangeID) String() string {
|
||
switch k {
|
||
case KeyExchangeInvalid:
|
||
return "invalid"
|
||
case KeyExchangeMLKEM768:
|
||
return "ml-kem-768"
|
||
case KeyExchangeMLKEM1024:
|
||
return "ml-kem-1024"
|
||
case KeyExchangeX25519Unsafe:
|
||
return "x25519-unsafe-classical-forbidden-in-pq"
|
||
default:
|
||
return fmt.Sprintf("key-exchange(0x%02x)", uint8(k))
|
||
}
|
||
}
|
||
|
||
// IsPostQuantum reports whether this KEM is acceptable in strict-PQ
|
||
// mode. ML-KEM-768 and ML-KEM-1024 qualify; classical X25519 does not.
|
||
func (k KeyExchangeID) IsPostQuantum() bool {
|
||
return k == KeyExchangeMLKEM768 || k == KeyExchangeMLKEM1024
|
||
}
|
||
|
||
// IsForbiddenInPQMode reports whether this KEM carries the explicit
|
||
// forbidden marker.
|
||
func (k KeyExchangeID) IsForbiddenInPQMode() bool {
|
||
return k == KeyExchangeX25519Unsafe
|
||
}
|
||
|
||
// RecoverySchemeID identifies the signature scheme used by the chain's
|
||
// account-recovery / breakglass key. Two valid posture choices:
|
||
//
|
||
// - A stateless hash-based scheme (SLH-DSA, FIPS 205) at parameter
|
||
// set 128 / 192 / 256. Stateless backstop: even if every lattice
|
||
// assumption falls, recovery still works.
|
||
// - ML-DSA-87 (FIPS 204 Cat 5). Acceptable ONLY when the chain's
|
||
// HighValueSchemeID is also ML-DSA-87 / Pulsar-M-87 — the
|
||
// high-value scheme then provides the strong-classical fallback
|
||
// that recovery would otherwise need.
|
||
// - RecoverySchemeNone is permitted iff HighValueSchemeID is
|
||
// ML-DSA-87 or Pulsar-M-87 (no stateless backstop required when
|
||
// the high-value path is already at Cat 5).
|
||
type RecoverySchemeID uint8
|
||
|
||
const (
|
||
RecoverySchemeInvalid RecoverySchemeID = 0x00
|
||
RecoverySchemeSLHDSA128 RecoverySchemeID = 0x05 // FIPS 205 Cat 1
|
||
RecoverySchemeSLHDSA192 RecoverySchemeID = 0x06 // FIPS 205 Cat 3 (production default)
|
||
RecoverySchemeSLHDSA256 RecoverySchemeID = 0x07 // FIPS 205 Cat 5
|
||
RecoverySchemeMLDSA87 RecoverySchemeID = 0x43 // FIPS 204 Cat 5 (acceptable if HighValue is also Cat 5)
|
||
RecoverySchemeNone RecoverySchemeID = 0xFF // permitted only when HighValue is Cat 5
|
||
)
|
||
|
||
// String returns the canonical wire name.
|
||
func (r RecoverySchemeID) String() string {
|
||
switch r {
|
||
case RecoverySchemeInvalid:
|
||
return "invalid"
|
||
case RecoverySchemeSLHDSA128:
|
||
return "slh-dsa-128"
|
||
case RecoverySchemeSLHDSA192:
|
||
return "slh-dsa-192"
|
||
case RecoverySchemeSLHDSA256:
|
||
return "slh-dsa-256"
|
||
case RecoverySchemeMLDSA87:
|
||
return "ml-dsa-87"
|
||
case RecoverySchemeNone:
|
||
return "none"
|
||
default:
|
||
return fmt.Sprintf("recovery-scheme(0x%02x)", uint8(r))
|
||
}
|
||
}
|
||
|
||
// IsPostQuantum reports whether this recovery scheme is acceptable in
|
||
// strict-PQ mode. SLH-DSA (stateless hash-based) and ML-DSA-87 qualify;
|
||
// the None sentinel is policy-dependent (acceptable iff HighValue is
|
||
// Cat 5) and returns true here so the post-quantum predicate stays
|
||
// orthogonal to the cross-axis validity gate.
|
||
func (r RecoverySchemeID) IsPostQuantum() bool {
|
||
switch r {
|
||
case RecoverySchemeSLHDSA128,
|
||
RecoverySchemeSLHDSA192,
|
||
RecoverySchemeSLHDSA256,
|
||
RecoverySchemeMLDSA87,
|
||
RecoverySchemeNone:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// IsForbiddenInPQMode reports whether this recovery scheme carries the
|
||
// explicit forbidden marker. The current numbering reserves no 0x90+
|
||
// recovery scheme — any classical recovery posture is simply absent
|
||
// from the enum, so the predicate is trivially false today. Kept for
|
||
// API symmetry with the other E2E enums and for future-proofing.
|
||
func (r RecoverySchemeID) IsForbiddenInPQMode() bool {
|
||
return false
|
||
}
|
||
|
||
// IsStateless reports whether this recovery scheme is a stateless
|
||
// hash-based signature (SLH-DSA / FIPS 205). True for the 0x05..0x07
|
||
// block; false for ML-DSA-87 (lattice, stateful key tree implicit in
|
||
// the protocol) and for the None sentinel.
|
||
//
|
||
// Audit tooling uses IsStateless to confirm a chain's breakglass key
|
||
// has the strongest available standalone-of-lattice property: a
|
||
// stateless hash-based recovery survives even a complete break of
|
||
// lattice assumptions.
|
||
func (r RecoverySchemeID) IsStateless() bool {
|
||
switch r {
|
||
case RecoverySchemeSLHDSA128,
|
||
RecoverySchemeSLHDSA192,
|
||
RecoverySchemeSLHDSA256:
|
||
return true
|
||
}
|
||
return false
|
||
}
|