mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
return PN10QP27
|
||||
}
|
||||
|
||||
+22
-66
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user