Home / Docs / Design documents / Risk Mandate

Rendered from docs/design/riskmandate-aws-cognito-architecture.md, which is the source of truth and the markdown twin of this page. Rendered at site v0.1.2.

Risk Mandate — AWS Cognito Client-Side Architecture

2026-10-05 · Dinis Cruz

Summary

Cognito handles login (Google, other social or OIDC/SAML providers, and Cognito-managed username/password or passkeys). A Cognito identity pool swaps the login token for short-lived AWS credentials in the browser, and the browser talks to S3 directly, limited to its own prefix. SGit vaults are already encrypted client-side, so S3 only ever holds ciphertext.

The one new piece is a per-user keyring: an encrypted file in the user's S3 prefix listing the vault keys that user can open, like a password manager's vault. The keyring is unlocked by a key the browser derives from the user's passkey (WebAuthn PRF). That key never exists in Cognito, KMS or any AWS service.

Answer to the core question: nothing stored inside Cognito or KMS can be kept from someone holding the AWS admin account. But the design doesn't need that. If the key that opens the keyring is derived on the user's authenticator, a full compromise of the Cognito and AWS admin accounts yields ciphertext only. This keeps SGit's existing property: server compromise causes no data disclosure.

Update: the primary build will be all-GCP (Identity Platform + Cloud Storage for Firebase, one project per client) so each deployment lives in one cloud and can be created and destroyed as a unit. This Cognito design remains the AWS variant for AWS-native clients. See Risk Mandate — GCP Key Vault Architecture & Password Manager MVP.

Why secrets cannot live in Cognito or KMS

Conclusion: the authority to decrypt must come from something the AWS account cannot reach — the user's authenticator, or a passphrase only the user knows.

Architecture

LayerComponentRoleWhat a compromised admin gets
Web appStatic site on GitHub Pages (outside AWS)All crypto and UI run hereNothing, if the code host is separate
LoginCognito user pool: Google federation, other IdPs, native username/password, passkeysProves who the user isAbility to impersonate users and alter config
AWS accessCognito identity pool, authenticated role with policy variablesShort-lived credentials scoped to vaults/${cognito-identity.amazonaws.com:sub}/*Access to any user's prefix
StorageS3: user keyring + SGit vault objectsHolds ciphertext onlyCiphertext; ability to delete or roll back
KeysPasskey PRF on the user's device, synced via iCloud Keychain or personal Google Password ManagerDerives the key that unwraps the keyringNothing

Login options

Login and key unlock are deliberately separate. Login decides which S3 prefix you may touch; the passkey PRF decides whether you can read what's there. An admin can fake the first, never the second.

Keyring design

Path: vaults/{identityId}/keyring.json (versioned).

Contents, conceptually:

Unlock flow:

  1. User signs in via Cognito; browser gets identity-pool credentials.
  2. Browser downloads keyring.json.
  3. Browser runs navigator.credentials.get with the PRF extension and the salt stored in the keyring; the authenticator returns a 32-byte secret.
  4. HKDF turns it into a wrapping key; browser unwraps the KEK, decrypts the body, holds vault keys in memory only.
  5. SGit reads and writes vault objects directly in S3.

Adding a device: sign in, unlock with an existing passkey or the recovery code, register the new passkey, add a new wrapped-KEK entry. Losing every passkey and the recovery code means the data is gone; say so in the terms.

Sharing a vault between users

Each user has an X25519 key pair; the private key lives in their keyring, the public key in a readable directory (directory/{identityId}.pub). To share a vault, the owner encrypts the vault key to the recipient's public key and drops it in the recipient's inbox prefix; the recipient's browser moves it into their keyring on next unlock.

Risk: a compromised admin could replace a public key in the directory and receive the next share. Mitigate with key fingerprints users can compare, or signed directory entries, before any sensitive sharing. Revocation means rotating the vault key and re-sharing to remaining members.

What a full admin compromise can and cannot do

AttackResultMitigation
Read S3Ciphertext only—
Read Cognito usersEmails, names, login metadataKeep the user pool minimal; accept metadata exposure
Impersonate a userGets their ciphertext, cannot pass the PRF stepPasskey bound to the riskmandate.ai origin
Point Cognito at a phishing sitePRF output is bound to the site's origin, so a phishing site cannot get itCode host outside AWS
Delete or encrypt-for-ransom S3 dataAvailability lossS3 versioning + Object Lock, replication to a separate AWS account, SGit clones on devices
Roll back a vault to an older versionStale dataSGit commit hashes; client remembers last-seen head
Swap a public key in the directoryIntercepts future sharesFingerprint check or signed directory
Serve malicious JavaScriptFull compromise of any user who loads itCode hosting outside the AWS account, protected separately; SRI on pinned libraries; reproducible builds

The last row is the real boundary: client-side crypto is only as trustworthy as the code delivered to the browser. Keeping the site on GitHub Pages, outside the AWS account, means an AWS or Cognito compromise does not reach it.

Compared with the Google designs

Cognito + S3Identity Platform + Cloud StorageWorkspace
Terms issue for multi-customer useNoneNoneNeeds Google's written agreement
Native passkeysYesLimitedYes (Google account)
Multi-tenancyPool per customer or tenant attributesBuilt inTenant per customer
Browser-direct storageIdentity pool + S3Firebase Storage rulesDrive/GCS via OAuth
Mail, Drive, CalendarNoNoYes
FamiliarityHigh (already used)LowerMedium

Open questions and next steps