Shield Core (crate)
A reusable in-canister firewall for the Internet Computer.
This crate is the portable extraction of the OhShii Shield — the
layered defence layer first introduced to protect four high-value
endpoints on the OhShii Launcher backend (prepare_token_creation,
prepare_ico_creation, request_refund, claim_tokens). It now wraps
39 endpoints across four canisters — the launcher backend,
ohshii_governance (ONS), sons_governance (SONS), and the OhShii Locker
backend — and applies to any third-party IC canister facing the same
threat model. Some access-controlled canisters (pool_manager, the
dao_storage / lock_storage storage canisters, the locker
proxy / strongbox) are deliberately not Shield-wrapped — they rely
on caller guards + inspect_message. The full, source-verified
protected-surface inventory (which method, which endpoint_id, which
verification level) lives in the OhShii Shield documentation.
What it gives you
Section titled “What it gives you”A single check() function you call at the top of any expensive
#[update] method:
use ohshii_shield_core::{check, ShieldConfig, ShieldDecision};
#[ic_cdk::update]async fn my_expensive_endpoint( pow_solution: Option<Vec<u8>>,) -> Result<(), String> { let caller = ic_cdk::api::msg_caller(); let decision = check( &MY_ENDPOINT_SHIELD, // ShieldConfig const caller, &MyGateway, // your VerificationGateway impl None, // optional campaign_id pow_solution.as_deref(), ) .await; if let Some(err) = decision.to_user_error() { return Err(err); } // ... expensive work ... Ok(())}The pipeline runs in this exact order: T → F → P → B → E → C → A → D
- T — trusted bypass for admin / self-canister / canister-authority calls.
- F — cycle-reserve floor (last-line DoS defence).
- P — pre-auth cache lookup; fresh positive hits skip only B.
- B — global cross-caller rate limit (Sybil-proof).
- E — per-caller rate limit (friction, two-threshold hygiene).
- C — Defcon state machine observation (anti-flap holds).
- A — identity verification dispatch via the consumer-provided
VerificationGateway(cached 5 min). - D — Argon2id PoW recompute (only under Defcon).
Threat model
Section titled “Threat model”The Shield defends against Sybil-driven cycle drain on the IC’s
reverse-gas model, where the canister pays cycles per call and the
attacker pays nothing per identity (dfx identity new is free; II
anchors cost ~0.0002 ICP and are scriptable). Per-principal counters
alone are insufficient, so this crate implements a four-layer defence
model:
- cycle-reserve floor — refuse high-value calls before the balance nears the freezing threshold.
- global cross-caller cap — a single Sybil-proof counter (one
(window, count)tuple per endpoint, no per-key allocation). This is the real ceiling on accepted burn, sized so the highest accepted steady-state rate is non-urgent. - per-caller friction — per-principal counters. Zero-cost to evade with fresh identities, so this is friction, not a ceiling.
- economic gate — the endpoint’s own pre-existing economic
commitment: an ICRC-2 payment from a real-balance account, or a
contribution-of-record row in storage. This is why most protected
endpoints carry
VerificationLevel::None— the cost of entry is already paid upstream, so identity gating would only lock out legitimate Guest-tier callers.
The memory-hard PoW (Step D) is a fifth, Defcon-only friction layer, not one of the four economic layers — see “What the PoW layer is — and is not” below for why its parameters are deliberately small.
Scope and non-goals
Section titled “Scope and non-goals”- The Shield is an in-canister defence that begins after a boundary
node has admitted the message. It addresses the cheap-fresh-principal
cycle-drain vector that reaches the canister; it is not a
network-layer (volumetric) DDoS defence — that is handled upstream by
the IC boundary-node layer. (
inspect_messageruns on a single replica pre-consensus and can be bypassed by a boundary node forwarding the message anyway, so it is a cycle-saving pre-filter, never a gate.) - The Shield defends against untrusted public callers. It does not
defend against a compromised trusted key (an admin allowlist entry
or the governance-set guardian). Trusted keys are, by design, the
recovery path and bypass every layer; the centralized kill switch even
lets one trusted key put the trusted core into
TrustedOnly. Protecting those keys (multisig / HSM / rotation) is an operational concern outside this crate — see the “Kill-switch trust boundary” note in the Shield Pre-Auth Cache document.
What’s in the box
Section titled “What’s in the box”| Module | Purpose |
|---|---|
pow | Argon2id verification, challenge-seed rotation, Argon2Params Candid record |
rate_limit | Global + per-caller layered defence, two-threshold hygiene |
trigger | Per-endpoint Defcon state machine with anti-flap holds |
verify | Generic 5-minute cache wrapper around a consumer-provided ICC |
config | ShieldConfig, VerificationLevel, Fallback |
decision | ShieldDecision enum + to_user_error() string contracts |
gateway | VerificationGateway trait you implement for your canister |
Wire contracts (do NOT change these)
Section titled “Wire contracts (do NOT change these)”A consumer frontend that has shipped against the OhShii Shield wire format MUST keep working byte-for-byte across crate upgrades. The following are stable forever:
- Argon2id parameters:
m_cost = 512 KiB,t_cost = 1,p_cost = 1,output_len = 32, algorithmArgon2id, version0x13. - Challenge window: 5 minutes (single bucket — frontend must refresh its seed at ~270 s).
- Minimum difficulty: 4 leading zero bits.
- PoW payload: exactly 40 bytes =
nonce_le8 || hash32. - Salt derivation:
sha256("ohshii-shield-pow-v1" || caller_bytes || endpoint_id || (now_ns / window_ns).to_le_bytes()). ShieldDecision::to_user_error()strings: byte-identical to the values the frontend’sSHIELD_ERROR_MARKERSregex contract expects.
Tightening the difficulty or changing any of the above is a breaking change for every consumer; treat as a major version bump.
What the PoW layer is — and is not
Section titled “What the PoW layer is — and is not”The Argon2id PoW (Step D) is a Defcon-only, memory-hard, per-call friction tax — not the economic gate of the four-layer model. Two consequences are worth stating so the small parameters are not misread:
- The 4-bit difficulty and 512 KiB
m_costare deliberately low.m_costis bounded by the wasm32 verification budget (~250 ms / ~1.5 B cycles, well under the per-message instruction ceiling), and PoW gets its teeth not from the bit-count but from the single-use nonce ledger (try_consume_pow_nonce, overridden by every consumer): each valid(nonce, hash)is consumed once per 5-minute window, so under Defcon every admitted non-trusted call must carry a fresh solve. It is a per-call cost, not a 5-minute ticket. ~4 bits is ~16 expected hashes — modest by intent; the economic ceiling is the global cap (Layer 2), not the PoW. - Difficulty is a compile-time constant (
MIN_DIFFICULTY_BITS) with no runtime / governance / Defcon knob. Step D is binary (PoW on under Defcon, off otherwise), not an intensity dial that scales with load. Raising the difficulty is a code change requiring a coordinated major-version redeploy of both vendored crate copies — intentionally rare. (The frontend already solves any advertised difficulty; only the backend constant is frozen.)
Design principles
Section titled “Design principles”The crate’s defensive posture rests on these IC canister security properties:
- Four-layer rate-limit defence — cycle reserve floor + global cross-caller cap + per-caller friction + economic gate hook, sized for DAO-timescale (slow-governance) threat models.
- Anonymous-principal rejection + the Sybil-bypass caveat: per-principal limits are zero-cost to evade with fresh identities, so the global cap is the real ceiling.
- Bounded vs unbounded inter-canister wait + the
msg_caller()-before-await binding pitfall. - Dependency-outage UX — fail closed without masking an upstream outage as a user error.
The full operational guide and the frontend/backend wire contract live in the OhShii Shield documentation (docs/).
License
Section titled “License”Dual-licensed under MIT or Apache-2.0, at your option.