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/math v1.4.0
|
||||||
github.com/luxfi/mdns v0.1.0
|
github.com/luxfi/mdns v0.1.0
|
||||||
github.com/luxfi/metric v1.5.0
|
github.com/luxfi/metric v1.5.0
|
||||||
|
github.com/luxfi/pq v1.0.3
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/urfave/cli/v3 v3.6.2
|
github.com/urfave/cli/v3 v3.6.2
|
||||||
golang.org/x/crypto v0.49.0
|
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/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 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
|
||||||
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
|
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 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
|
||||||
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
|
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
|
||||||
github.com/luxfi/utils v1.1.3 h1:yDEuuJ6bm9H8DzOzzveXI1AZlu/AeSpHNRrY1akbyc0=
|
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.
|
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||||
// See the file LICENSE for licensing terms.
|
// 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
|
package fhe
|
||||||
|
|
||||||
// SecurityProfile is the security profile a Liquid FHE chain pins.
|
import "github.com/luxfi/pq"
|
||||||
// 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
|
|
||||||
|
|
||||||
const (
|
// DefaultParamsForMode is the single seam every Liquid FHE chain
|
||||||
// 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
|
|
||||||
// boot routes through to pick a ParametersLiteral. Picking
|
// boot routes through to pick a ParametersLiteral. Picking
|
||||||
// parameters directly (e.g. fhe.PN10QP27) anywhere in a Liquid FHE
|
// 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.
|
// silently weaken the chain to classical-only security.
|
||||||
//
|
//
|
||||||
// Strict-PQ profiles return PN9QP27_STD128Q (the only PQ
|
// Strict-PQ ⇒ PN9QP27_STD128Q (NIST FIPS 203 / 204 LMKCDEY).
|
||||||
// ParametersLiteral currently published by this package). Once
|
// Classical / hybrid ⇒ PN10QP27 (~128-bit classical, dev-only).
|
||||||
// STD192Q_LMKCDEY-aligned literals land, ProfileStrictPQHighValue
|
func DefaultParamsForMode(mode pq.Mode) ParametersLiteral {
|
||||||
// will return them; today both strict-PQ profiles return the same
|
if mode.IsPostQuantum() {
|
||||||
// PQ literal.
|
|
||||||
func DefaultParamsForProfile(p SecurityProfile) ParametersLiteral {
|
|
||||||
switch p {
|
|
||||||
case ProfileStrictPQ, ProfileStrictPQHighValue:
|
|
||||||
return PN9QP27_STD128Q
|
return PN9QP27_STD128Q
|
||||||
case ProfileClassical128:
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
return PN10QP27
|
|
||||||
}
|
}
|
||||||
}
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-66
@@ -3,79 +3,35 @@
|
|||||||
|
|
||||||
package fhe
|
package fhe
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
func TestSecurityProfile_String(t *testing.T) {
|
"github.com/luxfi/pq"
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSecurityProfile_IsPostQuantum(t *testing.T) {
|
// TestDefaultParamsForMode_StrictPQReturnsPQLiteral pins the
|
||||||
for _, tc := range []struct {
|
// security guarantee: pq.ModeStrictPQ MUST return a PQ
|
||||||
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
|
|
||||||
// ParametersLiteral, never a classical one. A regression that
|
// ParametersLiteral, never a classical one. A regression that
|
||||||
// pointed ProfileStrictPQ at PN10QP27 would silently weaken every
|
// pointed strict-PQ at PN10QP27 would silently weaken every Liquid
|
||||||
// Liquid FHE chain. The test asserts byte-identity, not just "some
|
// FHE chain. The test asserts byte-identity, not just "some PQ
|
||||||
// PQ literal", so swapping the underlying PQ literal name requires
|
// literal", so swapping the underlying PQ literal name requires an
|
||||||
// an explicit test update.
|
// explicit test update.
|
||||||
func TestDefaultParamsForProfile_StrictPQReturnsPQLiteral(t *testing.T) {
|
func TestDefaultParamsForMode_StrictPQReturnsPQLiteral(t *testing.T) {
|
||||||
for _, p := range []SecurityProfile{ProfileStrictPQ, ProfileStrictPQHighValue} {
|
got := DefaultParamsForMode(pq.ModeStrictPQ)
|
||||||
got := DefaultParamsForProfile(p)
|
if got != PN9QP27_STD128Q {
|
||||||
if got != PN9QP27_STD128Q {
|
t.Errorf("DefaultParamsForMode(StrictPQ) = %+v, want PN9QP27_STD128Q", got)
|
||||||
t.Errorf("DefaultParamsForProfile(%s) = %+v, want PN9QP27_STD128Q",
|
|
||||||
p, got)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDefaultParamsForProfile_ClassicalReturnsClassicalLiteral pins
|
// TestDefaultParamsForMode_ClassicalReturnsClassicalLiteral pins
|
||||||
// the converse: classical profile returns the classical literal.
|
// the converse: classical mode returns the classical literal.
|
||||||
// Default-case fall-through (unknown profile) also returns
|
// Hybrid also returns classical params — the gate is selection, not
|
||||||
// classical so a typo in the consumer cannot accidentally promote
|
// refusal, and FHE parameter sets don't have a hybrid mid-tier.
|
||||||
// to PQ params (whose keygen is ~10x slower and would surface as a
|
func TestDefaultParamsForMode_ClassicalReturnsClassicalLiteral(t *testing.T) {
|
||||||
// boot-time hang rather than a clear error).
|
for _, m := range []pq.Mode{pq.ModeClassical, pq.ModeHybrid} {
|
||||||
func TestDefaultParamsForProfile_ClassicalReturnsClassicalLiteral(t *testing.T) {
|
got := DefaultParamsForMode(m)
|
||||||
for _, p := range []SecurityProfile{ProfileClassical128, SecurityProfile(99)} {
|
|
||||||
got := DefaultParamsForProfile(p)
|
|
||||||
if got != PN10QP27 {
|
if got != PN10QP27 {
|
||||||
t.Errorf("DefaultParamsForProfile(%s) = %+v, want PN10QP27",
|
t.Errorf("DefaultParamsForMode(%s) = %+v, want PN10QP27", m, got)
|
||||||
p, 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