Skip to content

Shield Core (crate)

ohshii-shield-core is the Rust crate behind the OhShii Shield, the in-canister firewall that protects the expensive public update methods of the launcher backend, the OHSHII governance canister (ONS), every campaign governance canister (SONS) and the OhShii Locker backend. It is a plain library: any Internet Computer canister facing the same threat model can use it. It is licensed under AGPL-3.0-or-later, like the rest of OhShii.

This page describes what the crate does, what it guarantees, and the client-side contract a frontend or script follows when it calls a Shield-protected method. How the Shield is configured on the OhShii canisters, and its pre-authorisation cache, are described in Shield Pre-Auth Cache.

On the Internet Computer the canister pays for the calls it receives (the reverse-gas model), while a caller can create new identities at no cost. A per-principal limit alone is therefore not a ceiling: an attacker simply uses more principals. The Shield bounds the cycles that untrusted callers can make a canister spend, without asking honest users to pay anything extra in normal operation.

A protected method calls the crate’s check() function before doing any expensive work. The check runs a fixed sequence of layers and stops at the first one that refuses:

LayerWhat it does
Trusted callersCalls from the canister itself and from the callers it is configured to trust (for example its governance canister) are not rate-limited.
Cycle floorRefuses expensive calls when the canister’s liquid cycle balance is too close to its freezing reserve, so the canister keeps enough cycles to run its own trusted paths. The same check can run in inspect_message, so most refusals cost the canister nothing.
Pre-authorisation cacheA recent positive result for the caller can skip the global rate-limit step.
Global rate limitOne counter per method shared by all callers. This is the real ceiling on accepted load, because it cannot be evaded by creating more identities.
Per-caller rate limitPer-principal friction against a single noisy caller.
Defcon stateEach method moves into a stricter “Defcon” state under sustained pressure and back out after a hold period, so it does not flap between states.
Identity verificationMethods that require it ask the canister’s identity authority whether the caller is verified (the answer is cached for a few minutes).
Proof of workOnly while a method is in Defcon: the caller must attach a fresh proof of work (see below).

Most protected methods also have their own economic gate: a payment or a recorded contribution that the caller must already have made. That is why most of them do not require identity verification.

  • The Shield runs inside the canister, after a boundary node has admitted the message. It is not a network-level (volumetric) DDoS defence; that is handled by the Internet Computer’s boundary layer. inspect_message is used only as a cycle-saving pre-filter, never as the security check itself: every check is repeated in the method body.
  • The Shield defends against untrusted public callers. Trusted callers are, by design, the recovery path and are not limited by it.

A client (the OhShii frontends, or any script) that calls a Shield-protected method relies on the following. These values are stable across crate versions; changing any of them would be a breaking change for every client and would require a new major version.

Proof of work, required only while the method is in Defcon

  1. Fetch the challenge from the canister you are about to call: get_shield_pow_challenge(endpoint_id) -> (seed: blob, difficulty_bits: nat32, params: Argon2Params). The seed is valid for the current 5-minute window, so refresh it shortly before the window ends (the OhShii frontends refresh at about 270 seconds).
  2. Find an 8-byte nonce such that hash = argon2id(input = caller_principal_bytes || nonce_le8, salt = seed, params) has at least difficulty_bits leading zero bits. The minimum difficulty is 4 bits.
  3. Pass the 40-byte proof nonce_le8 || hash as the method’s optional pow_solution argument.
  • Argon2id parameters: m_cost = 512 KiB, t_cost = 1, p_cost = 1, output_len = 32, algorithm Argon2id, version 0x13. They are also returned by the challenge query.
  • A proof is bound to the caller, the method, the 5-minute window and the canister that issued the challenge: a proof made for one canister is refused by every other canister.
  • Each proof can be used once. Under Defcon every admitted call needs a fresh proof; a proof is a per-call cost, not a ticket for the window.
  • A proof built on an expired challenge is refused with the ordinary proof-of-work message, and the client simply solves a new one.

Refusal messages

Every refusal is returned as an error text with a stable shape, so a client can classify it and show the right prompt (wait and retry, solve a proof of work, or verify identity). A refusal that asks for identity verification always contains the substring identity verification required (case-insensitive). When the method accepts any provider the identity authority accepts, the message names no provider, so it stays correct when providers are added or retired.

ModulePurpose
powArgon2id verification, challenge-seed rotation, the Argon2Params Candid record
rate_limitGlobal and per-caller rate limits, plus read-only checks usable from inspect_message
floorThe canister-level cycle floor shared by the method body and inspect_message
triggerThe per-method Defcon state machine with anti-flap holds
verifyA cache wrapper around the canister’s identity-verification call
configShieldConfig, VerificationLevel, Fallback
decisionThe ShieldDecision enum and its user-facing error texts
gatewayThe VerificationGateway trait a consuming canister implements
  • Layered rate limiting: cycle floor, global cap, per-caller friction and the method’s own economic gate, sized for a DAO whose parameter changes take days.
  • Per-principal limits are friction only: new identities cost nothing, so the global cap is the ceiling.
  • Fail closed without hiding an outage: when a dependency (such as the identity authority) is unreachable, the call is refused with a message that says so, not with a misleading user error.