Files
zooqueen d01dcfa1c1 feat(quasar): ConsensusCert envelope — policy-gated threshold certification
Canonical finality cert: one policy-gated envelope (VerifyConsensusCert) with
per-leg evidence modes — ThresholdSig (lattice O(1)), WeightedSigSet (independent
FIPS sigs + weighted-Merkle quorum; the only mode for SLH-DSA, never
seed-reconstructed), StarkCompressedSigSet (P3Q, same predicate, audit-gated),
ClassicalAggregate (BLS, never satisfies a PQ leg). Required legs are
policy-derived (CertPolicy.RequiredLegs), never cert bytes (Red H3 anti-downgrade);
full-tuple domain binding. WeightedQuorumCert foundation: policy-as-parameter,
message recomputed internally, strictly-increasing IDs, checked weight.

27 named envelope tests + F1-F5 + 6 CTO attacks PASS; verifier total (fuzz, zero
panics); a fuzz-found threshold-sig flags-byte malleability fixed. Fixed go.sum
[email protected] to canonical transparency-log hashes. Scrubbed ~54MB tracked binaries.
2026-06-21 20:39:11 -07:00

225 lines
9.2 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// quorum_scheme.go — the per-signer scheme axis of a weighted quorum
// certificate, plus the pluggable FIPS verifier registry.
//
// A weighted quorum certificate is scheme-agnostic: it proves that a
// quorum-weight subset of validators each produced a valid INDEPENDENT
// signature over the same domain-separated consensus message. "Independent"
// is the whole point — there is no key sharing, no reconstruction, no seed /
// WOTS+ / FORS / secret material combined. Each signer record names the
// scheme it signed under; the verifier dispatches to the unmodified FIPS
// verifier for that scheme.
//
// This is threshold CERTIFICATION, not threshold signing. The contrast with
// the Pulsar / Corona threshold legs of QuasarCert is deliberate: those run
// a DKG and emit ONE group signature; this runs no ceremony and emits N
// per-validator signatures that any peer can check with a stock FIPS
// verifier and no Lux dependency.
package quasar
import (
"errors"
"fmt"
"github.com/luxfi/crypto/mldsa"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
)
// QuorumSchemeID names the signature scheme a single quorum-certificate
// signer record was produced under. Orthogonal to config.SigSchemeID (the
// threshold-finality axis): a quorum cert's signer signs INDEPENDENTLY, so
// this axis names the per-signer FIPS relation the verifier dispatches to.
//
// The byte values intentionally MIRROR the canonical Lux wire bytes for the
// same primitives so a reader sees consistent numbering across the codebase:
//
// 0x41..0x43 — ML-DSA per FIPS 204 (matches config.SigSchemeID 0x40 block)
// 0x05..0x07 — SLH-DSA per FIPS 205 (matches config.RecoverySchemeID block)
//
// New schemes (e.g. an independent-signature Corona) claim the next free
// integer in their family block and MUST register a verifier in
// defaultQuorumVerifiers before any cert references them. Never reuse a
// retired id.
type QuorumSchemeID uint8
const (
QuorumSchemeNone QuorumSchemeID = 0x00
// FIPS 204 ML-DSA (Module-LWE lattice signature). Independent per-signer.
QuorumSchemeMLDSA44 QuorumSchemeID = 0x41
QuorumSchemeMLDSA65 QuorumSchemeID = 0x42 // production identity default
QuorumSchemeMLDSA87 QuorumSchemeID = 0x43
// FIPS 205 SLH-DSA (hash-based stateless signature). Independent
// per-signer; the cross-family backstop. Byte values match
// config.RecoverySchemeID's SLH-DSA block.
QuorumSchemeSLHDSA192s QuorumSchemeID = 0x06 // Magnetar production (SHAKE-192s)
QuorumSchemeSLHDSA192f QuorumSchemeID = 0x16 // SHAKE-192f
QuorumSchemeSLHDSA256s QuorumSchemeID = 0x07 // SHAKE-256s
)
// String returns the canonical wire name.
func (s QuorumSchemeID) String() string {
switch s {
case QuorumSchemeNone:
return "none"
case QuorumSchemeMLDSA44:
return "ml-dsa-44"
case QuorumSchemeMLDSA65:
return "ml-dsa-65"
case QuorumSchemeMLDSA87:
return "ml-dsa-87"
case QuorumSchemeSLHDSA192s:
return "slh-dsa-192s"
case QuorumSchemeSLHDSA192f:
return "slh-dsa-192f"
case QuorumSchemeSLHDSA256s:
return "slh-dsa-256s"
default:
return fmt.Sprintf("quorum-scheme(0x%02x)", uint8(s))
}
}
// FIPS reports the FIPS standard the scheme's verifier implements. "204"
// for ML-DSA, "205" for SLH-DSA, "" for unknown. The certificate's NIST
// posture rests on these being unmodified FIPS verifiers.
func (s QuorumSchemeID) FIPS() string {
switch s {
case QuorumSchemeMLDSA44, QuorumSchemeMLDSA65, QuorumSchemeMLDSA87:
return "204"
case QuorumSchemeSLHDSA192s, QuorumSchemeSLHDSA192f, QuorumSchemeSLHDSA256s:
return "205"
default:
return ""
}
}
var (
// ErrUnknownQuorumScheme is returned when a signer record names a
// scheme with no registered verifier. Treated as a per-record INVALID
// (not a fatal/DoS) by the certificate verifier.
ErrUnknownQuorumScheme = errors.New("quasar: unknown quorum signature scheme")
// ErrSchemeNotAllowed is returned (as a per-record invalidation) when a
// signer record's scheme is not in the verifier's allowed set.
ErrSchemeNotAllowed = errors.New("quasar: quorum signature scheme not in allowed set")
)
// quorumVerifyFn is the pluggable per-signature verification function. It
// MUST be a thin dispatch over an UNMODIFIED FIPS verifier — adding logic
// here breaks the wire-identity property that lets any peer verify a record
// with a stock FIPS library and no Lux dependency.
//
// Contract:
// - pubKey is the signer's canonical public-key bytes (exactly as bound
// into the validator-set leaf).
// - message is the domain-separated consensus message (the round digest
// subject — see quorum_message.go).
// - ctx is the FIPS context string (≤255 bytes), bound at sign time.
// - returns true iff the signature verifies; never panics.
type quorumVerifyFn func(pubKey, message, ctx, sig []byte) bool
// defaultQuorumVerifiers is the canonical scheme→verifier registry. Each
// entry is a thin dispatch over a stock FIPS verifier:
//
// ML-DSA → github.com/luxfi/crypto/mldsa PublicKey.VerifySignatureCtx (FIPS 204)
// SLH-DSA → github.com/luxfi/magnetar slhdsa.Verify via VerifyCtx (FIPS 205)
//
// The map is the single source of truth for which schemes a quorum cert
// can carry. A QuorumVerifierConfig may restrict to a subset but never
// adds schemes the registry does not know.
var defaultQuorumVerifiers = map[QuorumSchemeID]quorumVerifyFn{
QuorumSchemeMLDSA44: mldsaVerifier(mldsa.MLDSA44),
QuorumSchemeMLDSA65: mldsaVerifier(mldsa.MLDSA65),
QuorumSchemeMLDSA87: mldsaVerifier(mldsa.MLDSA87),
QuorumSchemeSLHDSA192s: slhdsaVerifier(magnetar.ModeM192s),
QuorumSchemeSLHDSA192f: slhdsaVerifier(magnetar.ModeM192f),
QuorumSchemeSLHDSA256s: slhdsaVerifier(magnetar.ModeM256s),
}
// mldsaVerifier builds a quorumVerifyFn over the unmodified FIPS 204
// ML-DSA verifier for a fixed parameter set. The public key is parsed
// per-call from its canonical bytes; a parse failure or wrong-length key is
// a clean false (the record is INVALID, not fatal).
func mldsaVerifier(mode mldsa.Mode) quorumVerifyFn {
return func(pubKey, message, ctx, sig []byte) bool {
pk, err := mldsa.PublicKeyFromBytes(pubKey, mode)
if err != nil || pk == nil {
return false
}
// VerifySignatureCtx is the stock FIPS 204 verify with a context
// string per §5.4; an empty ctx (nil) is the empty-context case.
return pk.VerifySignatureCtx(message, sig, ctx)
}
}
// slhdsaVerifier builds a quorumVerifyFn over the unmodified FIPS 205
// SLH-DSA verifier (via magnetar's thin VerifyCtx dispatch over
// circl/slhdsa) for a fixed parameter set.
func slhdsaVerifier(mode magnetar.Mode) quorumVerifyFn {
params, perr := magnetar.ParamsFor(mode)
return func(pubKey, message, ctx, sig []byte) bool {
if perr != nil || params == nil {
return false
}
if len(pubKey) != params.PublicKeySize || len(sig) != params.SignatureSize {
return false
}
pk := &magnetar.PublicKey{Mode: mode, Bytes: append([]byte(nil), pubKey...)}
s := &magnetar.Signature{Mode: mode, Bytes: append([]byte(nil), sig...)}
// VerifyCtx returns nil iff the signature is valid under
// FIPS 205 SLH-DSA.Verify; any error => invalid record.
return magnetar.VerifyCtx(params, pk, message, ctx, s) == nil
}
}
// contextForScheme returns the FIPS context string to verify a record of the
// given scheme under, resolving the mixed-scheme context asymmetry.
//
// The two FIPS sign paths used here bind DIFFERENT contexts:
// - ML-DSA (FIPS 204) records are signed via mldsa.SignCtx under the
// caller's context, so they MUST verify under cfg.Context.
// - SLH-DSA (FIPS 205) records are signed via magnetar.ValidatorSign, which
// binds the EMPTY FIPS-205 context (the round-digest message already
// carries the full domain binding). They MUST verify under the empty
// context regardless of cfg.Context.
//
// Applying one cfg.Context to every scheme would make a mixed ML-DSA ∧
// SLH-DSA cert verify only when cfg.Context is nil. Selecting the context per
// scheme family removes that footgun: the empty context is forced for the
// SLH-DSA leg, so a mixed cert verifies under any cfg.Context the ML-DSA
// signers used.
func contextForScheme(cfg QuorumVerifierConfig, scheme QuorumSchemeID) []byte {
if scheme.FIPS() == "205" {
// SLH-DSA: empty FIPS-205 context (matches magnetar.ValidatorSign).
return nil
}
return cfg.Context
}
// expectedPubKeyLen returns the canonical public-key byte length for a
// scheme, or (0, false) if the scheme is unknown. Used by the certificate
// verifier to reject a record whose embedded key length cannot match the
// scheme before any signature dispatch.
func expectedPubKeyLen(scheme QuorumSchemeID) (int, bool) {
switch scheme {
case QuorumSchemeMLDSA44:
return mldsa.GetPublicKeySize(mldsa.MLDSA44), true
case QuorumSchemeMLDSA65:
return mldsa.GetPublicKeySize(mldsa.MLDSA65), true
case QuorumSchemeMLDSA87:
return mldsa.GetPublicKeySize(mldsa.MLDSA87), true
case QuorumSchemeSLHDSA192s:
return magnetar.ParamsM192s.PublicKeySize, true
case QuorumSchemeSLHDSA192f:
return magnetar.ParamsM192f.PublicKeySize, true
case QuorumSchemeSLHDSA256s:
return magnetar.ParamsM256s.PublicKeySize, true
default:
return 0, false
}
}