fhe: decomplect onto luxfi/pq.Mode gate

Drop local SecurityProfile, ProfileClassical128, ProfileStrictPQ,
ProfileStrictPQHighValue, IsPostQuantum, String,
ProfileFromPQFlag. The strict-PQ Mode vocabulary lives in
github.com/luxfi/pq as the single source of truth across warp,
dex, evm, fhe, zap.

DefaultParamsForProfile (local enum) → DefaultParamsForMode
(pq.Mode): strict-PQ ⇒ PN9QP27_STD128Q (NIST FIPS 203 / 204
LMKCDEY); classical / hybrid ⇒ PN10QP27 (~128-bit classical,
dev-only). The FHE gate is parameter SELECTION, not refusal — no
classical ciphertext to refuse at the parameter layer, so the
ValidateMode call doesn't apply here.

ProfileStrictPQHighValue dropped: it was a no-op tier returning
the same PQ literal as the base strict-PQ profile. Once
STD192Q_LMKCDEY-aligned ParametersLiterals land, callers pick
them explicitly — the mode vocabulary stays single-purpose.
This commit is contained in:
Hanzo AI
2026-05-12 19:45:14 -07:00
parent fd4fa47ed5
commit 599ee814d9
4 changed files with 54 additions and 148 deletions
+1
View File
@@ -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
+2
View File
@@ -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=
+29 -82
View File
@@ -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/<id>/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
}
+21 -65
View File
@@ -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)
// 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("DefaultParamsForProfile(%s) = %+v, want PN9QP27_STD128Q",
p, got)
}
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)
}
}