mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route surface (one way, no backward compat). The node's baseURL is the source of truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the k8s/compose/gateway/explorer configs are updated to match. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
302 lines
12 KiB
Go
302 lines
12 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package security exposes the chain-wide ChainSecurityProfile to operators,
|
|
// dApps, and auditors through the security JSON-RPC namespace at
|
|
// /v1/security and two REST sidecars at /v1/security/profile and
|
|
// /v1/security/block/{n}.
|
|
//
|
|
// The handlers in this package are read-only: every shape is derived from
|
|
// the immutable *consensusconfig.ChainSecurityProfile resolved at node
|
|
// bootstrap (Node.initSecurityProfile, F102). There is no per-request
|
|
// re-resolution and no profile mutation surface.
|
|
//
|
|
// One service, one namespace ("security"), one fixed JSON shape. The
|
|
// wire vocabulary uses SCREAMING_SNAKE_CASE canonical names
|
|
// (renderName) so audit pipelines can grep on stable identifiers; the
|
|
// underlying scheme IDs come from consensus/config.
|
|
package security
|
|
|
|
import (
|
|
consensusconfig "github.com/luxfi/consensus/config"
|
|
)
|
|
|
|
// ProfileReply is the JSON body returned by the securityProfile RPC
|
|
// (POST /v1/security) and the REST sidecar (GET /v1/security/profile).
|
|
//
|
|
// Stable shape: every field has a fixed JSON tag, no embedded structs.
|
|
// Adding a new field requires bumping the major version of the security
|
|
// namespace; renaming an existing field is forbidden.
|
|
//
|
|
// The field order in the struct literal MUST match the field order in
|
|
// the locked profile (consensus/config.profiles.go) so a developer
|
|
// reviewing a diff against the canonical profile sees the same shape.
|
|
type ProfileReply struct {
|
|
// ProfileID is the wire byte of the active profile (see
|
|
// consensus/config.ProfileID). Stable across upgrades.
|
|
ProfileID uint32 `json:"profile_id"`
|
|
|
|
// ProfileName is the canonical SCREAMING_SNAKE_CASE name (e.g.
|
|
// "STRICT_PQ"). Audit tooling matches on this name.
|
|
ProfileName string `json:"profile_name"`
|
|
|
|
// ProfileHash is the 48-byte SHA3-384 commitment to the profile
|
|
// encoded as a 0x-prefixed lowercase hex string (96 hex chars).
|
|
// Distinct profiles produce distinct hashes; identical profiles
|
|
// produce identical hashes regardless of allow-list ordering.
|
|
ProfileHash string `json:"profile_hash"`
|
|
|
|
// PostQuantumEndToEnd reports whether every E2E axis (wallet, tx,
|
|
// contract-auth, KEM, recovery) is satisfied by a NIST-PQ
|
|
// primitive AND every classical primitive is explicitly
|
|
// forbidden. Single boolean for dashboard wiring.
|
|
PostQuantumEndToEnd bool `json:"post_quantum_end_to_end"`
|
|
|
|
// NISTFriendly reports whether the chain pins a NIST-aligned
|
|
// canonical Lux profile (StrictPQ or FIPS). A profile whose
|
|
// HashSuiteID is SHA3_NIST but whose Forbid* bits are disabled
|
|
// (the classical-compat fork case) is NOT NIST-friendly: NIST
|
|
// alignment is a full-posture predicate, not a hash-suite axis.
|
|
NISTFriendly bool `json:"nist_friendly"`
|
|
|
|
// LuxCanonical reports whether the profile is one of the three
|
|
// audit-signed-off-on Lux canonical profiles (StrictPQ, FIPS,
|
|
// Permissive). Downstream forks that pin a different ProfileID
|
|
// MUST receive false here so audit attestations refuse them.
|
|
LuxCanonical bool `json:"lux_canonical"`
|
|
|
|
// Scheme axes — each rendered as the SCREAMING_SNAKE canonical
|
|
// name of the scheme byte. None / Invalid / unknown bytes are
|
|
// rendered as the generic "UNKNOWN_0x<hex>" form so a forked
|
|
// binary that pins an unknown byte cannot silently masquerade as
|
|
// a canonical scheme.
|
|
HashSuite string `json:"hash_suite"`
|
|
WalletScheme string `json:"wallet_scheme"`
|
|
TxScheme string `json:"tx_scheme"`
|
|
ContractAuth string `json:"contract_auth"`
|
|
ValidatorScheme string `json:"validator_scheme"`
|
|
FinalityScheme string `json:"finality_scheme"`
|
|
HighValueScheme string `json:"high_value_scheme"`
|
|
ProofPolicy string `json:"proof_policy"`
|
|
KeyExchange string `json:"key_exchange"`
|
|
HighValueKEM string `json:"high_value_kem"`
|
|
RecoveryScheme string `json:"recovery_scheme"`
|
|
|
|
// Forbid* axes — one boolean per refused-primitive class. Audit
|
|
// tooling reads these to verify a deployment refuses what it
|
|
// claims to refuse. Strict-PQ profiles set every entry true.
|
|
ForbidECDSAWallets bool `json:"forbid_ecdsa_wallets"`
|
|
ForbidECDSAContractAuth bool `json:"forbid_ecdsa_contract_auth"`
|
|
ForbidBLSContractAuth bool `json:"forbid_bls_contract_auth"`
|
|
ForbidClassicalKEM bool `json:"forbid_classical_kem"`
|
|
RequireTypedTxAuth bool `json:"require_typed_tx_auth"`
|
|
ForbidPairings bool `json:"forbid_pairings"`
|
|
ForbidKZG bool `json:"forbid_kzg"`
|
|
ForbidTrustedSetup bool `json:"forbid_trusted_setup"`
|
|
ForbidClassicalSNARKs bool `json:"forbid_classical_snarks"`
|
|
ForbidDevProofs bool `json:"forbid_dev_proofs"`
|
|
ForbidFallbacks bool `json:"forbid_fallbacks"`
|
|
}
|
|
|
|
// BlockSecurityReply is the JSON body returned by the blockSecurity
|
|
// RPC (POST /v1/security) and the REST endpoint
|
|
// /v1/security/block/{n}. It enriches a block lookup with the
|
|
// chain-wide security envelope so explorers can show "this block was
|
|
// finalised under profile X with backend Y" without reimplementing
|
|
// profile lookup.
|
|
//
|
|
// pulsar_m_signature_valid is true iff the chain-wide FinalitySchemeID
|
|
// is a Pulsar-M scheme — the per-block signature is verified by the
|
|
// consensus layer before the block is accepted, so a block reachable
|
|
// via this API is already known-valid. Explorers that want to
|
|
// double-verify can re-fetch the cert envelope; this field is the
|
|
// summary boolean.
|
|
//
|
|
// proof_backend_id reports the chain-wide canonical proof backend
|
|
// (first entry of AllowedProofBackends). Per-block backend variance is
|
|
// not part of the chain's posture; if a chain accepts multiple
|
|
// backends, the explorer should fetch the cert envelope for the exact
|
|
// backend used.
|
|
type BlockSecurityReply struct {
|
|
// SecurityProfileID is the wire byte of the chain's active
|
|
// profile at the time the block was accepted. Constant for a
|
|
// chain that has not undergone a profile migration.
|
|
SecurityProfileID uint32 `json:"security_profile_id"`
|
|
|
|
// SecurityProfileName mirrors ProfileReply.ProfileName.
|
|
SecurityProfileName string `json:"security_profile_name"`
|
|
|
|
// PulsarMSignatureValid is true iff the chain's finality scheme
|
|
// is a Pulsar-M variant. Blocks accepted under this profile have
|
|
// already passed the finality-signature gate at the consensus
|
|
// layer; this field is the explorer-facing summary.
|
|
PulsarMSignatureValid bool `json:"pulsar_m_signature_valid"`
|
|
|
|
// ProofBackendID is the wire byte of the chain's canonical
|
|
// proof backend (first AllowedProofBackends entry). Pre-rendered
|
|
// here for explorer convenience.
|
|
ProofBackendID uint32 `json:"proof_backend_id"`
|
|
|
|
// ProofBackendName is the canonical SCREAMING_SNAKE_CASE name of
|
|
// the chain's proof backend.
|
|
ProofBackendName string `json:"proof_backend_name"`
|
|
|
|
// PostQuantumEndToEnd mirrors ProfileReply.PostQuantumEndToEnd.
|
|
// Duplicated onto the block-level reply so a single block
|
|
// fetch carries the full E2E posture.
|
|
PostQuantumEndToEnd bool `json:"post_quantum_end_to_end"`
|
|
}
|
|
|
|
// renderName converts an scheme String() (kebab-lowercase, e.g.
|
|
// "ml-dsa-65", "sha3-nist", "stark-fri-sha3-pq") to the
|
|
// SCREAMING_SNAKE_CASE canonical form ("ML_DSA_65", "SHA3_NIST",
|
|
// "STARK_FRI_SHA3_PQ") used on the wire.
|
|
//
|
|
// Single point of translation so the wire vocabulary stays consistent
|
|
// across every axis. Audit tooling matches on these names; renaming
|
|
// here breaks every downstream consumer, so do not.
|
|
func renderName(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
out := make([]byte, len(s))
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
switch {
|
|
case c == '-':
|
|
out[i] = '_'
|
|
case c >= 'a' && c <= 'z':
|
|
out[i] = c - ('a' - 'A')
|
|
default:
|
|
out[i] = c
|
|
}
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// hashSuiteName returns the canonical SCREAMING_SNAKE name for the
|
|
// HashSuiteID byte. Unknown bytes get the generic UNKNOWN_0x<hex> form
|
|
// so a forked profile can't silently masquerade.
|
|
func hashSuiteName(h consensusconfig.HashSuiteID) string {
|
|
switch h {
|
|
case consensusconfig.HashSuiteSHA3NIST:
|
|
return "SHA3_NIST"
|
|
case consensusconfig.HashSuiteBLAKE3Legacy:
|
|
return "BLAKE3_LEGACY"
|
|
default:
|
|
return renderName(h.String())
|
|
}
|
|
}
|
|
|
|
// proofBackendName returns the canonical SCREAMING_SNAKE name for the
|
|
// ProofBackendID byte. Mirrors hashSuiteName for the backend axis.
|
|
func proofBackendName(b consensusconfig.ProofBackendID) string {
|
|
return renderName(b.String())
|
|
}
|
|
|
|
// contractAuthName renders the contract-auth axis. Under a strict-PQ
|
|
// profile the chain accepts ML-DSA-65 AND Z-Chain validity proofs;
|
|
// the profile pins one default ID but the second is implicitly
|
|
// admissible. Reflect that union on the wire so a dApp reading the
|
|
// RPC sees both gates it can route through.
|
|
//
|
|
// Forks that pin a different ContractAuthID get just the rendered
|
|
// name of that byte.
|
|
func contractAuthName(profile *consensusconfig.ChainSecurityProfile) string {
|
|
if profile == nil {
|
|
return ""
|
|
}
|
|
base := renderName(profile.ContractAuthID.String())
|
|
if profile.ProfileID == uint32(consensusconfig.ProfileStrictPQ) ||
|
|
profile.ProfileID == uint32(consensusconfig.ProfileFIPS) {
|
|
// Strict-PQ + FIPS implicitly admit Z-Chain validity proofs
|
|
// as a contract-auth primitive in addition to the raw
|
|
// lattice scheme pinned on the profile.
|
|
return base + "|ZCHAIN_AUTH_PROOF"
|
|
}
|
|
return base
|
|
}
|
|
|
|
// validatorSchemeName returns the canonical name of the identity
|
|
// scheme the chain accepts for validator registration. Identity
|
|
// scheme is the IdentitySchemeID (raw FIPS 204 ML-DSA on every
|
|
// locked profile).
|
|
func validatorSchemeName(profile *consensusconfig.ChainSecurityProfile) string {
|
|
if profile == nil {
|
|
return ""
|
|
}
|
|
return renderName(profile.IdentitySchemeID.String())
|
|
}
|
|
|
|
// isPostQuantumEndToEnd reports whether the profile satisfies the
|
|
// HIP-0078 §"E2E PQ surface" gate: every scheme axis is post-quantum
|
|
// AND every classical primitive is forbidden.
|
|
//
|
|
// This is the single boolean a dashboard / wallet / dApp reads to
|
|
// decide "am I behind a strict-PQ chain?". A profile that fails any
|
|
// axis returns false even if it satisfies the rest.
|
|
func isPostQuantumEndToEnd(profile *consensusconfig.ChainSecurityProfile) bool {
|
|
if profile == nil {
|
|
return false
|
|
}
|
|
return profile.WalletSchemeID.IsPostQuantum() &&
|
|
profile.TxSchemeID.IsPostQuantum() &&
|
|
profile.KeyExchangeID.IsPostQuantum() &&
|
|
profile.HighValueKEM.IsPostQuantum() &&
|
|
profile.RecoverySchemeID.IsPostQuantum() &&
|
|
profile.ContractAuthID.IsPostQuantum() &&
|
|
profile.ProofPolicyID.IsPostQuantum() &&
|
|
profile.ForbidECDSAWallets &&
|
|
profile.ForbidECDSAContractAuth &&
|
|
profile.ForbidBLSContractAuth &&
|
|
profile.ForbidClassicalKEM &&
|
|
profile.ForbidPairings &&
|
|
profile.ForbidKZG &&
|
|
profile.ForbidTrustedSetup &&
|
|
profile.ForbidClassicalSNARKs &&
|
|
profile.RequireTypedTxAuth
|
|
}
|
|
|
|
// isLuxCanonical reports whether profile.ProfileID matches one of the
|
|
// three audit-signed-off-on Lux profiles (StrictPQ, Permissive, FIPS).
|
|
// Downstream forks that claim a different byte return false here.
|
|
func isLuxCanonical(profile *consensusconfig.ChainSecurityProfile) bool {
|
|
if profile == nil {
|
|
return false
|
|
}
|
|
switch profile.ProfileID {
|
|
case uint32(consensusconfig.ProfileStrictPQ),
|
|
uint32(consensusconfig.ProfilePermissive),
|
|
uint32(consensusconfig.ProfileFIPS):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// isNISTFriendly reports whether the chain pins a NIST-aligned
|
|
// canonical Lux profile (StrictPQ or FIPS only — Permissive is
|
|
// canonical but allows dev backends so it does NOT carry the NIST
|
|
// posture). A profile whose HashSuiteID is SHA3_NIST but whose
|
|
// Forbid* bits are disabled (the classical-compat fork case) returns
|
|
// false: NIST alignment is a full-posture predicate, not a single-
|
|
// axis hash check.
|
|
//
|
|
// Single point of definition — service, metrics, and the banner all
|
|
// route through this predicate so the wire vocabulary stays consistent.
|
|
func isNISTFriendly(profile *consensusconfig.ChainSecurityProfile) bool {
|
|
if profile == nil {
|
|
return false
|
|
}
|
|
if profile.HashSuiteID != consensusconfig.HashSuiteSHA3NIST {
|
|
return false
|
|
}
|
|
switch profile.ProfileID {
|
|
case uint32(consensusconfig.ProfileStrictPQ),
|
|
uint32(consensusconfig.ProfileFIPS):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|