Skip to content

Security model

Sigil is built around one principle: private keys never leave the trust boundary they were generated in. For client-mode wallets the boundary is the user’s browser iframe. For TEE-mode wallets it’s the hardware-attested workload. Every layer — authentication, transport, storage, recovery — is designed to enforce that.

Trust boundaries, in one diagram

┌─────────────────────────────────────────────────────────────┐
│ Your bundle ── publishable key only │
└──────┬──────────────────────────────────────────────────────┘
│ postMessage (origin-checked, per-org allowlist)
┌─────────────────────────────────────────────────────────────┐
│ Sigil iframe ── client-mode key reconstruction │
│ wallet.sigilkeys.com (strict CSP, no third-party scripts) │
└──────┬──────────────────────────────────────────────────────┘
│ HTTPS + publishable key + end-user JWT
┌─────────────────────────────────────────────────────────────┐
│ Sigil API ── never holds plaintext key material │
│ api.sigilkeys.com │
└──────┬─────────────────────┬────────────────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌────────────────────────────────────────┐
│ Cloud KMS │ │ TEE workload (TEE-mode only) │
│ AAD-bound │ │ Confidential Space + AMD SEV-SNP │
│ per wallet │ │ Sealed share decryption, sign, wipe │
└──────────────┘ └────────────────────────────────────────┘

To move a single wei, an attacker has to break at least one of:

  • Client mode: the SSS threshold (steal two of three shares) AND the user’s email (to receive the recovery OTP).
  • TEE mode: the attestation chain (run a different image and convince Google’s TEE attestation issuer it’s the real one) AND the AMD SEV-SNP hardware boundary.

Multi-tenancy

Each company is one organization. Every domain row carries an organization_id and the data layer filters on it at the repository level — not in handlers, not in middleware. The KMS AAD binds every encrypt/decrypt call to (organization_id, wallet_id), so even a leaked ciphertext from one org can’t be replayed under another.

Sigil keeps two identity universes that never mix:

  • Portal users: developers managing the integration. Owned by Ory Kratos (passwordless email-OTP). Cookie session scoped to platform.sigilkeys.com.
  • End-user identities: the people whose wallets you embed. Created lazily on first wallet auth. Authenticated by Sigil-hosted email-OTP or your OIDC.

A portal user is not an end user, and vice versa. Logging in to the portal does nothing for wallet flows, and signing into a wallet does nothing for the portal.

Origin validation

Each organization declares an allowed origins list in the portal (Settings → Allowed origins). It’s enforced in two places:

  1. CORS — the backend only adds Access-Control-Allow-Origin for origins in the allowlist.
  2. Wallet iframe — the iframe validates event.origin on every incoming postMessage and drops anything not in the list. Replies go only to the event.source that issued the request.

Publishable keys are safe to ship in browser bundles because of this — origin validation ensures they cannot be used from unauthorized domains. There is no wildcard origin.

API keys

PropertyPublishable key (pk_live_…)Secret key (sk_live_…)
Intended useBrowser / mobile SDKServer-to-server
Exposed to end usersYes (CORS-gated)Never
IP allowlistNoYes (CIDRs)
Scopesn/afull (read + write) or read
StorageSHA-256 hashSHA-256 hash
CleartextVisible on creation onlyVisible on creation only

Secret keys with an IP allowlist reject any request from a source IP outside the listed CIDRs.

JWT sessions

Sigil authenticates end users with short-lived JSON Web Tokens.

  • TTL is 1 hour. Tokens are not refreshable — when a session expires the user must re-authenticate.
  • Sigil-hosted auth: tokens signed with HS256, verified server-side only.
  • OIDC (bring-your-own provider): tokens validated against your IdP’s JWKS, so Sigil never handles your users’ credentials.
  • Revocation: POST /v1/wallets/me/logout immediately invalidates the token via a server-side blacklist. The token cannot be reused even if it has not expired.

Rate limiting

All sensitive endpoints enforce per-entity rate limits. When a limit is exceeded the API returns 429 Too Many Requests.

EndpointLimit
Login OTP verification5 attempts per code; 5 codes per email per window
Recovery OTP verification5 attempts per recovery process
Provider share retrieval3-burst per user, sustained ~1 req / 5 s
Server-to-server API30 req/s sustained, 60 burst per API key
Smart-account sponsorship€10 / wallet / calendar month, EUR-equivalent

Recovery protections

For client-mode wallets specifically — TEE wallets have no recovery flow.

  • Expiry: a recovery process expires after 15 minutes.
  • OTP lockout: 5 attempts per process; lockout after that.
  • Email notification: the user receives an email on recovery completion, so an unauthorized attempt is visible.
  • Polynomial rotation: every successful recovery rotates the Shamir polynomial; old shares are invalidated.

Wallet archival

If a wallet is compromised, archive it via the S2S API:

Terminal window
curl -X POST https://api.sigilkeys.com/v1/s2s/wallets/{walletID}/archive \
-H "Authorization: Bearer sk_live_..."

Once archived:

  • Client mode: the iframe rejects any operation targeting the wallet.
  • TEE mode: the TEE refuses to reconstruct the key.

Archival is the kill switch. It cannot be undone — provision a new wallet and migrate funds out-of-band.

Envelope encryption (KMS)

Every share Sigil persists is envelope-encrypted:

  1. A fresh 256-bit DEK is generated per write.
  2. The share is sealed with AES-GCM under the DEK.
  3. The DEK is wrapped by Cloud KMS using a per-platform KEK.
  4. Both the wrapped DEK and the AES-GCM ciphertext are stored.
  5. The DEK is wiped from memory.

Every Cloud KMS call passes additional_authenticated_data of the form:

organization:<org_id>:wallet:<wallet_id>

If a ciphertext from one wallet is replayed against another wallet’s id, KMS refuses to decrypt — the AAD doesn’t match. This neutralises the class of attacks where a database read leak combined with KMS access could be replayed under a different identity.

The local AES-GCM AAD on the share itself is wallet:<wallet_id>, so the same binding is enforced even before the KMS call.

For TEE-mode wallets, the KMS decrypt permission is granted exclusively to the attested workload via a Workload Identity Pool condition — Sigil operators cannot decrypt these shares even with the database in front of them. See TEE security model.

Sigil-managed recovery: separate KEK

When the recovery share is held by Sigil (Sigil-managed recovery mode), it is wrapped with a distinct KMS purpose:

organization:<org_id>:wallet:<wallet_id>:purpose:recovery

So the unwrap material for the provider share and the recovery share are distinct — a single key compromise can’t unwrap both.

Audit log

Every sensitive operation is recorded immutably:

  • Wallet creation and signing requests
  • Recovery initiation and completion (including failures)
  • API key creation, rotation, deletion
  • Server-to-server operations, including the key id used, source IP, and HTTP status
  • Smart-account provisioning + sponsorship spend

Audit entries are accessible through the Audit page in the portal and via GET /v1/orgs/{orgID}/audit.

What’s not in this section

  • The agent policy stack — see Policies.
  • Connector credential handling — see Connectors.