Status: CURRENT (Nitro Architecture) Last Updated: 2026-01-18 Supersedes: REST-based enrollment flow (EC2 model - deprecated) Reference: See NITRO-ENCLAVE-VAULT-ARCHITECTURE.md Section 5.6 for the complete enrollment flow
This document specifies the NATS-based enrollment flow for the Nitro Enclave architecture. Mobile apps communicate directly with the enclave via NATS messaging for attestation verification and credential creation.
Note: This replaces the old REST-based
/vault/enroll/*endpoints which were used in the EC2-per-user model. Those endpoints are deprecated and should not be used.
┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐
│ Mobile App │◄─NATS──►│ NATS Cluster│◄─NATS──►│ Nitro Enclave │
│ │ │ (JetStream) │ │ ┌───────────────────┐ │
│ Attestation │ │ │ │ │ Parent Process │ │
│ Verification│ │ ENROLLMENT │ │ │ (nats_client.go) │ │
│ │ │ Stream │ │ └─────────┬─────────┘ │
└─────────────┘ └─────────────┘ │ │ vsock │
│ ┌─────────▼─────────┐ │
│ │ Supervisor │ │
│ │ (NSM/KMS/DEK) │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Vault Manager │ │
│ │ (CEK, Credential) │ │
│ └───────────────────┘ │
└─────────────────────────┘
Key Components:
- Supervisor: Handles NSM attestation, KMS operations, DEK derivation
- Vault-Manager: Handles CEK keypair, Protean Credential operations
Purpose: Create DEK and initialize vault. Does NOT create the Protean Credential.
| Step | Action | Component |
|---|---|---|
| 1 | Decrypt PIN (ECIES with attestation key) | Supervisor |
| 2 | Generate random material (32 bytes) | Supervisor |
| 3 | KMS.Encrypt(material) → sealed_material | Supervisor → KMS |
| 4 | DEK = KDF(sealed_material, PIN) | Supervisor |
| 5 | Store sealed_material to S3 | Supervisor |
| 6 | Start vault-manager with DEK | Supervisor → Vault-Manager |
| 7 | Initialize SQLite (DEK-encrypted) | Vault-Manager |
| 8 | Generate CEK keypair | Vault-Manager |
| 9 | Generate UTK/LTK pairs | Vault-Manager |
| 10 | Return { status: "vault_ready", utks } |
→ App |
Returns: vault_ready + UTKs. No credential yet.
Purpose: Create the Protean Credential with user's password.
| Step | Action | Component |
|---|---|---|
| 1 | Receive encrypted password hash + UTK ID | From App |
| 2 | Decrypt password hash with LTK | Vault-Manager |
| 3 | Mark UTK as used | Vault-Manager |
| 4 | Generate identity keypair (Ed25519) | Vault-Manager |
| 5 | Generate vault master secret | Vault-Manager |
| 6 | Create Protean Credential struct | Vault-Manager |
| 7 | Encrypt credential with CEK | Vault-Manager |
| 8 | Store CEK, LTKs in SQLite | Vault-Manager |
| 9 | Sync SQLite to S3 | Vault-Manager |
| 10 | Return { status: "created", encrypted_credential, new_utks } |
→ App |
Returns: encrypted_credential + new UTKs.
The enrollment flow has three phases after NATS connection:
- Attestation - Verify enclave identity, get ephemeral public key
- PIN Setup - Create DEK, initialize vault, get UTKs
- Credential Creation - User creates credential password, Protean Credential generated
┌─────────────┐ ┌─────────────────┐
│ Mobile App │ │ Nitro Enclave │
└──────┬──────┘ └────────┬────────┘
│ │
│ 1. POST /nats/account (REST) │
│─────────────────────────────────────────► │
│ { owner_space, nats_url, jwt, seed } │
│◄─────────────────────────────────────────│
│ │
│ 2. Connect to NATS │
│─────────────────────────────────────────► │
│ │
═══════════════════════════════════════════════════════════════
PHASE 1: ATTESTATION
═══════════════════════════════════════════════════════════════
│ │
│ 3. Generate random nonce (32 bytes) │
│ │
│ 4. forVault.attestation │
│ { nonce } │
│─────────────────────────────────────────► │
│ │
│ 5. Supervisor:
│ NSM.GetAttestation()
│ (ephemeral pubkey + nonce)
│ │
│ 6. forApp.attestation.response │
│ { attestation_document, │
│ enclave_public_key } │
│◄─────────────────────────────────────────│
│ │
│ 7. VERIFY: │
│ • AWS Nitro signature valid? │
│ • PCRs match published values? │
│ • Nonce matches ours? │
│ • Extract enclave public key │
│ │
═══════════════════════════════════════════════════════════════
PHASE 2: PIN SETUP (DEK Creation + Vault Initialization)
═══════════════════════════════════════════════════════════════
│ │
│ 8. User creates PIN (4-8 digits) │
│ │
│ 9. Encrypt PIN to enclave pubkey (ECIES) │
│ │
│ 10. forVault.pin │
│ { type: "pin.setup", payload: {...} } │
│─────────────────────────────────────────► │
│ │
│ 11. Supervisor:
│ - Decrypt PIN
│ - Generate random material
│ - KMS.Encrypt(material) → sealed
│ - DEK = KDF(material, PIN)
│ - Store sealed_material
│ │
│ 12. Supervisor → Vault-Manager:
│ - Start with DEK
│ - Initialize SQLite (DEK-encrypted)
│ - Generate CEK keypair
│ - Generate UTK/LTK pairs
│ │
│ 13. forApp.pin.response │
│ { status: "vault_ready", │
│ utks: [...] } │
│◄─────────────────────────────────────────│
│ │
═══════════════════════════════════════════════════════════════
PHASE 3: CREDENTIAL CREATION (Protean Credential)
═══════════════════════════════════════════════════════════════
│ │
│ 14. User creates credential password │
│ (different from PIN!) │
│ │
│ 15. App hashes password (Argon2id) │
│ Encrypts with UTK │
│ │
│ 16. forVault.credential.create │
│ { encrypted_password_hash, │
│ utk_id } │
│─────────────────────────────────────────► │
│ │
│ 17. Vault-Manager:
│ - Decrypt with LTK
│ - Generate identity_keypair
│ - Generate master_secret
│ - Create Protean Credential:
│ {
│ identity_private_key,
│ identity_public_key,
│ vault_master_secret,
│ password_hash (Argon2id),
│ auth_salt,
│ crypto_keys: []
│ }
│ - Encrypt credential with CEK
│ - Store CEK, LTKs in SQLite
│ - Sync SQLite to S3
│ │
│ 18. forApp.credential.response │
│ { status: "created", │
│ encrypted_credential, │
│ new_utks: [...] } │
│◄─────────────────────────────────────────│
│ │
│ 19. Store encrypted_credential locally │
│ + UTKs for future operations │
│ │
═══════════════════════════════════════════════════════════════
PHASE 4: VERIFY ENROLLMENT
═══════════════════════════════════════════════════════════════
│ │
│ 20. Send test operation (e.g., get_info) │
│─────────────────────────────────────────► │
│ │
│ 21. Decrypt credential
│ Verify password hash
│ Return info
│ │
│ 22. Success! Enrollment verified │
│◄─────────────────────────────────────────│
│ │
| PIN | Credential Password | |
|---|---|---|
| Purpose | Unlock vault (DEK derivation) | Authorize operations |
| Format | 4-8 digits | User-chosen string |
| Hashing | KDF with KMS-sealed material | Argon2id (app-side) |
| Storage | Not stored (derived each time) | Hash in Protean Credential |
| When Used | Vault unlock, credential restore | Every vault operation |
- UTK (User Transaction Key): Public keys held by app for encrypting requests
- LTK (Ledger Transaction Key): Private keys held by vault for decrypting
- UTKs are single-use and rotated after each operation
- New UTKs returned with each response
┌─────────────────────────────────────────────────────────────────┐
│ Key Hierarchy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PIN (user's mind) │
│ + KMS-sealed material │
│ ──────────────────── │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ DEK │ Data Encryption Key │
│ │ (volatile) │ - Derived each time from PIN + sealed mat │
│ └──────┬──────┘ - Never stored, only in enclave memory │
│ │ - Encrypts SQLite database │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SQLite Database (DEK-encrypted) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ CEK Keypair │ │ LTK Store │ │ Vault Data │ │ │
│ │ │ (X25519) │ │ (per UTK) │ │ │ │ │
│ │ └──────┬──────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────┼───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Protean Credential (CEK-encrypted) │ │
│ │ - identity_private_key (Ed25519) │ │
│ │ - identity_public_key │ │
│ │ - vault_master_secret │ │
│ │ - password_hash (Argon2id) │ │
│ │ - crypto_keys[] │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Stored on App (encrypted blob) │
│ Backed up to Cloud Storage │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Points:
- DEK encrypts the SQLite database (where CEK and LTKs are stored)
- CEK encrypts the Protean Credential (which app stores locally)
- PIN is never stored - DEK is derived fresh each time
- Password hash is stored inside the Protean Credential for operation authorization
| Direction | Topic | Purpose |
|---|---|---|
| App → Enclave | OwnerSpace.{guid}.forVault.attestation |
Request attestation doc |
| Enclave → App | OwnerSpace.{guid}.forApp.attestation.response |
Attestation response |
| Direction | Topic | Purpose |
|---|---|---|
| App → Enclave | OwnerSpace.{guid}.forVault.pin |
PIN setup/unlock/change |
| Enclave → App | OwnerSpace.{guid}.forApp.pin.response |
PIN operation response |
| Direction | Topic | Purpose |
|---|---|---|
| App → Enclave | OwnerSpace.{guid}.forVault.credential.create |
Create Protean Credential |
| Enclave → App | OwnerSpace.{guid}.forApp.credential.response |
Credential response |
{
"id": "<uuid>",
"type": "attestation.request",
"nonce": "<base64-32-bytes>",
"timestamp": "<ISO8601>"
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique request ID for correlation |
type |
string | Yes | Must be "attestation.request" |
nonce |
string | Yes | 32-byte random nonce (base64) for freshness |
timestamp |
string | Yes | ISO8601 timestamp |
{
"id": "<uuid>",
"status": "success",
"attestation_document": "<base64-CBOR>",
"enclave_public_key": "<base64-X25519-32-bytes>",
"module_id": "<enclave-module-id>",
"timestamp": "<ISO8601>"
}| Field | Type | Description |
|---|---|---|
id |
string | Correlates to request ID |
status |
string | "success" or "error" |
attestation_document |
string | AWS Nitro attestation doc (CBOR, base64) |
enclave_public_key |
string | X25519 public key for ECIES encryption |
module_id |
string | Enclave module identifier |
timestamp |
string | ISO8601 timestamp |
{
"id": "<uuid>",
"type": "pin.setup",
"payload": {
"encrypted_pin": "<base64>",
"ephemeral_public_key": "<base64-X25519-32-bytes>",
"nonce": "<base64-12-bytes>"
},
"timestamp": "<ISO8601>"
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique request ID |
type |
string | Yes | Must be "pin.setup" |
payload.encrypted_pin |
string | Yes | ECIES-encrypted PIN payload |
payload.ephemeral_public_key |
string | Yes | App's ephemeral X25519 public key |
payload.nonce |
string | Yes | 12-byte nonce for ChaCha20-Poly1305 |
timestamp |
string | Yes | ISO8601 timestamp |
Encrypted PIN Payload Format:
The encrypted_pin field contains the ChaCha20-Poly1305 ciphertext of:
{"pin": "123456"}Encryption Process (ECIES):
- App generates ephemeral X25519 keypair
- Compute shared secret:
ECDH(ephemeral_private, enclave_public_key) - Derive key:
HKDF-SHA256(shared_secret, "vettid-pin-encryption-v1") - Encrypt:
ChaCha20-Poly1305(derived_key, nonce, plaintext={"pin": "123456"})
{
"status": "vault_ready",
"utks": [
{ "id": "<utk-id>", "public_key": "<base64>" },
...
]
}| Field | Type | Description |
|---|---|---|
status |
string | "vault_ready" on success |
utks |
array | User Transaction Keys for credential creation |
Note: PIN setup does NOT return the credential. The vault is initialized and ready, but the Protean Credential is created in the next step.
{
"id": "<uuid>",
"type": "credential.create",
"utk_id": "<utk-id>",
"encrypted_payload": "<base64>",
"timestamp": "<ISO8601>"
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique request ID |
type |
string | Yes | Must be "credential.create" |
utk_id |
string | Yes | UTK used for encryption |
encrypted_payload |
string | Yes | UTK-encrypted password hash |
timestamp |
string | Yes | ISO8601 timestamp |
Encrypted Payload (before encryption):
{
"password_hash": "$argon2id$v=19$m=65536,t=3,p=4$<base64-salt>$<base64-hash>"
}The app hashes the credential password with Argon2id and encodes in PHC (Password Hashing Competition) string format.
PHC String Format:
- Self-describing format that includes algorithm, version, and all parameters
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>- Salt is encoded in base64 (without padding)
- Hash is encoded in base64 (without padding)
Required Argon2id Parameters:
m(memory): minimum 65536 (64 MB)t(time/iterations): minimum 3p(parallelism): minimum 1- Key length: 32 bytes
Encryption:
- Use XChaCha20-Poly1305 (24-byte nonce)
- HKDF domain:
vettid-utk-v1 - Format:
ephemeral_pubkey (32) || nonce (24) || ciphertext
{
"status": "created",
"encrypted_credential": "<base64>",
"new_utks": [
{ "id": "<utk-id>", "public_key": "<base64>" },
...
]
}| Field | Type | Description |
|---|---|---|
status |
string | "created" on success |
encrypted_credential |
string | CEK-encrypted Protean Credential |
new_utks |
array | Fresh UTKs for future operations |
The encrypted_credential contains the user's Protean Credential, encrypted with the CEK using XChaCha20-Poly1305 with domain vettid-cek-v1.
Decrypted Structure:
{
"identity_private_key": "<base64-Ed25519>",
"identity_public_key": "<base64-Ed25519>",
"vault_master_secret": "<base64-32-bytes>",
"password_hash": "$argon2id$v=19$m=65536,t=3,p=4$<base64-salt>$<base64-hash>",
"auth_type": "password",
"crypto_keys": [],
"created_at": 1705555555,
"version": 1
}| Field | Description |
|---|---|
identity_private_key |
User's Ed25519 signing key (kept in enclave memory) |
identity_public_key |
User's public identity (shared with contacts) |
vault_master_secret |
Seed for deriving sub-keys |
password_hash |
PHC-format string containing Argon2id hash + salt + params |
auth_type |
Always "password" for new credentials |
crypto_keys |
Additional derived keys (populated later) |
Note: The password_hash uses PHC format which is self-describing - the salt and all parameters are embedded in the string. This ensures the app always uses the correct parameters for verification, even if defaults change in future versions.
All operations can return errors:
{
"status": "error",
"error": "<error-message>",
"timestamp": "<ISO8601>"
}Common Errors:
| Error | Cause |
|---|---|
"attestation key required" |
Attestation not done or expired (5 min) |
"invalid payload format" |
Malformed request |
"PIN must be 4-8 digits" |
PIN validation failed |
"decryption failed" |
ECIES decryption error |
"invalid UTK" |
UTK not found or already used |
"vault not initialized" |
PIN setup not completed |
Responses are delivered via JetStream for guaranteed delivery:
Stream: ENROLLMENT
Subjects: OwnerSpace.*.forApp.>
Consumer Configuration (for mobile apps):
ConsumerConfiguration.builder()
.filterSubject("OwnerSpace.{guid}.forApp.>")
.deliverPolicy(DeliverPolicy.New)
.ackPolicy(AckPolicy.None)
.build()Why JetStream?
- Responses may arrive before subscription is established
- Messages persist for 30 minutes
- Survives brief network disconnections
- Attestation keys are held in enclave memory for 5 minutes
- PIN setup must occur within this window
- If expired, request a new attestation
- PIN protects DEK derivation (combined with KMS-sealed material)
- Password protects operation authorization (Argon2id hashed)
- Both are required for full vault access
- Each UTK can only be used once
- Always use fresh UTK from previous response
- Reusing UTKs will fail
| Old (EC2) | New (Nitro) |
|---|---|
POST /vault/enroll/start |
NATS: forVault.attestation |
POST /vault/enroll/set-password |
NATS: forVault.pin + forVault.credential.create |
POST /vault/enroll/finalize |
Not needed |
| Poll for vault status | Not needed - enclave always ready |
encrypted_blob |
encrypted_credential |
-
2026-01-19: Security cryptography updates
- Changed password_hash to PHC string format (self-describing with embedded params)
- Removed separate auth_salt field (salt now in PHC string)
- Updated encryption to XChaCha20-Poly1305 (24-byte nonce)
- Added HKDF domain separation (vettid-cek-v1, vettid-utk-v1)
- Updated Argon2id to 64MB memory (OWASP recommended minimum)
- Added detailed encryption format documentation
-
2026-01-18: Initial specification for Nitro architecture
- Added complete three-phase enrollment flow (Attestation → PIN → Credential)
- Documented PIN vs Credential Password distinction
- Added credential.create message format
- Documented JetStream consumer requirements