Files
node/service/security/types.go
T
zeekayandHanzo Dev 51a304804c chore: migrate luxd HTTP routes /ext -> /v1
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>
2026-07-01 11:40:13 -07:00

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
}
}