Wallet Lifecycle Security Model
Purpose
This document formalises the security lifecycle of a SIROS ID wallet unit from creation through active use to deactivation. While no explicit state machine exists in the codebase, the lifecycle phases are enforced through a combination of WebAuthn registration flows, cryptographic key hierarchies, and data deletion procedures.
Lifecycle Overview
Phase 1: Creation (Provisioning)
Trigger: User initiates registration via WebAuthn passkey signup.
Backend (go-wallet-backend)
BeginRegistration validates tenant, generates UserID (UUID), encodes tenant-scoped userHandle (v1 binary: 1-byte version + 8-byte SHA-256 tenant hash + 16-byte UUID), stores challenge with 5-minute TTL.
FinishRegistration validates challenge (single-use), verifies attestation, checks AAGUID against configurable blacklist, creates user record with WalletType: "client", creates UserTenantMembership, issues JWT session token.
Frontend (wallet-frontend)
- Browser calls
navigator.credentials.create() with PRF extension and a fresh 32-byte prfSalt.
keystore.initPrf() performs the cryptographic initialisation:
- Generates AES-256-GCM main encryption key
- Generates ECDH ephemeral keypair for asymmetric key encapsulation
- Derives PRF wrapping key via HKDF-SHA256 from authenticator PRF output
- Wraps ECDH keypair with PRF-derived AES-KW key
- Encrypts empty initial
WalletStateContainer (schema v3) as JWE
- Result:
EncryptedContainer { mainKey, prfKeys[], jwe }
Security Properties at Creation
| Property | Mechanism |
|---|
| Passkey with discoverable credential | residentKey: required in WebAuthn options |
| User verification required | userVerification: required |
| Client-held keys only | No private key material reaches the backend |
| PRF output never transmitted | Used locally for HKDF derivation only |
| Tenant isolation | SHA-256 tenant hash embedded in userHandle |
| Attestation validation | AAGUID blacklist, configurable attestation preference |
Phase 2: Authentication (Session Establishment)
Trigger: User initiates login via WebAuthn discoverable credentials.
Flow
- Backend creates discoverable login challenge (no
allowCredentials), 5-minute TTL.
- User authenticates with passkey (user verification required).
- Backend validates assertion, decodes userHandle, verifies tenant hash, updates credential sign count (clone detection).
- If OIDC gate with
bind_identity: validates enterprise identity binding.
- Backend issues JWT (HS256) with claims:
user_id, tenant_id, jti, exp.
- Frontend calls
keystore.unlockPrf():
- Derives PRF key from WebAuthn PRF output
- Decapsulates main key: ECDH + AES-KW (v2) or AES-KW direct (v1)
- Decrypts JWE container to verify successful unlock
- Auto-upgrades v1 → v2 format if needed (re-wraps, pushes updated
privateData)
- Opens WebSocket for keystoreService signing channel.
Session Token Lifecycle
| Aspect | Mechanism |
|---|
| Token format | JWT (HS256) with user_id, tenant_id, jti, exp, iss, aud |
| Expiry | Configurable (expiry_hours); refresh via rotated refresh token |
| Revocation | In-memory JTI blacklist with periodic cleanup |
| Concurrency | ETag-based optimistic locking on privateData |
| Cross-tab coordination | globalTabId in localStorage, tabId in sessionStorage |
| Auto-refresh | Frontend withTokenRefresh() retries on 401 with refreshed token |
Phase 3: Management (Active Operations)
Key Operations
All credential key generation happens entirely in the browser via WebCrypto:
- Generate keypair:
crypto.subtle.generateKey("ECDSA", "") creates per-credential signing key.
- Create DID: Compressed public key → multicodec prefix
0x1200 → did:key identifier.
- Store: Private key stored as JWK in
WalletStateContainer (v3 schema), encrypted within JWE container.
- Sign: Backend requests signatures via WebSocket → frontend signs with
jose.SignJWT using imported private key.
Credential Operations
| Operation | Backend | Frontend |
|---|
| Issue | CredentialService.Store — tenant-scoped, holder DID-indexed | Event sourced: addNewCredentialEvent appended to container |
| Present | KeystoreService.SignJwtPresentation via WebSocket | keystore.signJwtPresentation() — signs VP JWT with credential key |
| Attest | WalletProviderService.GenerateKeyAttestation — signs keyattestation+jwt with server ECDSA key + x5c chain (15s TTL) | Provides public keys for attestation |
| Delete | Removes VerifiableCredential record | addDeleteCredentialEvent appended; state refolded |
Data Integrity
- Event sourcing:
WalletStateContainer.events[] with parentHash chain for integrity
- Optimistic concurrency:
If-Match ETag on all privateData updates; 412 on conflict → re-sync and merge
- Forward secrecy: Each
privateData update re-encrypts with new ephemeral ECDH keypair
- Schema migration: Automatic v0→v1→v2→v3 migration chain for legacy wallets
Passkey Management
| Operation | Constraint |
|---|
| Add passkey | Generates new PRF salt, wraps main key with new PRF-derived key |
| Delete passkey | Backend enforces minimum 1 credential (ErrLastWebAuthnCredential) |
| Rename passkey | Metadata-only update |
Phase 4: Deactivation (Destruction)
Session Close (Logout)
keystore.close() dispatches CloseSessionTabLocal event, zeroes in-memory key material.
- Clears
sessionStorage (appToken, userHandle, tabId).
- Token expiry enforced server-side; forced revocation via JTI blacklist.
Account Deletion
- Retrieves all
UserTenantMembership records for user.
- Per-tenant cleanup: deletes all
VerifiableCredential and VerifiablePresentation records for holder DID.
- Removes all
UserTenantMembership records.
- Deletes
User record (cascades: WebAuthn credentials, privateData, enterprise identities).
Security Properties at Deletion
| Property | Mechanism |
|---|
| Server-side credential material destroyed | Per-tenant credential/presentation deletion + user record cascade |
| Encrypted container destroyed | privateData deleted with user record |
| Main key unrecoverable | Key hierarchy rooted in authenticator PRF — no server-side copy |
| Client-side cleanup | Browser localStorage/IndexedDB remains until cleared by user or browser |
Component Involvement
| Phase | go-wallet-backend | wallet-frontend |
|---|
| Creation | WebAuthnService.{Begin,Finish}Registration, UserTenantMembership | keystore.initPrf(), navigator.credentials.create() |
| Authentication | WebAuthnService.{Begin,Finish}Login, JWT issuance, tenant hash validation | keystore.unlockPrf(), PRF evaluation, session storage |
| Management | CredentialService, KeystoreService (WebSocket proxy), WalletProviderService | crypto.subtle.generateKey, event sourcing, SignJWT |
| Deactivation | UserService.DeleteUser, TokenBlacklist.Add | keystore.close(), storage clear |