diff --git a/go.mod b/go.mod index 923d9ce..ba5e33b 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/luxfi/math v1.4.0 github.com/luxfi/mdns v0.1.0 github.com/luxfi/metric v1.5.0 + github.com/luxfi/pq v1.0.3 github.com/spf13/cobra v1.10.2 github.com/urfave/cli/v3 v3.6.2 golang.org/x/crypto v0.49.0 diff --git a/go.sum b/go.sum index 3475cd5..3bdb23d 100644 --- a/go.sum +++ b/go.sum @@ -140,6 +140,8 @@ github.com/luxfi/metric v1.5.0 h1:11PKOjH6ntnnPF5GpspXFAqoKG9raqxT+zW5mN2jztI= github.com/luxfi/metric v1.5.0/go.mod h1:PkD4D4JoGuyKtfUkqPNYkrg9xKrJeNVZFdW/5XvAe/A= github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY= github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU= +github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs= +github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE= github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0= github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk= github.com/luxfi/utils v1.1.3 h1:yDEuuJ6bm9H8DzOzzveXI1AZlu/AeSpHNRrY1akbyc0= diff --git a/security_profile.go b/security_profile.go index 62bbedf..757ebf9 100644 --- a/security_profile.go +++ b/security_profile.go @@ -1,95 +1,42 @@ // Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. +// security_profile.go — strict-PQ adapter for the FHE parameter set. +// +// FHE chains have one decision to make at boot: which OpenFHE +// ParametersLiteral binds the security target. A classical +// ParametersLiteral is ~128-bit classical security (broken by Shor); +// a PQ ParametersLiteral lands in NIST FIPS 203 / 204 territory +// (LMKCDEY bootstrapping, lattice problem hardness preserved against +// quantum adversaries). +// +// This file plugs that single decision into the canonical pq.Mode +// gate so audit + chain-config code uses the same vocabulary as +// warp, dex, evm, zap: +// +// mode, _ := pq.ModeFromString(cfg.FHEProfile) +// params := fhe.DefaultParamsForMode(mode) +// +// Strict-PQ ⇒ PN9QP27_STD128Q (the only PQ literal currently +// published). Classical / hybrid fall back to PN10QP27 — there's no +// classical FHE ciphertext to refuse at the parameter layer, so the +// gate is selection, not refusal. + package fhe -// SecurityProfile is the security profile a Liquid FHE chain pins. -// There is no relaxed-PQ middle ground: a chain is strict-PQ or it -// isn't. -// -// The profile maps onto a specific ParametersLiteral so callers -// don't pick parameters at random and accidentally weaken the chain. -// DefaultParamsForProfile is the single seam every Liquid FHE chain -// boot routes through; direct references to PN10QP27 / PN11QP54 / -// PN9QP27_STD128Q in chain-config code are a bug (they bypass the -// profile gate). -type SecurityProfile int +import "github.com/luxfi/pq" -const ( - // ProfileClassical128 selects PN10QP27 — ~128-bit *classical* - // security. Acceptable for dev / regression testing only; a - // strict-PQ Liquid chain MUST NOT boot under this profile. - ProfileClassical128 SecurityProfile = iota - - // ProfileStrictPQ selects PN9QP27_STD128Q — 128-bit *quantum* - // security via OpenFHE's STD128Q_LMKCDEY parameter set (NIST - // FIPS 203 / 204 family alignment, LMKCDEY bootstrapping). - // This is the canonical Liquid FHE chain profile. - ProfileStrictPQ - - // ProfileStrictPQHighValue is reserved for the high-value tier - // (treasury, governance roots) once STD192Q_LMKCDEY-aligned - // ParametersLiterals land. For now it returns the same PQ - // literal as ProfileStrictPQ so consumers can wire - // IsPostQuantum()==true chains without holding for the - // high-value tier. - ProfileStrictPQHighValue -) - -// String renders the canonical wire name. Audit pipelines match on -// these strings; renaming here breaks every downstream consumer. -func (p SecurityProfile) String() string { - switch p { - case ProfileClassical128: - return "classical-128" - case ProfileStrictPQ: - return "strict-pq" - case ProfileStrictPQHighValue: - return "strict-pq-high-value" - default: - return "unknown" - } -} - -// IsPostQuantum reports whether this profile selects a parameter -// set whose security target is quantum-secure (NIST PQ alignment). -// ProfileClassical128 returns false; strict-PQ profiles return true. -func (p SecurityProfile) IsPostQuantum() bool { - return p == ProfileStrictPQ || p == ProfileStrictPQHighValue -} - -// DefaultParamsForProfile is the single seam every Liquid FHE chain +// DefaultParamsForMode is the single seam every Liquid FHE chain // boot routes through to pick a ParametersLiteral. Picking // parameters directly (e.g. fhe.PN10QP27) anywhere in a Liquid FHE -// chain's boot path is a bug — it bypasses the profile gate and can +// chain's boot path is a bug — it bypasses the mode gate and can // silently weaken the chain to classical-only security. // -// Strict-PQ profiles return PN9QP27_STD128Q (the only PQ -// ParametersLiteral currently published by this package). Once -// STD192Q_LMKCDEY-aligned literals land, ProfileStrictPQHighValue -// will return them; today both strict-PQ profiles return the same -// PQ literal. -func DefaultParamsForProfile(p SecurityProfile) ParametersLiteral { - switch p { - case ProfileStrictPQ, ProfileStrictPQHighValue: +// Strict-PQ ⇒ PN9QP27_STD128Q (NIST FIPS 203 / 204 LMKCDEY). +// Classical / hybrid ⇒ PN10QP27 (~128-bit classical, dev-only). +func DefaultParamsForMode(mode pq.Mode) ParametersLiteral { + if mode.IsPostQuantum() { return PN9QP27_STD128Q - case ProfileClassical128: - fallthrough - default: - return PN10QP27 } -} - -// ProfileFromPQFlag maps a chain-config "pq" boolean (as written by -// liquidity/operator into /data/configs/chains//config.json) to -// a SecurityProfile. true → ProfileStrictPQ; false → -// ProfileClassical128. Reading via this helper means a misnamed -// flag (e.g. "PQ" with capitals, "post_quantum") that bypasses the -// JSON tag silently degrades to ProfileClassical128 — and the -// calling chain boot rejects the boot if it expected strict-PQ. -func ProfileFromPQFlag(pq bool) SecurityProfile { - if pq { - return ProfileStrictPQ - } - return ProfileClassical128 + return PN10QP27 } diff --git a/security_profile_test.go b/security_profile_test.go index 62e83bc..a33fc56 100644 --- a/security_profile_test.go +++ b/security_profile_test.go @@ -3,79 +3,35 @@ package fhe -import "testing" +import ( + "testing" -func TestSecurityProfile_String(t *testing.T) { - for _, tc := range []struct { - profile SecurityProfile - want string - }{ - {ProfileClassical128, "classical-128"}, - {ProfileStrictPQ, "strict-pq"}, - {ProfileStrictPQHighValue, "strict-pq-high-value"}, - {SecurityProfile(99), "unknown"}, - } { - if got := tc.profile.String(); got != tc.want { - t.Errorf("SecurityProfile(%d).String() = %q, want %q", - tc.profile, got, tc.want) - } - } -} + "github.com/luxfi/pq" +) -func TestSecurityProfile_IsPostQuantum(t *testing.T) { - for _, tc := range []struct { - profile SecurityProfile - want bool - }{ - {ProfileClassical128, false}, - {ProfileStrictPQ, true}, - {ProfileStrictPQHighValue, true}, - } { - if got := tc.profile.IsPostQuantum(); got != tc.want { - t.Errorf("%s.IsPostQuantum() = %t, want %t", - tc.profile, got, tc.want) - } - } -} - -// TestDefaultParamsForProfile_StrictPQReturnsPQLiteral pins the -// security guarantee: a strict-PQ profile MUST return a PQ +// TestDefaultParamsForMode_StrictPQReturnsPQLiteral pins the +// security guarantee: pq.ModeStrictPQ MUST return a PQ // ParametersLiteral, never a classical one. A regression that -// pointed ProfileStrictPQ at PN10QP27 would silently weaken every -// Liquid FHE chain. The test asserts byte-identity, not just "some -// PQ literal", so swapping the underlying PQ literal name requires -// an explicit test update. -func TestDefaultParamsForProfile_StrictPQReturnsPQLiteral(t *testing.T) { - for _, p := range []SecurityProfile{ProfileStrictPQ, ProfileStrictPQHighValue} { - got := DefaultParamsForProfile(p) - if got != PN9QP27_STD128Q { - t.Errorf("DefaultParamsForProfile(%s) = %+v, want PN9QP27_STD128Q", - p, got) - } +// pointed strict-PQ at PN10QP27 would silently weaken every Liquid +// FHE chain. The test asserts byte-identity, not just "some PQ +// literal", so swapping the underlying PQ literal name requires an +// explicit test update. +func TestDefaultParamsForMode_StrictPQReturnsPQLiteral(t *testing.T) { + got := DefaultParamsForMode(pq.ModeStrictPQ) + if got != PN9QP27_STD128Q { + t.Errorf("DefaultParamsForMode(StrictPQ) = %+v, want PN9QP27_STD128Q", got) } } -// TestDefaultParamsForProfile_ClassicalReturnsClassicalLiteral pins -// the converse: classical profile returns the classical literal. -// Default-case fall-through (unknown profile) also returns -// classical so a typo in the consumer cannot accidentally promote -// to PQ params (whose keygen is ~10x slower and would surface as a -// boot-time hang rather than a clear error). -func TestDefaultParamsForProfile_ClassicalReturnsClassicalLiteral(t *testing.T) { - for _, p := range []SecurityProfile{ProfileClassical128, SecurityProfile(99)} { - got := DefaultParamsForProfile(p) +// TestDefaultParamsForMode_ClassicalReturnsClassicalLiteral pins +// the converse: classical mode returns the classical literal. +// Hybrid also returns classical params — the gate is selection, not +// refusal, and FHE parameter sets don't have a hybrid mid-tier. +func TestDefaultParamsForMode_ClassicalReturnsClassicalLiteral(t *testing.T) { + for _, m := range []pq.Mode{pq.ModeClassical, pq.ModeHybrid} { + got := DefaultParamsForMode(m) if got != PN10QP27 { - t.Errorf("DefaultParamsForProfile(%s) = %+v, want PN10QP27", - p, got) + t.Errorf("DefaultParamsForMode(%s) = %+v, want PN10QP27", m, got) } } } - -func TestProfileFromPQFlag(t *testing.T) { - if got := ProfileFromPQFlag(true); got != ProfileStrictPQ { - t.Errorf("ProfileFromPQFlag(true) = %s, want strict-pq", got) - } - if got := ProfileFromPQFlag(false); got != ProfileClassical128 { - t.Errorf("ProfileFromPQFlag(false) = %s, want classical-128", got) - } -}