mirror of
https://github.com/luxfi/threshold.git
synced 2026-07-27 04:01:59 +00:00
security: fail-CLOSED strict-PQ gate — empty/unknown chainID DEFAULT-REFUSE
The one canonical RefuseUnderStrictPQ gate (decomplected — magnetar's duplicate slhdsatee gate removed with the TEE extraction). Drop the chainID=="" short-circuit so empty/unknown chains consult the resolver; production wires def=ProfileStrictPQ → refuse. Proven by the policy matrix.
This commit is contained in:
@@ -192,17 +192,19 @@ var ErrRefusedUnderStrictPQ = errors.New("operation refused on strict-PQ chain p
|
||||
//
|
||||
// Returns nil — gate passes — when:
|
||||
//
|
||||
// - resolver is nil (no chain-profile resolver wired; documented
|
||||
// fail-OPEN for backward compatibility), OR
|
||||
// - chainID is empty (caller did not assert a chain; treat as
|
||||
// "no chain context, no strict-PQ posture to enforce"), OR
|
||||
// - resolver is nil (no chain-profile resolver wired; dev-only
|
||||
// default — production MUST wire a resolver), OR
|
||||
// - resolver returns a non-strict-PQ profile.
|
||||
//
|
||||
// Empty chainID is NOT a pass condition: it is forwarded to the
|
||||
// resolver, which maps "" to ProfileStrictPQ (fail-CLOSED), so a
|
||||
// caller cannot bypass the gate by omitting the chain context.
|
||||
//
|
||||
// Returns ErrRefusedUnderStrictPQ wrapped with op + chainID when
|
||||
// the chain is on ProfileStrictPQ. The wrapped message MUST stay
|
||||
// stable so audit-log parsing downstream does not break.
|
||||
func RefuseUnderStrictPQ(chainID, op string, resolver ChainProfileResolver) error {
|
||||
if resolver == nil || chainID == "" {
|
||||
if resolver == nil {
|
||||
return nil
|
||||
}
|
||||
if resolver.ResolveChainProfile(chainID) != ProfileStrictPQ {
|
||||
|
||||
@@ -66,6 +66,14 @@ func TestRefuseUnderStrictPQ_PolicyMatrix(t *testing.T) {
|
||||
"lux-testnet": ProfileLegacyCompat,
|
||||
})
|
||||
|
||||
// Production posture (what mpcd wires): def=ProfileStrictPQ, so any
|
||||
// chain not on the legacy allow-list — INCLUDING empty/unknown —
|
||||
// DEFAULT-REFUSES. This proves the fail-CLOSED behavior: a caller
|
||||
// cannot bypass the gate by omitting or faking the chain context.
|
||||
strictDefaultResolver := NewStaticChainProfileResolver(ProfileStrictPQ, map[string]Profile{
|
||||
"lux-testnet": ProfileLegacyCompat,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
chainID string
|
||||
@@ -102,6 +110,25 @@ func TestRefuseUnderStrictPQ_PolicyMatrix(t *testing.T) {
|
||||
resolver: strictResolver,
|
||||
wantError: false,
|
||||
},
|
||||
// Fail-CLOSED proofs (production posture, def=ProfileStrictPQ).
|
||||
{
|
||||
name: "empty chainID REFUSED on strict-default node",
|
||||
chainID: "",
|
||||
resolver: strictDefaultResolver,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "unknown chain REFUSED on strict-default node",
|
||||
chainID: "some-other-chain",
|
||||
resolver: strictDefaultResolver,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "legacy allow-listed chain passes on strict-default node",
|
||||
chainID: "lux-testnet",
|
||||
resolver: strictDefaultResolver,
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
Reference in New Issue
Block a user