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:
zeekay
2026-06-27 11:38:14 -07:00
parent c4044388e0
commit 7606b46ee9
2 changed files with 34 additions and 5 deletions
+7 -5
View File
@@ -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 {
+27
View File
@@ -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 {