Skip to main content

Wallet Lifecycle Security Model

PropertyValue
ControlsSID-AUTH-01, SID-AUTH-02, SID-TRANS-02, SID-TRANS-03, SID-HARD-04
EUDI RequirementsCS-I.3-WUS (wallet unit security), CS-I.3-Prov (provisioning), CS-I.6-Valid (validation)
ISO 27001A.5.15, A.5.16, A.5.17, A.8.5

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)

  1. 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.
  2. 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)

  1. Browser calls navigator.credentials.create() with PRF extension and a fresh 32-byte prfSalt.
  2. 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

PropertyMechanism
Passkey with discoverable credentialresidentKey: required in WebAuthn options
User verification requireduserVerification: required
Client-held keys onlyNo private key material reaches the backend
PRF output never transmittedUsed locally for HKDF derivation only
Tenant isolationSHA-256 tenant hash embedded in userHandle
Attestation validationAAGUID blacklist, configurable attestation preference

Phase 2: Authentication (Session Establishment)

Trigger: User initiates login via WebAuthn discoverable credentials.

Flow

  1. Backend creates discoverable login challenge (no allowCredentials), 5-minute TTL.
  2. User authenticates with passkey (user verification required).
  3. Backend validates assertion, decodes userHandle, verifies tenant hash, updates credential sign count (clone detection).
  4. If OIDC gate with bind_identity: validates enterprise identity binding.
  5. Backend issues JWT (HS256) with claims: user_id, tenant_id, jti, exp.
  6. 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)
  7. Opens WebSocket for keystoreService signing channel.

Session Token Lifecycle

AspectMechanism
Token formatJWT (HS256) with user_id, tenant_id, jti, exp, iss, aud
ExpiryConfigurable (expiry_hours); refresh via rotated refresh token
RevocationIn-memory JTI blacklist with periodic cleanup
ConcurrencyETag-based optimistic locking on privateData
Cross-tab coordinationglobalTabId in localStorage, tabId in sessionStorage
Auto-refreshFrontend 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:

  1. Generate keypair: crypto.subtle.generateKey("ECDSA", "") creates per-credential signing key.
  2. Create DID: Compressed public key → multicodec prefix 0x1200did:key identifier.
  3. Store: Private key stored as JWK in WalletStateContainer (v3 schema), encrypted within JWE container.
  4. Sign: Backend requests signatures via WebSocket → frontend signs with jose.SignJWT using imported private key.

Credential Operations

OperationBackendFrontend
IssueCredentialService.Store — tenant-scoped, holder DID-indexedEvent sourced: addNewCredentialEvent appended to container
PresentKeystoreService.SignJwtPresentation via WebSocketkeystore.signJwtPresentation() — signs VP JWT with credential key
AttestWalletProviderService.GenerateKeyAttestation — signs keyattestation+jwt with server ECDSA key + x5c chain (15s TTL)Provides public keys for attestation
DeleteRemoves VerifiableCredential recordaddDeleteCredentialEvent 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

OperationConstraint
Add passkeyGenerates new PRF salt, wraps main key with new PRF-derived key
Delete passkeyBackend enforces minimum 1 credential (ErrLastWebAuthnCredential)
Rename passkeyMetadata-only update

Phase 4: Deactivation (Destruction)

Session Close (Logout)

  1. keystore.close() dispatches CloseSessionTabLocal event, zeroes in-memory key material.
  2. Clears sessionStorage (appToken, userHandle, tabId).
  3. Token expiry enforced server-side; forced revocation via JTI blacklist.

Account Deletion

  1. Retrieves all UserTenantMembership records for user.
  2. Per-tenant cleanup: deletes all VerifiableCredential and VerifiablePresentation records for holder DID.
  3. Removes all UserTenantMembership records.
  4. Deletes User record (cascades: WebAuthn credentials, privateData, enterprise identities).

Security Properties at Deletion

PropertyMechanism
Server-side credential material destroyedPer-tenant credential/presentation deletion + user record cascade
Encrypted container destroyedprivateData deleted with user record
Main key unrecoverableKey hierarchy rooted in authenticator PRF — no server-side copy
Client-side cleanupBrowser localStorage/IndexedDB remains until cleared by user or browser

Component Involvement

Phasego-wallet-backendwallet-frontend
CreationWebAuthnService.{Begin,Finish}Registration, UserTenantMembershipkeystore.initPrf(), navigator.credentials.create()
AuthenticationWebAuthnService.{Begin,Finish}Login, JWT issuance, tenant hash validationkeystore.unlockPrf(), PRF evaluation, session storage
ManagementCredentialService, KeystoreService (WebSocket proxy), WalletProviderServicecrypto.subtle.generateKey, event sourcing, SignJWT
DeactivationUserService.DeleteUser, TokenBlacklist.Addkeystore.close(), storage clear