Key custody
The protocol is silent on where a principal’s hybrid private key lives. It just specifies the signing operations the key must support. This page describes the three custody modes the SDK and Verify support, the tradeoffs, and when to use each.
┌─────────────────────────────────┐ │ Principal's hybrid private │ │ key │ │ │ │ ▸ Ed25519 component │ │ ▸ ML-DSA-65 component │ └────────────┬────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │ Self-custody │ │ Custodial │ │ Self-custody upgrade │ │ │ │ │ │ │ │ Key lives on │ │ Key lives in │ │ Started custodial. │ │ user's device. │ │ Verify backend, │ │ Generated a new key │ │ Never sent over │ │ envelope-encr- │ │ on device. Signed a │ │ the wire. │ │ ypted by Cloud │ │ rotation statement │ │ │ │ KMS. │ │ by BOTH old +new. │ │ Strongest. │ │ │ │ Continuous identity │ │ │ │ Convenient for │ │ with strictly │ │ │ │ most teams. │ │ stronger custody │ │ │ │ │ │ going forward. │ └──────────────────┘ └──────────────────┘ └──────────────────────┘Self-custody
Section titled “Self-custody”The user generates their hybrid keypair locally — on their device, in their browser, on their phone, on their server — and the private key never leaves that environment.
import { generateHumanRoot, issueDelegation } from "@identities-ai/ratify-protocol";
// User generates keypair locallyconst { root, privateKey } = await generateHumanRoot();
// Only the public root.id and root.public_key are ever sharedawait publishToRegistry(root.id, root.public_key);
// Delegations are signed locally with the private keyconst cert = { /* fields */ };await issueDelegation(cert, privateKey);
// privateKey never leaves this environmentThreat model
Section titled “Threat model”| Threat | Mitigation |
|---|---|
| Server-side key extraction | Not applicable — the key isn’t on a server |
| Insider at Identities AI accesses keys | Not applicable — Identities AI never has the key |
| Compromised laptop/device | Real threat. Use OS keychain / Secure Enclave / TPM. |
| Lost device with no backup | Real threat. The user must back up the key (encrypted at rest) or accept that losing the device means losing the identity. |
Self-custody is the strongest mode by definition: there is no third party to compromise. But it puts operational burden on the user (backups, device sync, recovery). For technical users this is acceptable; for typical enterprise end-users it’s a UX challenge.
Custodial
Section titled “Custodial”The Verify backend generates the keypair server-side and stores it under envelope encryption:
┌──────────────────────────────────────────────┐ │ Ratify Verify backend │ │ │ │ 1. Generate hybrid keypair in-memory │ │ 2. Generate a fresh DEK (data enc key) │ │ 3. Encrypt private key bytes with DEK │ │ using AES-256-GCM │ │ 4. Encrypt DEK with Cloud KMS KEK │ │ (key encryption key) │ │ 5. Store {encrypted_priv, encrypted_DEK} │ │ in DB │ │ 6. Wipe in-memory plaintext │ │ │ └────────────┬─────────────────────────────────┘ │ │ To sign a delegation: │ - Fetch encrypted record │ - Decrypt DEK via Cloud KMS API │ - Decrypt private key with DEK │ - Sign in-memory │ - Wipe plaintext ▼ ┌──────────────────────────────────────────────┐ │ At rest, Verify only has: │ │ - encrypted private key (AES-GCM) │ │ - encrypted DEK (Cloud KMS-wrapped) │ │ │ │ Cannot sign without both DB access AND │ │ IAM permission on the Cloud KMS KEK. │ └──────────────────────────────────────────────┘Threat model
Section titled “Threat model”| Threat | Mitigation |
|---|---|
| Database leak alone | Encrypted keys are useless without the Cloud KMS KEK |
| Cloud KMS compromise alone | DEKs are useless without the DB |
| Identities AI insider | Requires both DB access AND KMS access. Audit-logged. Need-to-know. |
| Verify service compromise | Real threat. Verify must be hardened. SOC 2 / ISO 27001 in progress. |
| Quantum adversary in the future | Same as self-custody — hybrid signatures defend against this |
Custodial is the right default for enterprise SaaS use cases: users sign in, the system handles keys, no one loses their identity to a dead laptop. The tradeoff is trust: you trust Identities AI’s operational controls, audit posture, and incident response.
Self-custody upgrade
Section titled “Self-custody upgrade”A user who started in custodial mode can migrate to self-custody at any time without losing
their identity. The mechanism is a KeyRotationStatement signed by both the old (custodial)
key and the new (device) key.
┌──────────────────────────────────────────┐ │ User's account in Verify: │ │ - custodial key (in KMS) — old_root │ │ - delegations issued by old_root │ └────────────────┬─────────────────────────┘ │ │ User decides to migrate │ ▼ ┌──────────────────────────────────────────┐ │ User generates fresh hybrid keypair │ │ on their device → new_root │ └────────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ KeyRotationStatement: │ │ old_id: old_root.id │ │ old_pub_key: old_root.public_key │ │ new_id: new_root.id │ │ new_pub_key: new_root.public_key │ │ rotated_at: now │ │ reason: "self_custody_upgrade" │ │ signature_old: signed by old_root │ │ signature_new: signed by new_root │ └────────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Anyone with old_root.public_key OR │ │ new_root.public_key can verify the │ │ continuity: same person, new key. │ │ │ │ Existing delegations issued by old_root│ │ stay valid until expiry. New ones must │ │ be issued by new_root. │ └──────────────────────────────────────────┘import { generateHumanRoot, issueKeyRotationStatement } from "@identities-ai/ratify-protocol";
// User generates a NEW keypair on their deviceconst { root: newRoot, privateKey: newPrivateKey } = await generateHumanRoot();
// Rotation statement signed by BOTH old (custodial) and new (device) keysconst stmt = { version: 1, old_id: oldRoot.id, old_pub_key: oldRoot.public_key, new_id: newRoot.id, new_pub_key: newRoot.public_key, rotated_at: Math.floor(Date.now() / 1000), reason: "self_custody_upgrade", signature_old: { ed25519: new Uint8Array(), ml_dsa_65: new Uint8Array() }, signature_new: { ed25519: new Uint8Array(), ml_dsa_65: new Uint8Array() },};await issueKeyRotationStatement(stmt, oldCustodialPrivateKey, newPrivateKey);
// Verify accepts new_root signatures going forward.// Old_root signatures still verify against old delegations until those expire.The double-signature is what proves continuity. Without the old key’s signature, anyone could claim to be the rotation target. Without the new key’s signature, anyone could replay the statement. Both are required.
Picking a mode
Section titled “Picking a mode”| You’re building… | Recommended mode |
|---|---|
| A research prototype | Self-custody |
| A consumer agent (browser/mobile) | Custodial, with optional self-custody upgrade later |
| Enterprise SaaS where users sign in via SSO | Custodial — that’s what enterprise IT expects |
| A regulated deployment (SOX, HIPAA, FINRA) | Custodial initially, with a documented self-custody upgrade path for users who request it |
| An embedded device (drone, vehicle) | Self-custody using the device’s secure element (TPM, ARM TrustZone, Secure Enclave) |
| An OSS project distributing identities | Self-custody, no Verify needed |
Where to next
Section titled “Where to next”- Revocation — what to do if a key is compromised regardless of custody mode
- Ratify Verify overview — what custodial Verify operationally looks like
- Hybrid post-quantum crypto — the underlying signature primitives