From 4e10c0a22749e5ab4189d33088d7217e1e5ef71a Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Mon, 10 Aug 2026 15:07:15 -0400 Subject: [PATCH 01/29] Add Secret Storage feature page and Programmable Credential Manager solution page --- docs.json | 6 +- features/secrets.mdx | 94 ++++++++++++ .../programmable-credential-manager.mdx | 142 ++++++++++++++++++ 3 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 features/secrets.mdx create mode 100644 solutions/key-management/programmable-credential-manager.mdx diff --git a/docs.json b/docs.json index 21bd186b..6638beae 100644 --- a/docs.json +++ b/docs.json @@ -225,7 +225,8 @@ "group": "Solution", "pages": [ "solutions/key-management/encryption-key-storage", - "solutions/key-management/enterprise-disaster-recovery" + "solutions/key-management/enterprise-disaster-recovery", + "solutions/key-management/programmable-credential-manager" ] } ] @@ -359,7 +360,8 @@ }, "features/wallets/pregenerated-wallets", "features/wallets/claim-links", - "features/wallets/aa-wallets" + "features/wallets/aa-wallets", + "features/secrets" ] }, { diff --git a/features/secrets.mdx b/features/secrets.mdx new file mode 100644 index 00000000..afea3013 --- /dev/null +++ b/features/secrets.mdx @@ -0,0 +1,94 @@ +--- +title: "Secret Storage" +description: "Import, store, and export arbitrary secrets — passwords, credit cards, API keys — with policy-gated, end-to-end encrypted access." +--- + +import { FeatureCard } from '/snippets/feature-card.mdx' +import { SolutionCard } from '/snippets/solution-card.mdx' + + + **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. + + +Turnkey Secrets lets you store arbitrary sensitive data — passwords, credit card details, API keys, SSNs — encrypted end-to-end between your client and Turnkey's [secure enclaves](/security/secure-enclaves). Every export is evaluated by the [policy engine](/features/policies/overview), so you control exactly who can retrieve a secret, under what conditions, and with how many approvals. The secret storage API was designed for maximum flexibility and programmability. + +Plaintext only ever exists inside the enclave and on the client that imported or exported it. Turnkey's coordinator, database, and public API only ever see ciphertext. + +## How it works + +**Import**: Turnkey mints a single-use ingress target key inside the enclave, signed by the enclave's quorum key. Your client verifies that signature, encrypts the secret to the target key using HPKE, and submits the ciphertext. The enclave decrypts it, re-encrypts it for storage at rest under a quorum-key-derived key, and deletes the ingress key. + +**Export**: the export request carries an ephemeral P-256 target public key, and after policy evaluation approves the request, the enclave decrypts the stored secret and re-encrypts it to that key. The target key is fully configurable: it can belong to the requester, to another agent or service, or to a party that isn't an approver at all — whoever holds the private half is the only one who can decrypt the result. The export payload is useless to anyone else, including the approvers themselves. + +For a batch export, the request succeeds only if every policy evaluation returns `ALLOW`; a `DENY` or any evaluation without an `ALLOW` outcome, rejects the entire batch. No batches are partially exported. + +## Static properties + +Secrets are created with optional **static properties**: string key-value pairs that are immutably bound to the secret and visible to the policy engine. They let you write export policies against classes of secrets instead of individual IDs: + +```json +{ + "policyName": "Only the payments agent can export credit cards", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(u, u.tags.contains('payment-agent'))", + "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['kind'] == 'creditCard'" +} +``` + +## Importing a secret + +The `importSecret` method in [`@turnkey/sdk-server`](/sdks/typescript-sdk) and [`@turnkey/core`](/sdks/typescript-sdk) handles the full flow — initializing the ingress key, verifying the enclave signature, encrypting, and submitting: + +```typescript +const secretId = await turnkey.apiClient().importSecret({ + plaintext: JSON.stringify({ number: "4242...", exp: "11/29", cvv: "123" }), + name: "corporateVisa", + staticProperties: { + kind: "creditCard", + requiresConsensus: "true", + }, +}); +``` + +For sensitive material you want wiped from memory after encryption, pass a `Uint8Array` instead of a string — the SDK zeroizes the buffer once the ciphertext is produced. + +Under the hood this calls [init_import_secrets](/api-reference/activities/init-import-secrets) and [import_secrets](/api-reference/activities/import-secrets). + +## Exporting a secret + +`exportSecret` generates the ephemeral keypair, submits the export activity, decrypts the result, and zeroizes the key — a single call when policy allows the caller to export unilaterally: + +```typescript +const plaintext = await turnkey.apiClient().exportSecret({ + secretId, +}); +``` + +If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows — including multiple agent instances co-signing the same export with session keys — use the proposal sdk helpers described in [Programmable Credential Manager](/solutions/key-management/programmable-credential-manager). + +## Listing secrets + +[list_secrets](/api-reference/queries/list-secrets) returns metadata: IDs, names, static properties, and creation timestamps: + +```typescript +const { secrets } = await turnkey.apiClient().listSecrets({}); +``` + +## Multi-party approval + +Because export is an activity, it composes with everything the policy engine supports: [consensus](/features/policies/overview) across durable users, tag-based approver requirements, and [root quorum](/features/users/root-quorum). Model browser and payment agent roles as separate Turnkey users, then use session keys to authenticate their ephemeral instances. Policy can require both roles to approve before a credit card leaves the enclave — while the payload stays encrypted to only one instance. This makes credential delegation easy to model without treating each ephemeral agent instance as a separate user. + +## Security model + +- **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys. +- **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key, with the organization, secret ID, and cipher suite bound into the authenticated data — ciphertext cannot be substituted across secrets or organizations. +- **Signed provenance**: every stored secret and ingress key is signed by the enclave quorum key; enclaves refuse anything they didn't produce. +- **Forward secrecy**: ingress and egress target keys are single-use. Compromising one exposes at most one payload. +- **Full auditability**: every import, export, and approval is an activity — attributable to the authenticating credential, logged, and queryable. + +## Next steps + +
+ + +
diff --git a/solutions/key-management/programmable-credential-manager.mdx b/solutions/key-management/programmable-credential-manager.mdx new file mode 100644 index 00000000..476c3033 --- /dev/null +++ b/solutions/key-management/programmable-credential-manager.mdx @@ -0,0 +1,142 @@ +--- +title: "Programmable Credential Manager" +description: "A policy-gated, programmable access layer for secrets — built for humans, services, and AI agents." +--- + +import { FeatureCard } from '/snippets/feature-card.mdx' +import { SolutionCard } from '/snippets/solution-card.mdx' + + + **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. + + +When an AI agent needs a password, a card number, or an API key, teams face an unpleasant choice: hand the agent unfettered access to credentials, or put a human in the loop for every single request. Turnkey gives you a third option — a programmable access layer where every credential request is evaluated against policies you control, inside a [secure enclave](/security/secure-enclaves) that never releases plaintext unless the policy allows it. + +Policies, metadata, and tags are fully dynamic: update them at runtime to widen or narrow access on the fly, without re-importing anything. This solution builds on [Secret Storage](/features/secrets). + +## Access patterns + +One policy engine supports the full spectrum of trust models: + +| Pattern | How it works | +| :--- | :--- | +| 🤖 **Unilateral agent** | A session key authenticates an ephemeral agent instance as a durable agent-role user whose policy permits direct export. | +| 🤖 → 🧑 **Agent → human** | An agent instance requests access; the export completes only after a human approves the pending activity. | +| 🤖 🤝 🤖 **Multi-agent consensus** | Instances of separate agent-role users must each sign the same export before the secret is released, and only the designated recipient can decrypt it. | + +## Key implementation decisions + +| Decision | What to consider | Learn more | +| :--- | :--- | :--- | +| **Secret classification** | Bind static properties at import time (`kind`, `environment`, `requiresConsensus`, ...) so policies target classes of secrets, not IDs. | [Secret Storage](/features/secrets) | +| **Agent identity** | Model each agent type or role as a durable Turnkey user. A scoped, expiring session key authenticates each ephemeral instance of that user, making credential delegation easy to express in policy. | [Sessions](/features/authentication/sessions) | +| **Consensus requirements** | Decide which secret classes need one agent role, several, or a specific combination of user tags (e.g. one `browser-agent` *and* one `payment-agent`). | [Policy Engine](/features/policies/overview) | +| **Recipient control** | The export payload is encrypted to a single ephemeral public key. Decide which party generates that key — that party, and only that party, can read the secret. | | +| **Revocation** | Invalidate a session key to revoke one agent instance, or delete the agent-role user to revoke every instance of that role. | [Sessions](/features/authentication/sessions) | + +## Example: multi-agent consensus for payments + +A browser agent and a payment agent are modeled as durable Turnkey users, with session keys authenticating their ephemeral instances. Policy requires both agent roles to approve before card details are exported, and only the payment agent instance holds the decryption key — so neither instance can act alone, and the browser agent never sees the card. + +| Need | How Turnkey solves it | +| :--- | :--- | +| No single agent instance can exfiltrate the card | Consensus policy requires approvals from both durable agent-role users before the enclave re-encrypts the secret | +| Approver ≠ recipient | The payload is encrypted to the payment agent instance's ephemeral key; the browser agent instance's approval releases a ciphertext it cannot read | +| Agent instances act in parallel, not lockstep | Both instances sign and submit the byte-identical export request in any order; Turnkey matches them to the same activity | +| Every access is attributable | Each export and approval is signed with a session key and logged under the durable agent-role user | + +### Policy: require two agent roles for credit card access + +```json +{ + "policyName": "Require two agent roles for credit card access", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(u, u.tags.contains('browser-agent')) && approvers.any(u, u.tags.contains('payment-agent'))", + "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['requiresConsensus'] == 'true' && secret.static_properties['kind'] == 'creditCard'" +} +``` + +### Implementation steps + + + + Import the card once, with static properties that the policy above targets. Any client with import permission can do this — here, a backend service: + + ```typescript + const secretId = await turnkey.apiClient().importSecret({ + plaintext: JSON.stringify({ number: "4242...", exp: "11/29", cvv: "123" }), + name: "corporateVisa", + staticProperties: { + kind: "creditCard", + requiresConsensus: "true", + }, + }); + ``` + + + + The payment agent instance — the intended recipient — generates an ephemeral keypair and builds the proposal. `createExportSecretsProposal` is a local call: it produces the canonical request body and its fingerprint, with no network round trip. + + ```typescript + import { generateP256KeyPair } from "@turnkey/crypto"; + + const { publicKey, privateKey } = generateP256KeyPair(); + + const proposal = paymentAgent.createExportSecretsProposal({ + secrets: [{ secretId }], + targetPublicKey: publicKey, + organizationId, + }); + ``` + + The proposal is plain JSON and contains no key material — share it with co-signing agent instances over any channel. + + + + Each agent instance stamps the identical proposal body with the session key for its durable agent-role user and submits. Order doesn't matter: the first submission creates the activity, and every subsequent identical submission counts as an approval. + + ```typescript + await Promise.all([ + paymentAgent.submitExportSecrets(proposal), + browserAgent.submitExportSecrets(proposal), + ]); + ``` + + Until the consensus expression is satisfied, the activity reports `ACTIVITY_STATUS_CONSENSUS_NEEDED` and no secret leaves the enclave. + + + + Once policy is satisfied, the enclave re-encrypts the card to the payment agent instance's ephemeral key. Only that instance can decrypt: + + ```typescript + const [cardJson] = await paymentAgent.awaitExportedSecrets({ + proposal, + embeddedPrivateKey: privateKey, + }); + + const card = JSON.parse(cardJson); + ``` + + + + + For direct human approval instead of a second agent role, skip the co-signing step: the activity stays in `CONSENSUS_NEEDED` until the human approves it from the dashboard or via [approve_activity](/api-reference/activities/approve-activity). + + +## Under the hood + +Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives underpin credential management: + +- 📜 **Policy engine** — gate on identity, approval count, tags, static properties, and more. +- 🔄 **Dynamic policies** — policies and tags are mutable; widen or narrow access programmatically, at runtime. +- 🔐 **End-to-end encryption** — plaintext exists only in the enclave and on the recipient's client; approvers see ciphertext they cannot read. +- 📋 **Fully auditable** — every request and approval is logged under the durable agent-role user and authenticating session key. + +## Next steps + +
+ + + + +
From e0ad8e87ef4f518a7181f89dafe1f2969c04e20d Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:17:31 -0400 Subject: [PATCH 02/29] Rename Programmable Credential Manager to Programmable Credential Access and add High Security API Key Storage solution page --- docs.json | 3 +- features/secrets.mdx | 5 +- .../high-security-api-key-storage.mdx | 137 ++++++++++++++++++ solutions/key-management/overview.mdx | 12 ++ ...mdx => programmable-credential-access.mdx} | 5 +- 5 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 solutions/key-management/high-security-api-key-storage.mdx rename solutions/key-management/{programmable-credential-manager.mdx => programmable-credential-access.mdx} (95%) diff --git a/docs.json b/docs.json index 6638beae..7f57e7cf 100644 --- a/docs.json +++ b/docs.json @@ -226,7 +226,8 @@ "pages": [ "solutions/key-management/encryption-key-storage", "solutions/key-management/enterprise-disaster-recovery", - "solutions/key-management/programmable-credential-manager" + "solutions/key-management/programmable-credential-access", + "solutions/key-management/high-security-api-key-storage" ] } ] diff --git a/features/secrets.mdx b/features/secrets.mdx index afea3013..9307b9c2 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -64,7 +64,7 @@ const plaintext = await turnkey.apiClient().exportSecret({ }); ``` -If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows — including multiple agent instances co-signing the same export with session keys — use the proposal sdk helpers described in [Programmable Credential Manager](/solutions/key-management/programmable-credential-manager). +If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows — including multiple agent instances co-signing the same export with session keys — use the proposal sdk helpers described in [Programmable Credential Access](/solutions/key-management/programmable-credential-access). ## Listing secrets @@ -89,6 +89,7 @@ Because export is an activity, it composes with everything the policy engine sup ## Next steps
- + +
diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx new file mode 100644 index 00000000..a83f285b --- /dev/null +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -0,0 +1,137 @@ +--- +title: "High Security API Key Storage" +description: "Custody exchange keys, trading credentials, and bearer tokens in secure enclaves, so plaintext never lives in your infrastructure." +--- + +import { FeatureCard } from '/snippets/feature-card.mdx' +import { SolutionCard } from '/snippets/solution-card.mdx' + + + **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. + + +An exchange API key is not just another config value. A key that can place orders, cancel orders, or move funds is a direct line to your balance sheet, and it usually sits in an environment variable, a config file, or a vault that decrypts inside the very infrastructure you are trying to protect. If your infrastructure is compromised, the key is too. + +Turnkey removes the plaintext from your infrastructure entirely. The credential lives encrypted inside a [secure enclave](/security/secure-enclaves), and every retrieval is evaluated by the [policy engine](/features/policies/overview) before the enclave releases anything. You decide which service identities can retrieve which keys, under what conditions, and with how many approvals. This solution builds on [Secret Storage](/features/secrets). + +Typical credentials this pattern protects: + +- Exchange and EMS API keys for trading operations (REST and WebSocket) +- Long-lived JWTs and bearer tokens with broad authority +- OAuth refresh tokens for backend services +- Service account credentials for third party platforms + +## Why not a traditional secrets vault? + +| | Vault in your infrastructure | Turnkey | +| :--- | :--- | :--- | +| Where plaintext exists | Decrypted inside your infrastructure; a vault admin or a compromised host can read it | Only inside the enclave and on the single authorized recipient | +| Access control | Enforced by software you operate | Enforced by the policy engine inside the enclave, independent of your infrastructure | +| Multi-party approval | Bolted on, if available | Native consensus: require m-of-n approvals before a key is released | +| Who can read a released secret | Anyone who sees the response | Only the holder of the ephemeral key the payload is encrypted to, which approvers cannot read | +| Audit trail | Vault logs you maintain | Every retrieval and approval is a signed, attributable Turnkey activity | + +Encrypting the credential client side and storing the ciphertext in your own database has the same weakness: the ciphertext, and eventually the plaintext, still transits infrastructure you have to defend. With Turnkey the credential enters the enclave once and only comes out policy-gated, encrypted to a single recipient. + +## Key implementation decisions + +| Decision | What to consider | Learn more | +| :--- | :--- | :--- | +| **Key classification** | Bind static properties at import time (`exchange`, `permissions`, `environment`, ...) so policies target classes of keys, such as all withdrawal-capable keys, instead of individual IDs. | [Secret Storage](/features/secrets) | +| **Service identity** | Model each trading service or environment as a Turnkey user with its own API key or session keys, so retrieval permission is scoped per service. | [Sessions](/features/authentication/sessions) | +| **Approval requirements** | Trade-only keys can allow unilateral retrieval by the trading service; withdrawal-capable or production keys can require human or multi-party approval. | [Policy Engine](/features/policies/overview) | +| **Recipient control** | The export payload is encrypted to a single ephemeral public key, so only the service that generated it can read the credential, even when other parties approve. | | +| **Rotation and revocation** | Rotate by importing the new key and deleting the old one; revoke a service's access instantly by updating policy or removing its credentials. | | + +## Example: exchange trading key for a trading firm + +A trading firm holds a long-lived JWT for an execution management system that authorizes placing and canceling orders on crypto exchanges. The plaintext should never exist in the firm's own infrastructure. + +| Need | How Turnkey solves it | +| :--- | :--- | +| Plaintext never lives in the firm's infrastructure | The JWT is imported once over an end-to-end encrypted channel and stored inside the enclave; retrieval re-encrypts it to a single ephemeral key | +| Only the trading service can retrieve it | Policy scopes retrieval of `kind == 'exchangeApiKey'` secrets to the trading service user | +| High-risk keys need oversight | A consensus policy requires human approval before any withdrawal-capable key is released | +| Every access is attributable | Each retrieval and approval is a signed activity, logged and queryable | + +### Policy: scope retrieval to the trading service + +```json +{ + "policyName": "Trading service can retrieve trade-only exchange keys", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(u, u.id == '')", + "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'trade'" +} +``` + +### Implementation steps + + + + Import the credential once, with static properties the policies above target. Plaintext is encrypted to the enclave on the client; Turnkey's API and database only ever see ciphertext: + + ```typescript + const secretId = await turnkey.apiClient().importSecret({ + plaintext: emsJwt, + name: "ems-trading-key", + staticProperties: { + kind: "exchangeApiKey", + permissions: "trade", + environment: "production", + }, + }); + ``` + + The plaintext can now be deleted from wherever it existed before. + + + + The trading service authenticates with its own credentials and retrieves the key when it boots or opens a session. `exportSecret` generates the ephemeral keypair, submits the export activity, decrypts the result, and zeroizes the key: + + ```typescript + const emsJwt = await turnkey.apiClient().exportSecret({ + secretId, + }); + + // Use the JWT for REST or WebSocket calls, keep it in memory only + ``` + + + + For withdrawal-capable keys, add a consensus policy so no service can retrieve them alone: + + ```json + { + "policyName": "Withdrawal-capable keys require a human approver", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(u, u.id == '') && approvers.any(u, u.tags.contains('risk-admin'))", + "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'withdraw'" + } + ``` + + The export stays in `ACTIVITY_STATUS_CONSENSUS_NEEDED` until a risk admin approves it from the dashboard or via [approve_activity](/api-reference/activities/approve-activity), and the released payload is still readable only by the trading service. + + + + Rotate by importing the replacement key and deleting the old secret. Revoke a service by invalidating its session keys or removing its user; policy changes take effect immediately, with no re-encryption or re-import of the stored keys. + + + +## Under the hood + +Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives protect API keys: + +- 🔐 **End-to-end encryption**: plaintext exists only in the enclave and on the single authorized recipient; approvers see ciphertext they cannot read. +- 📜 **Policy engine**: gate retrieval on identity, static properties, approval counts, and tags. +- 🔄 **Dynamic policies**: widen or narrow access at runtime without re-importing anything. +- 📋 **Fully auditable**: every import, retrieval, and approval is a signed, queryable activity. + +## Next steps + +
+ + + + +
diff --git a/solutions/key-management/overview.mdx b/solutions/key-management/overview.mdx index f8ecba93..e328d4b1 100644 --- a/solutions/key-management/overview.mdx +++ b/solutions/key-management/overview.mdx @@ -68,6 +68,18 @@ Key management serves different needs depending on how your application uses cry href="/solutions/key-management/enterprise-disaster-recovery" description="Import and recover wallets with end-to-end encryption, quorum-controlled access, and a cryptographic audit trail for treasury recovery, provider migration, and failover." /> + + ## Ready to build? diff --git a/solutions/key-management/programmable-credential-manager.mdx b/solutions/key-management/programmable-credential-access.mdx similarity index 95% rename from solutions/key-management/programmable-credential-manager.mdx rename to solutions/key-management/programmable-credential-access.mdx index 476c3033..56bb6b5c 100644 --- a/solutions/key-management/programmable-credential-manager.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -1,6 +1,6 @@ --- -title: "Programmable Credential Manager" -description: "A policy-gated, programmable access layer for secrets — built for humans, services, and AI agents." +title: "Programmable Credential Access" +description: "A password manager built for machines: policy-gated, programmable access to secrets for humans, services, and AI agents." --- import { FeatureCard } from '/snippets/feature-card.mdx' @@ -138,5 +138,6 @@ Turnkey is a signing and encryption platform running inside secure enclaves, ori + From 3ffe2adb0172fcfdde882cfe1feb15cc8b24f7fd Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:19:09 -0400 Subject: [PATCH 03/29] Add Beta sidebar tag to secrets feature and solutions pages --- features/secrets.mdx | 1 + solutions/key-management/high-security-api-key-storage.mdx | 1 + solutions/key-management/programmable-credential-access.mdx | 1 + 3 files changed, 3 insertions(+) diff --git a/features/secrets.mdx b/features/secrets.mdx index 9307b9c2..eb6f8468 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -1,6 +1,7 @@ --- title: "Secret Storage" description: "Import, store, and export arbitrary secrets — passwords, credit cards, API keys — with policy-gated, end-to-end encrypted access." +tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx index a83f285b..3973c670 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -1,6 +1,7 @@ --- title: "High Security API Key Storage" description: "Custody exchange keys, trading credentials, and bearer tokens in secure enclaves, so plaintext never lives in your infrastructure." +tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index 56bb6b5c..b8549ead 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -1,6 +1,7 @@ --- title: "Programmable Credential Access" description: "A password manager built for machines: policy-gated, programmable access to secrets for humans, services, and AI agents." +tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' From abb5fc627e25ff72a967df7787c15869509d20ca Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:32:25 -0400 Subject: [PATCH 04/29] Apply STE plain-writing pass to secrets pages --- features/secrets.mdx | 24 ++++++++--------- .../high-security-api-key-storage.mdx | 18 ++++++------- .../programmable-credential-access.mdx | 26 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/features/secrets.mdx b/features/secrets.mdx index eb6f8468..50bb49db 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -1,6 +1,6 @@ --- title: "Secret Storage" -description: "Import, store, and export arbitrary secrets — passwords, credit cards, API keys — with policy-gated, end-to-end encrypted access." +description: "Import, store, and export arbitrary secrets (passwords, credit cards, API keys) with policy-gated, end-to-end encrypted access." tag: "Beta" --- @@ -11,7 +11,7 @@ import { SolutionCard } from '/snippets/solution-card.mdx' **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. -Turnkey Secrets lets you store arbitrary sensitive data — passwords, credit card details, API keys, SSNs — encrypted end-to-end between your client and Turnkey's [secure enclaves](/security/secure-enclaves). Every export is evaluated by the [policy engine](/features/policies/overview), so you control exactly who can retrieve a secret, under what conditions, and with how many approvals. The secret storage API was designed for maximum flexibility and programmability. +Turnkey Secrets lets you store arbitrary sensitive data (passwords, credit card details, API keys, SSNs) encrypted end-to-end between your client and Turnkey's [secure enclaves](/security/secure-enclaves). The [policy engine](/features/policies/overview) evaluates every export, so you control exactly who can retrieve a secret, under what conditions, and with how many approvals. We designed the secret storage API for flexibility and programmability. Plaintext only ever exists inside the enclave and on the client that imported or exported it. Turnkey's coordinator, database, and public API only ever see ciphertext. @@ -19,9 +19,9 @@ Plaintext only ever exists inside the enclave and on the client that imported or **Import**: Turnkey mints a single-use ingress target key inside the enclave, signed by the enclave's quorum key. Your client verifies that signature, encrypts the secret to the target key using HPKE, and submits the ciphertext. The enclave decrypts it, re-encrypts it for storage at rest under a quorum-key-derived key, and deletes the ingress key. -**Export**: the export request carries an ephemeral P-256 target public key, and after policy evaluation approves the request, the enclave decrypts the stored secret and re-encrypts it to that key. The target key is fully configurable: it can belong to the requester, to another agent or service, or to a party that isn't an approver at all — whoever holds the private half is the only one who can decrypt the result. The export payload is useless to anyone else, including the approvers themselves. +**Export**: the export request carries an ephemeral P-256 target public key, and after policy evaluation approves the request, the enclave decrypts the stored secret and re-encrypts it to that key. The target key is fully configurable: it can belong to the requester, to another agent or service, or to a party that isn't an approver at all. Only the holder of the private half can decrypt the result. The export payload is useless to anyone else, including the approvers themselves. -For a batch export, the request succeeds only if every policy evaluation returns `ALLOW`; a `DENY` or any evaluation without an `ALLOW` outcome, rejects the entire batch. No batches are partially exported. +For a batch export, the request succeeds only if every policy evaluation returns `ALLOW`. A `DENY`, or any evaluation without an `ALLOW` outcome, rejects the entire batch. Turnkey never exports part of a batch. ## Static properties @@ -38,7 +38,7 @@ Secrets are created with optional **static properties**: string key-value pairs ## Importing a secret -The `importSecret` method in [`@turnkey/sdk-server`](/sdks/typescript-sdk) and [`@turnkey/core`](/sdks/typescript-sdk) handles the full flow — initializing the ingress key, verifying the enclave signature, encrypting, and submitting: +The `importSecret` method in [`@turnkey/sdk-server`](/sdks/typescript-sdk) and [`@turnkey/core`](/sdks/typescript-sdk) handles the full flow. It initializes the ingress key, verifies the enclave signature, encrypts the secret, and submits the ciphertext: ```typescript const secretId = await turnkey.apiClient().importSecret({ @@ -51,13 +51,13 @@ const secretId = await turnkey.apiClient().importSecret({ }); ``` -For sensitive material you want wiped from memory after encryption, pass a `Uint8Array` instead of a string — the SDK zeroizes the buffer once the ciphertext is produced. +For sensitive material you want wiped from memory after encryption, pass a `Uint8Array` instead of a string. The SDK zeroizes the buffer after it produces the ciphertext. Under the hood this calls [init_import_secrets](/api-reference/activities/init-import-secrets) and [import_secrets](/api-reference/activities/import-secrets). ## Exporting a secret -`exportSecret` generates the ephemeral keypair, submits the export activity, decrypts the result, and zeroizes the key — a single call when policy allows the caller to export unilaterally: +`exportSecret` generates the ephemeral keypair, submits the export activity, decrypts the result, and zeroizes the key. It is a single call when policy allows the caller to export unilaterally: ```typescript const plaintext = await turnkey.apiClient().exportSecret({ @@ -65,7 +65,7 @@ const plaintext = await turnkey.apiClient().exportSecret({ }); ``` -If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows — including multiple agent instances co-signing the same export with session keys — use the proposal sdk helpers described in [Programmable Credential Access](/solutions/key-management/programmable-credential-access). +If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows, including multiple agent instances that co-sign the same export with session keys, use the proposal SDK helpers described in [Programmable Credential Access](/solutions/key-management/programmable-credential-access). ## Listing secrets @@ -77,15 +77,15 @@ const { secrets } = await turnkey.apiClient().listSecrets({}); ## Multi-party approval -Because export is an activity, it composes with everything the policy engine supports: [consensus](/features/policies/overview) across durable users, tag-based approver requirements, and [root quorum](/features/users/root-quorum). Model browser and payment agent roles as separate Turnkey users, then use session keys to authenticate their ephemeral instances. Policy can require both roles to approve before a credit card leaves the enclave — while the payload stays encrypted to only one instance. This makes credential delegation easy to model without treating each ephemeral agent instance as a separate user. +Because export is an activity, it composes with everything the policy engine supports: [consensus](/features/policies/overview) across durable users, tag-based approver requirements, and [root quorum](/features/users/root-quorum). Model browser and payment agent roles as separate Turnkey users, then use session keys to authenticate their ephemeral instances. Policy can require both roles to approve before a credit card leaves the enclave, while the payload stays encrypted to only one instance. This makes credential delegation easy to model without treating each ephemeral agent instance as a separate user. ## Security model - **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys. -- **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key, with the organization, secret ID, and cipher suite bound into the authenticated data — ciphertext cannot be substituted across secrets or organizations. -- **Signed provenance**: every stored secret and ingress key is signed by the enclave quorum key; enclaves refuse anything they didn't produce. +- **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key, with the organization, secret ID, and cipher suite bound into the authenticated data, so no one can substitute ciphertext across secrets or organizations. +- **Signed provenance**: the enclave quorum key signs every stored secret and ingress key. Enclaves refuse anything they didn't produce. - **Forward secrecy**: ingress and egress target keys are single-use. Compromising one exposes at most one payload. -- **Full auditability**: every import, export, and approval is an activity — attributable to the authenticating credential, logged, and queryable. +- **Full auditability**: every import, export, and approval is an activity that is attributed to the authenticating credential, logged, and queryable. ## Next steps diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx index 3973c670..5e102bc6 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -11,7 +11,7 @@ import { SolutionCard } from '/snippets/solution-card.mdx' **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. -An exchange API key is not just another config value. A key that can place orders, cancel orders, or move funds is a direct line to your balance sheet, and it usually sits in an environment variable, a config file, or a vault that decrypts inside the very infrastructure you are trying to protect. If your infrastructure is compromised, the key is too. +An exchange API key that can place orders, cancel orders, or move funds is a direct line to your balance sheet. It usually sits in an environment variable, a config file, or a vault that decrypts inside the infrastructure you are trying to protect. An attacker who compromises that infrastructure gets the key too. Turnkey removes the plaintext from your infrastructure entirely. The credential lives encrypted inside a [secure enclave](/security/secure-enclaves), and every retrieval is evaluated by the [policy engine](/features/policies/overview) before the enclave releases anything. You decide which service identities can retrieve which keys, under what conditions, and with how many approvals. This solution builds on [Secret Storage](/features/secrets). @@ -26,7 +26,7 @@ Typical credentials this pattern protects: | | Vault in your infrastructure | Turnkey | | :--- | :--- | :--- | -| Where plaintext exists | Decrypted inside your infrastructure; a vault admin or a compromised host can read it | Only inside the enclave and on the single authorized recipient | +| Where plaintext exists | Decrypted inside your infrastructure, where a vault admin or a compromised host can read it | Only inside the enclave and on the single authorized recipient | | Access control | Enforced by software you operate | Enforced by the policy engine inside the enclave, independent of your infrastructure | | Multi-party approval | Bolted on, if available | Native consensus: require m-of-n approvals before a key is released | | Who can read a released secret | Anyone who sees the response | Only the holder of the ephemeral key the payload is encrypted to, which approvers cannot read | @@ -40,9 +40,9 @@ Encrypting the credential client side and storing the ciphertext in your own dat | :--- | :--- | :--- | | **Key classification** | Bind static properties at import time (`exchange`, `permissions`, `environment`, ...) so policies target classes of keys, such as all withdrawal-capable keys, instead of individual IDs. | [Secret Storage](/features/secrets) | | **Service identity** | Model each trading service or environment as a Turnkey user with its own API key or session keys, so retrieval permission is scoped per service. | [Sessions](/features/authentication/sessions) | -| **Approval requirements** | Trade-only keys can allow unilateral retrieval by the trading service; withdrawal-capable or production keys can require human or multi-party approval. | [Policy Engine](/features/policies/overview) | +| **Approval requirements** | Trade-only keys can allow unilateral retrieval by the trading service. Withdrawal-capable or production keys can require human or multi-party approval. | [Policy Engine](/features/policies/overview) | | **Recipient control** | The export payload is encrypted to a single ephemeral public key, so only the service that generated it can read the credential, even when other parties approve. | | -| **Rotation and revocation** | Rotate by importing the new key and deleting the old one; revoke a service's access instantly by updating policy or removing its credentials. | | +| **Rotation and revocation** | Rotate by importing the new key and deleting the old one. Revoke a service's access instantly by updating policy or removing its credentials. | | ## Example: exchange trading key for a trading firm @@ -50,7 +50,7 @@ A trading firm holds a long-lived JWT for an execution management system that au | Need | How Turnkey solves it | | :--- | :--- | -| Plaintext never lives in the firm's infrastructure | The JWT is imported once over an end-to-end encrypted channel and stored inside the enclave; retrieval re-encrypts it to a single ephemeral key | +| Plaintext never lives in the firm's infrastructure | The firm imports the JWT once over an end-to-end encrypted channel, and it stays inside the enclave. Retrieval re-encrypts it to a single ephemeral key | | Only the trading service can retrieve it | Policy scopes retrieval of `kind == 'exchangeApiKey'` secrets to the trading service user | | High-risk keys need oversight | A consensus policy requires human approval before any withdrawal-capable key is released | | Every access is attributable | Each retrieval and approval is a signed activity, logged and queryable | @@ -70,7 +70,7 @@ A trading firm holds a long-lived JWT for an execution management system that au - Import the credential once, with static properties the policies above target. Plaintext is encrypted to the enclave on the client; Turnkey's API and database only ever see ciphertext: + Import the credential once, with static properties the policies above target. The client encrypts the plaintext to the enclave. Turnkey's API and database only ever see ciphertext: ```typescript const secretId = await turnkey.apiClient().importSecret({ @@ -84,7 +84,7 @@ A trading firm holds a long-lived JWT for an execution management system that au }); ``` - The plaintext can now be deleted from wherever it existed before. + You can now delete the plaintext from wherever it existed before. @@ -115,7 +115,7 @@ A trading firm holds a long-lived JWT for an execution management system that au - Rotate by importing the replacement key and deleting the old secret. Revoke a service by invalidating its session keys or removing its user; policy changes take effect immediately, with no re-encryption or re-import of the stored keys. + Rotate by importing the replacement key and deleting the old secret. Revoke a service by invalidating its session keys or removing its user. Policy changes take effect immediately, with no re-encryption or re-import of the stored keys. @@ -123,7 +123,7 @@ A trading firm holds a long-lived JWT for an execution management system that au Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives protect API keys: -- 🔐 **End-to-end encryption**: plaintext exists only in the enclave and on the single authorized recipient; approvers see ciphertext they cannot read. +- 🔐 **End-to-end encryption**: plaintext exists only in the enclave and on the single authorized recipient. Approvers see ciphertext they cannot read. - 📜 **Policy engine**: gate retrieval on identity, static properties, approval counts, and tags. - 🔄 **Dynamic policies**: widen or narrow access at runtime without re-importing anything. - 📋 **Fully auditable**: every import, retrieval, and approval is a signed, queryable activity. diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index b8549ead..5441c22b 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -11,7 +11,7 @@ import { SolutionCard } from '/snippets/solution-card.mdx' **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. -When an AI agent needs a password, a card number, or an API key, teams face an unpleasant choice: hand the agent unfettered access to credentials, or put a human in the loop for every single request. Turnkey gives you a third option — a programmable access layer where every credential request is evaluated against policies you control, inside a [secure enclave](/security/secure-enclaves) that never releases plaintext unless the policy allows it. +When an AI agent needs a password, a card number, or an API key, teams face an unpleasant choice: hand the agent unfettered access to credentials, or put a human in the loop for every single request. Turnkey gives you a third option: a programmable access layer that evaluates every credential request against policies you control, inside a [secure enclave](/security/secure-enclaves) that never releases plaintext unless the policy allows it. Policies, metadata, and tags are fully dynamic: update them at runtime to widen or narrow access on the fly, without re-importing anything. This solution builds on [Secret Storage](/features/secrets). @@ -29,21 +29,21 @@ One policy engine supports the full spectrum of trust models: | Decision | What to consider | Learn more | | :--- | :--- | :--- | -| **Secret classification** | Bind static properties at import time (`kind`, `environment`, `requiresConsensus`, ...) so policies target classes of secrets, not IDs. | [Secret Storage](/features/secrets) | +| **Secret classification** | Bind static properties at import time (`kind`, `environment`, `requiresConsensus`, ...) so policies target classes of secrets instead of individual IDs. | [Secret Storage](/features/secrets) | | **Agent identity** | Model each agent type or role as a durable Turnkey user. A scoped, expiring session key authenticates each ephemeral instance of that user, making credential delegation easy to express in policy. | [Sessions](/features/authentication/sessions) | | **Consensus requirements** | Decide which secret classes need one agent role, several, or a specific combination of user tags (e.g. one `browser-agent` *and* one `payment-agent`). | [Policy Engine](/features/policies/overview) | -| **Recipient control** | The export payload is encrypted to a single ephemeral public key. Decide which party generates that key — that party, and only that party, can read the secret. | | +| **Recipient control** | The export payload is encrypted to a single ephemeral public key. Decide which party generates that key. That party, and only that party, can read the secret. | | | **Revocation** | Invalidate a session key to revoke one agent instance, or delete the agent-role user to revoke every instance of that role. | [Sessions](/features/authentication/sessions) | ## Example: multi-agent consensus for payments -A browser agent and a payment agent are modeled as durable Turnkey users, with session keys authenticating their ephemeral instances. Policy requires both agent roles to approve before card details are exported, and only the payment agent instance holds the decryption key — so neither instance can act alone, and the browser agent never sees the card. +Model a browser agent and a payment agent as durable Turnkey users, with session keys that authenticate their ephemeral instances. Policy requires both agent roles to approve before the enclave exports card details, and only the payment agent instance holds the decryption key. Neither instance can act alone, and the browser agent never sees the card. | Need | How Turnkey solves it | | :--- | :--- | | No single agent instance can exfiltrate the card | Consensus policy requires approvals from both durable agent-role users before the enclave re-encrypts the secret | | Approver ≠ recipient | The payload is encrypted to the payment agent instance's ephemeral key; the browser agent instance's approval releases a ciphertext it cannot read | -| Agent instances act in parallel, not lockstep | Both instances sign and submit the byte-identical export request in any order; Turnkey matches them to the same activity | +| Agent instances act in parallel | Both instances sign and submit the byte-identical export request in any order. Turnkey matches them to the same activity | | Every access is attributable | Each export and approval is signed with a session key and logged under the durable agent-role user | ### Policy: require two agent roles for credit card access @@ -61,7 +61,7 @@ A browser agent and a payment agent are modeled as durable Turnkey users, with s - Import the card once, with static properties that the policy above targets. Any client with import permission can do this — here, a backend service: + Import the card once, with static properties that the policy above targets. Any client with import permission can do this. Here, a backend service imports it: ```typescript const secretId = await turnkey.apiClient().importSecret({ @@ -76,7 +76,7 @@ A browser agent and a payment agent are modeled as durable Turnkey users, with s - The payment agent instance — the intended recipient — generates an ephemeral keypair and builds the proposal. `createExportSecretsProposal` is a local call: it produces the canonical request body and its fingerprint, with no network round trip. + The payment agent instance, the intended recipient, generates an ephemeral keypair and builds the proposal. `createExportSecretsProposal` is a local call: it produces the canonical request body and its fingerprint, with no network round trip. ```typescript import { generateP256KeyPair } from "@turnkey/crypto"; @@ -90,10 +90,10 @@ A browser agent and a payment agent are modeled as durable Turnkey users, with s }); ``` - The proposal is plain JSON and contains no key material — share it with co-signing agent instances over any channel. + The proposal is plain JSON and contains no key material. Share it with co-signing agent instances over any channel. - + Each agent instance stamps the identical proposal body with the session key for its durable agent-role user and submits. Order doesn't matter: the first submission creates the activity, and every subsequent identical submission counts as an approval. ```typescript @@ -128,10 +128,10 @@ A browser agent and a payment agent are modeled as durable Turnkey users, with s Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives underpin credential management: -- 📜 **Policy engine** — gate on identity, approval count, tags, static properties, and more. -- 🔄 **Dynamic policies** — policies and tags are mutable; widen or narrow access programmatically, at runtime. -- 🔐 **End-to-end encryption** — plaintext exists only in the enclave and on the recipient's client; approvers see ciphertext they cannot read. -- 📋 **Fully auditable** — every request and approval is logged under the durable agent-role user and authenticating session key. +- 📜 **Policy engine**: gate on identity, approval count, tags, static properties, and more. +- 🔄 **Dynamic policies**: policies and tags are mutable. Widen or narrow access programmatically at runtime. +- 🔐 **End-to-end encryption**: plaintext exists only in the enclave and on the recipient's client. Approvers see ciphertext they cannot read. +- 📋 **Fully auditable**: every request and approval is logged under the durable agent-role user and authenticating session key. ## Next steps From f411d24f3c4f31185bab796a15e194310689501b Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:34:16 -0400 Subject: [PATCH 05/29] Remove client-side encryption comparison paragraph --- solutions/key-management/high-security-api-key-storage.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx index 5e102bc6..cbd6eb7e 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -32,8 +32,6 @@ Typical credentials this pattern protects: | Who can read a released secret | Anyone who sees the response | Only the holder of the ephemeral key the payload is encrypted to, which approvers cannot read | | Audit trail | Vault logs you maintain | Every retrieval and approval is a signed, attributable Turnkey activity | -Encrypting the credential client side and storing the ciphertext in your own database has the same weakness: the ciphertext, and eventually the plaintext, still transits infrastructure you have to defend. With Turnkey the credential enters the enclave once and only comes out policy-gated, encrypted to a single recipient. - ## Key implementation decisions | Decision | What to consider | Learn more | From b9a5043db8380b375d911bbae77c4228a2b72895 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:35:28 -0400 Subject: [PATCH 06/29] Reword High Security API Key Storage description --- features/secrets.mdx | 2 +- solutions/key-management/high-security-api-key-storage.mdx | 2 +- solutions/key-management/overview.mdx | 2 +- solutions/key-management/programmable-credential-access.mdx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/secrets.mdx b/features/secrets.mdx index 50bb49db..347596e6 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -91,6 +91,6 @@ Because export is an activity, it composes with everything the policy engine sup
- +
diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx index cbd6eb7e..d26ec7d8 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -1,6 +1,6 @@ --- title: "High Security API Key Storage" -description: "Custody exchange keys, trading credentials, and bearer tokens in secure enclaves, so plaintext never lives in your infrastructure." +description: "Programmatically store and gate access to your most sensitive API keys." tag: "Beta" --- diff --git a/solutions/key-management/overview.mdx b/solutions/key-management/overview.mdx index e328d4b1..f343a722 100644 --- a/solutions/key-management/overview.mdx +++ b/solutions/key-management/overview.mdx @@ -78,7 +78,7 @@ Key management serves different needs depending on how your application uses cry title="High Security API Key Storage" icon="encryption-key-storage" href="/solutions/key-management/high-security-api-key-storage" - description="Custody exchange keys, trading credentials, and bearer tokens in secure enclaves, with policy-gated retrieval and multi-party approval." + description="Programmatically store and gate access to your most sensitive API keys." /> diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index 5441c22b..ee89d754 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -139,6 +139,6 @@ Turnkey is a signing and encryption platform running inside secure enclaves, ori - + From 296a96bfcf0ad5191303392f59adba398c5aa9b0 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:42:20 -0400 Subject: [PATCH 07/29] Add Secret storage link to front page features section --- welcome.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/welcome.mdx b/welcome.mdx index af8d8010..7a4b0b2b 100644 --- a/welcome.mdx +++ b/welcome.mdx @@ -372,6 +372,18 @@ mode: "custom" Policy engine + +
+ + + + + + + + + Secret storage + From ec26bc8f82ab01288b01344f8b5612af5631e3fa Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:42:47 -0400 Subject: [PATCH 08/29] Add Programmable Credential Access link to Key Management solution card --- welcome.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/welcome.mdx b/welcome.mdx index 7a4b0b2b..4c36204e 100644 --- a/welcome.mdx +++ b/welcome.mdx @@ -268,6 +268,10 @@ mode: "custom" Enterprise Disaster Recovery + + + Programmable Credential Access + From 66e1c6e4d0600101ff41838b7ea1bc1d1ce3286c Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:43:32 -0400 Subject: [PATCH 09/29] Add new secrets solutions to Key Management section of solutions overview --- solutions/overview.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/solutions/overview.mdx b/solutions/overview.mdx index 4724bc32..28e0ece0 100644 --- a/solutions/overview.mdx +++ b/solutions/overview.mdx @@ -82,4 +82,16 @@ Enterprise-grade security for your most sensitive keys — hardware-backed with href="/solutions/key-management/enterprise-disaster-recovery" description="Non-custodial wallet recovery with instant policy enforcement." /> + + From 481ba84b3bb799ca77dc3a9e88c3aeaf3757daa6 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:44:53 -0400 Subject: [PATCH 10/29] Trim next steps on new solution pages to Secret Storage and Policy Engine --- solutions/key-management/high-security-api-key-storage.mdx | 3 --- solutions/key-management/programmable-credential-access.mdx | 4 ---- 2 files changed, 7 deletions(-) diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx index d26ec7d8..0b8493a3 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -5,7 +5,6 @@ tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' -import { SolutionCard } from '/snippets/solution-card.mdx' **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. @@ -131,6 +130,4 @@ Turnkey is a signing and encryption platform running inside secure enclaves, ori
- -
diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index ee89d754..4a5c4bb1 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -5,7 +5,6 @@ tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' -import { SolutionCard } from '/snippets/solution-card.mdx' **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. @@ -138,7 +137,4 @@ Turnkey is a signing and encryption platform running inside secure enclaves, ori
- - -
From a58136f3607a13fe0a2eee8727e3904e28f73bfe Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:46:46 -0400 Subject: [PATCH 11/29] Add bespoke icons for new solution pages and quantum resistance note to secrets page --- features/secrets.mdx | 5 +++-- .../solutions/dark/high-security-api-key-storage.svg | 8 ++++++++ .../solutions/dark/programmable-credential-access.svg | 10 ++++++++++ .../solutions/light/high-security-api-key-storage.svg | 8 ++++++++ .../solutions/light/programmable-credential-access.svg | 10 ++++++++++ solutions/key-management/overview.mdx | 4 ++-- solutions/overview.mdx | 4 ++-- 7 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 images/solutions/dark/high-security-api-key-storage.svg create mode 100644 images/solutions/dark/programmable-credential-access.svg create mode 100644 images/solutions/light/high-security-api-key-storage.svg create mode 100644 images/solutions/light/programmable-credential-access.svg diff --git a/features/secrets.mdx b/features/secrets.mdx index 347596e6..b08ba4d3 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -84,13 +84,14 @@ Because export is an activity, it composes with everything the policy engine sup - **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys. - **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key, with the organization, secret ID, and cipher suite bound into the authenticated data, so no one can substitute ciphertext across secrets or organizations. - **Signed provenance**: the enclave quorum key signs every stored secret and ingress key. Enclaves refuse anything they didn't produce. +- **Quantum resistant internally, agile in transit**: secrets rest under AES-256-GCM, a quantum resistant cipher. The cipher suite is versioned into every payload, so transport protocols can be upgraded without re-encrypting stored secrets. - **Forward secrecy**: ingress and egress target keys are single-use. Compromising one exposes at most one payload. - **Full auditability**: every import, export, and approval is an activity that is attributed to the authenticating credential, logged, and queryable. ## Next steps
- - + +
diff --git a/images/solutions/dark/high-security-api-key-storage.svg b/images/solutions/dark/high-security-api-key-storage.svg new file mode 100644 index 00000000..867c3f5f --- /dev/null +++ b/images/solutions/dark/high-security-api-key-storage.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/images/solutions/dark/programmable-credential-access.svg b/images/solutions/dark/programmable-credential-access.svg new file mode 100644 index 00000000..d4360217 --- /dev/null +++ b/images/solutions/dark/programmable-credential-access.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/images/solutions/light/high-security-api-key-storage.svg b/images/solutions/light/high-security-api-key-storage.svg new file mode 100644 index 00000000..a9e89a85 --- /dev/null +++ b/images/solutions/light/high-security-api-key-storage.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/images/solutions/light/programmable-credential-access.svg b/images/solutions/light/programmable-credential-access.svg new file mode 100644 index 00000000..f28bacc1 --- /dev/null +++ b/images/solutions/light/programmable-credential-access.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/solutions/key-management/overview.mdx b/solutions/key-management/overview.mdx index f343a722..dac42781 100644 --- a/solutions/key-management/overview.mdx +++ b/solutions/key-management/overview.mdx @@ -70,13 +70,13 @@ Key management serves different needs depending on how your application uses cry /> diff --git a/solutions/overview.mdx b/solutions/overview.mdx index 28e0ece0..d3664ff1 100644 --- a/solutions/overview.mdx +++ b/solutions/overview.mdx @@ -84,13 +84,13 @@ Enterprise-grade security for your most sensitive keys — hardware-backed with /> From 290cfe3a9f8adc216b9fc48f724895f3f337fe13 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:49:16 -0400 Subject: [PATCH 12/29] Correct cipher suite agility claim on secrets page --- features/secrets.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/secrets.mdx b/features/secrets.mdx index b08ba4d3..fcee5a5f 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -84,7 +84,7 @@ Because export is an activity, it composes with everything the policy engine sup - **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys. - **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key, with the organization, secret ID, and cipher suite bound into the authenticated data, so no one can substitute ciphertext across secrets or organizations. - **Signed provenance**: the enclave quorum key signs every stored secret and ingress key. Enclaves refuse anything they didn't produce. -- **Quantum resistant internally, agile in transit**: secrets rest under AES-256-GCM, a quantum resistant cipher. The cipher suite is versioned into every payload, so transport protocols can be upgraded without re-encrypting stored secrets. +- **Quantum resistant internally, agile in transit**: secrets rest under AES-256-GCM, a quantum resistant cipher. The transport cipher suite is a field in import and export requests, designed to be extended over time, so Turnkey can adopt new transport protocols as they mature. - **Forward secrecy**: ingress and egress target keys are single-use. Compromising one exposes at most one payload. - **Full auditability**: every import, export, and approval is an activity that is attributed to the authenticating credential, logged, and queryable. From 513d6618ec0f4660c965ed39a363ded530c46485 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:51:37 -0400 Subject: [PATCH 13/29] Remove emojis from solution pages --- .../high-security-api-key-storage.mdx | 8 ++++---- .../programmable-credential-access.mdx | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx index 0b8493a3..c77ae768 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -120,10 +120,10 @@ A trading firm holds a long-lived JWT for an execution management system that au Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives protect API keys: -- 🔐 **End-to-end encryption**: plaintext exists only in the enclave and on the single authorized recipient. Approvers see ciphertext they cannot read. -- 📜 **Policy engine**: gate retrieval on identity, static properties, approval counts, and tags. -- 🔄 **Dynamic policies**: widen or narrow access at runtime without re-importing anything. -- 📋 **Fully auditable**: every import, retrieval, and approval is a signed, queryable activity. +- **End-to-end encryption**: plaintext exists only in the enclave and on the single authorized recipient. Approvers see ciphertext they cannot read. +- **Policy engine**: gate retrieval on identity, static properties, approval counts, and tags. +- **Dynamic policies**: widen or narrow access at runtime without re-importing anything. +- **Fully auditable**: every import, retrieval, and approval is a signed, queryable activity. ## Next steps diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index 4a5c4bb1..8f7dc5ff 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -20,9 +20,9 @@ One policy engine supports the full spectrum of trust models: | Pattern | How it works | | :--- | :--- | -| 🤖 **Unilateral agent** | A session key authenticates an ephemeral agent instance as a durable agent-role user whose policy permits direct export. | -| 🤖 → 🧑 **Agent → human** | An agent instance requests access; the export completes only after a human approves the pending activity. | -| 🤖 🤝 🤖 **Multi-agent consensus** | Instances of separate agent-role users must each sign the same export before the secret is released, and only the designated recipient can decrypt it. | +| **Unilateral agent** | A session key authenticates an ephemeral agent instance as a durable agent-role user whose policy permits direct export. | +| **Agent → human** | An agent instance requests access; the export completes only after a human approves the pending activity. | +| **Multi-agent consensus** | Instances of separate agent-role users must each sign the same export before the secret is released, and only the designated recipient can decrypt it. | ## Key implementation decisions @@ -127,10 +127,10 @@ Model a browser agent and a payment agent as durable Turnkey users, with session Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives underpin credential management: -- 📜 **Policy engine**: gate on identity, approval count, tags, static properties, and more. -- 🔄 **Dynamic policies**: policies and tags are mutable. Widen or narrow access programmatically at runtime. -- 🔐 **End-to-end encryption**: plaintext exists only in the enclave and on the recipient's client. Approvers see ciphertext they cannot read. -- 📋 **Fully auditable**: every request and approval is logged under the durable agent-role user and authenticating session key. +- **Policy engine**: gate on identity, approval count, tags, static properties, and more. +- **Dynamic policies**: policies and tags are mutable. Widen or narrow access programmatically at runtime. +- **End-to-end encryption**: plaintext exists only in the enclave and on the recipient's client. Approvers see ciphertext they cannot read. +- **Fully auditable**: every request and approval is logged under the durable agent-role user and authenticating session key. ## Next steps From a465f646eb6201d2185ef124e29262ebcc642701 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:52:10 -0400 Subject: [PATCH 14/29] Move Secret storage link to Manage wallets and keys column --- welcome.mdx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/welcome.mdx b/welcome.mdx index 4c36204e..d8c49e03 100644 --- a/welcome.mdx +++ b/welcome.mdx @@ -334,6 +334,17 @@ mode: "custom" Claim links +
+ + + + + + + + + Secret storage + @@ -377,17 +388,6 @@ mode: "custom" Policy engine -
- - - - - - - - - Secret storage - From 0aa3c61880e03ddeff688254e0e59c69a8d32316 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 12:59:19 -0400 Subject: [PATCH 15/29] Remove Under the hood sections from solution pages --- .../key-management/high-security-api-key-storage.mdx | 9 --------- .../key-management/programmable-credential-access.mdx | 9 --------- 2 files changed, 18 deletions(-) diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/high-security-api-key-storage.mdx index c77ae768..5003e980 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/high-security-api-key-storage.mdx @@ -116,15 +116,6 @@ A trading firm holds a long-lived JWT for an execution management system that au
-## Under the hood - -Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives protect API keys: - -- **End-to-end encryption**: plaintext exists only in the enclave and on the single authorized recipient. Approvers see ciphertext they cannot read. -- **Policy engine**: gate retrieval on identity, static properties, approval counts, and tags. -- **Dynamic policies**: widen or narrow access at runtime without re-importing anything. -- **Fully auditable**: every import, retrieval, and approval is a signed, queryable activity. - ## Next steps
diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index 8f7dc5ff..483dc293 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -123,15 +123,6 @@ Model a browser agent and a payment agent as durable Turnkey users, with session For direct human approval instead of a second agent role, skip the co-signing step: the activity stays in `CONSENSUS_NEEDED` until the human approves it from the dashboard or via [approve_activity](/api-reference/activities/approve-activity). -## Under the hood - -Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. The same primitives underpin credential management: - -- **Policy engine**: gate on identity, approval count, tags, static properties, and more. -- **Dynamic policies**: policies and tags are mutable. Widen or narrow access programmatically at runtime. -- **End-to-end encryption**: plaintext exists only in the enclave and on the recipient's client. Approvers see ciphertext they cannot read. -- **Fully auditable**: every request and approval is logged under the durable agent-role user and authenticating session key. - ## Next steps
From babf2dac1e1eea184cac562531ee2fdd038f17f7 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 13:00:55 -0400 Subject: [PATCH 16/29] Add Under the hood section to secrets feature page --- features/secrets.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/features/secrets.mdx b/features/secrets.mdx index fcee5a5f..73b6aef6 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -79,6 +79,14 @@ const { secrets } = await turnkey.apiClient().listSecrets({}); Because export is an activity, it composes with everything the policy engine supports: [consensus](/features/policies/overview) across durable users, tag-based approver requirements, and [root quorum](/features/users/root-quorum). Model browser and payment agent roles as separate Turnkey users, then use session keys to authenticate their ephemeral instances. Policy can require both roles to approve before a credit card leaves the enclave, while the payload stays encrypted to only one instance. This makes credential delegation easy to model without treating each ephemeral agent instance as a separate user. +## Under the hood + +Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. Secret storage is built from the same primitives: + +- **Secure enclaves**: import, storage, and export all execute inside hardware-isolated enclaves, on the same infrastructure that runs Turnkey's own key management. +- **Policy engine**: gate export on identity, approval counts, tags, and static properties. +- **Dynamic policies**: policies and tags are mutable. Widen or narrow access programmatically at runtime, without re-importing secrets. + ## Security model - **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys. From ab70221badecdbc942ad964189927605e5dbba0f Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 13:01:38 -0400 Subject: [PATCH 17/29] Fold platform framing into Security model and drop Under the hood --- features/secrets.mdx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/features/secrets.mdx b/features/secrets.mdx index 73b6aef6..d24d949c 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -79,16 +79,10 @@ const { secrets } = await turnkey.apiClient().listSecrets({}); Because export is an activity, it composes with everything the policy engine supports: [consensus](/features/policies/overview) across durable users, tag-based approver requirements, and [root quorum](/features/users/root-quorum). Model browser and payment agent roles as separate Turnkey users, then use session keys to authenticate their ephemeral instances. Policy can require both roles to approve before a credit card leaves the enclave, while the payload stays encrypted to only one instance. This makes credential delegation easy to model without treating each ephemeral agent instance as a separate user. -## Under the hood +## Security model Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. Secret storage is built from the same primitives: -- **Secure enclaves**: import, storage, and export all execute inside hardware-isolated enclaves, on the same infrastructure that runs Turnkey's own key management. -- **Policy engine**: gate export on identity, approval counts, tags, and static properties. -- **Dynamic policies**: policies and tags are mutable. Widen or narrow access programmatically at runtime, without re-importing secrets. - -## Security model - - **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys. - **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key, with the organization, secret ID, and cipher suite bound into the authenticated data, so no one can substitute ciphertext across secrets or organizations. - **Signed provenance**: the enclave quorum key signs every stored secret and ingress key. Enclaves refuse anything they didn't produce. From 8c0b1a6f0bdec923a18c95195a316c3c4d8c1896 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 13:02:09 -0400 Subject: [PATCH 18/29] Say only policies and tags are mutable --- solutions/key-management/programmable-credential-access.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index 483dc293..dba8067e 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -12,7 +12,7 @@ import { FeatureCard } from '/snippets/feature-card.mdx' When an AI agent needs a password, a card number, or an API key, teams face an unpleasant choice: hand the agent unfettered access to credentials, or put a human in the loop for every single request. Turnkey gives you a third option: a programmable access layer that evaluates every credential request against policies you control, inside a [secure enclave](/security/secure-enclaves) that never releases plaintext unless the policy allows it. -Policies, metadata, and tags are fully dynamic: update them at runtime to widen or narrow access on the fly, without re-importing anything. This solution builds on [Secret Storage](/features/secrets). +Policies and tags are fully dynamic: update them at runtime to widen or narrow access on the fly, without re-importing anything. This solution builds on [Secret Storage](/features/secrets). ## Access patterns From 7bc093608d34d23071dff931a2c9771efb1ae53c Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 18:15:55 -0400 Subject: [PATCH 19/29] Apply review feedback: sentence case titles, rename API key storage, shared beta callout --- docs.json | 2 +- features/secrets.mdx | 23 ++++++++++--------- ...pi-key-storage.svg => api-key-storage.svg} | 0 ...pi-key-storage.svg => api-key-storage.svg} | 0 snippets/secrets-beta-callout.mdx | 7 ++++++ ...pi-key-storage.mdx => api-key-storage.mdx} | 23 +++++++++++-------- solutions/key-management/overview.mdx | 8 +++---- .../programmable-credential-access.mdx | 17 +++++++------- solutions/overview.mdx | 8 +++---- welcome.mdx | 2 +- 10 files changed, 50 insertions(+), 40 deletions(-) rename images/solutions/dark/{high-security-api-key-storage.svg => api-key-storage.svg} (100%) rename images/solutions/light/{high-security-api-key-storage.svg => api-key-storage.svg} (100%) create mode 100644 snippets/secrets-beta-callout.mdx rename solutions/key-management/{high-security-api-key-storage.mdx => api-key-storage.mdx} (82%) diff --git a/docs.json b/docs.json index 7f57e7cf..fbf2ee07 100644 --- a/docs.json +++ b/docs.json @@ -227,7 +227,7 @@ "solutions/key-management/encryption-key-storage", "solutions/key-management/enterprise-disaster-recovery", "solutions/key-management/programmable-credential-access", - "solutions/key-management/high-security-api-key-storage" + "solutions/key-management/api-key-storage" ] } ] diff --git a/features/secrets.mdx b/features/secrets.mdx index d24d949c..71d1fb75 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -1,15 +1,14 @@ --- -title: "Secret Storage" +title: "Secret storage" description: "Import, store, and export arbitrary secrets (passwords, credit cards, API keys) with policy-gated, end-to-end encrypted access." tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' import { SolutionCard } from '/snippets/solution-card.mdx' +import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' - - **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. - + Turnkey Secrets lets you store arbitrary sensitive data (passwords, credit card details, API keys, SSNs) encrypted end-to-end between your client and Turnkey's [secure enclaves](/security/secure-enclaves). The [policy engine](/features/policies/overview) evaluates every export, so you control exactly who can retrieve a secret, under what conditions, and with how many approvals. We designed the secret storage API for flexibility and programmability. @@ -17,11 +16,13 @@ Plaintext only ever exists inside the enclave and on the client that imported or ## How it works -**Import**: Turnkey mints a single-use ingress target key inside the enclave, signed by the enclave's quorum key. Your client verifies that signature, encrypts the secret to the target key using HPKE, and submits the ciphertext. The enclave decrypts it, re-encrypts it for storage at rest under a quorum-key-derived key, and deletes the ingress key. +The Secrets methods use the same enclave secure-channel pattern as [wallet import](/features/wallets/import-wallets) and [wallet export](/features/wallets/export-wallets): every transfer is HPKE-encrypted to a single-use target key, so plaintext appears only inside the enclave and on the client holding the matching private key. See [Enclave secure channels](/security/enclave-secure-channels) for the canonical protocol details. -**Export**: the export request carries an ephemeral P-256 target public key, and after policy evaluation approves the request, the enclave decrypts the stored secret and re-encrypts it to that key. The target key is fully configurable: it can belong to the requester, to another agent or service, or to a party that isn't an approver at all. Only the holder of the private half can decrypt the result. The export payload is useless to anyone else, including the approvers themselves. +What is specific to Secrets: -For a batch export, the request succeeds only if every policy evaluation returns `ALLOW`. A `DENY`, or any evaluation without an `ALLOW` outcome, rejects the entire batch. Turnkey never exports part of a batch. +- **Import**: your client encrypts the secret to a single-use ingress target key minted inside the enclave and submits only ciphertext. The enclave re-encrypts it for storage at rest and deletes the ingress key. +- **Export**: the export request carries an ephemeral P-256 target public key. After policy evaluation approves the request, the enclave re-encrypts the secret to that key. The recipient key is fully configurable: it can belong to the requester, to another agent or service, or to a party that isn't an approver at all. Only the holder of the matching private key can decrypt the result; the payload is useless to anyone else, including the approvers themselves. +- **Batch export is all-or-nothing**: the request succeeds only if every policy evaluation returns `ALLOW`. A `DENY`, or any evaluation without an `ALLOW` outcome, rejects the entire batch. Turnkey fails closed and never exports part of a batch. ## Static properties @@ -32,7 +33,7 @@ Secrets are created with optional **static properties**: string key-value pairs "policyName": "Only the payments agent can export credit cards", "effect": "EFFECT_ALLOW", "consensus": "approvers.any(u, u.tags.contains('payment-agent'))", - "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['kind'] == 'creditCard'" + "condition": "secret.static_properties['kind'] == 'creditCard' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'" } ``` @@ -65,7 +66,7 @@ const plaintext = await turnkey.apiClient().exportSecret({ }); ``` -If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows, including multiple agent instances that co-sign the same export with session keys, use the proposal SDK helpers described in [Programmable Credential Access](/solutions/key-management/programmable-credential-access). +If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows, including multiple agent instances that co-sign the same export with session keys, use the proposal SDK helpers described in [Programmable credential access](/solutions/key-management/programmable-credential-access). ## Listing secrets @@ -93,7 +94,7 @@ Turnkey is a signing and encryption platform running inside secure enclaves, ori ## Next steps
- - + +
diff --git a/images/solutions/dark/high-security-api-key-storage.svg b/images/solutions/dark/api-key-storage.svg similarity index 100% rename from images/solutions/dark/high-security-api-key-storage.svg rename to images/solutions/dark/api-key-storage.svg diff --git a/images/solutions/light/high-security-api-key-storage.svg b/images/solutions/light/api-key-storage.svg similarity index 100% rename from images/solutions/light/high-security-api-key-storage.svg rename to images/solutions/light/api-key-storage.svg diff --git a/snippets/secrets-beta-callout.mdx b/snippets/secrets-beta-callout.mdx new file mode 100644 index 00000000..cac8c31f --- /dev/null +++ b/snippets/secrets-beta-callout.mdx @@ -0,0 +1,7 @@ +export const SecretsBetaCallout = () => ( + + The Secrets API is currently in closed beta.{" "} + Contact us to get + onboarded. + +); diff --git a/solutions/key-management/high-security-api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx similarity index 82% rename from solutions/key-management/high-security-api-key-storage.mdx rename to solutions/key-management/api-key-storage.mdx index 5003e980..d0c28072 100644 --- a/solutions/key-management/high-security-api-key-storage.mdx +++ b/solutions/key-management/api-key-storage.mdx @@ -1,18 +1,17 @@ --- -title: "High Security API Key Storage" +title: "API key storage" description: "Programmatically store and gate access to your most sensitive API keys." tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' +import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' - - **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. - + An exchange API key that can place orders, cancel orders, or move funds is a direct line to your balance sheet. It usually sits in an environment variable, a config file, or a vault that decrypts inside the infrastructure you are trying to protect. An attacker who compromises that infrastructure gets the key too. -Turnkey removes the plaintext from your infrastructure entirely. The credential lives encrypted inside a [secure enclave](/security/secure-enclaves), and every retrieval is evaluated by the [policy engine](/features/policies/overview) before the enclave releases anything. You decide which service identities can retrieve which keys, under what conditions, and with how many approvals. This solution builds on [Secret Storage](/features/secrets). +Turnkey removes the plaintext from your infrastructure entirely. The credential lives encrypted inside a [secure enclave](/security/secure-enclaves), and every retrieval is evaluated by the [policy engine](/features/policies/overview) before the enclave releases anything. You decide which service identities can retrieve which keys, under what conditions, and with how many approvals. This solution builds on [Secret storage](/features/secrets). Typical credentials this pattern protects: @@ -31,14 +30,18 @@ Typical credentials this pattern protects: | Who can read a released secret | Anyone who sees the response | Only the holder of the ephemeral key the payload is encrypted to, which approvers cannot read | | Audit trail | Vault logs you maintain | Every retrieval and approval is a signed, attributable Turnkey activity | + + Exporting a secret necessarily places plaintext on the authorized recipient while the credential is in use. For symmetric credentials (HMAC keys, bearer tokens, OAuth secrets) this is unavoidable: the service must present the secret itself. For asymmetric API credentials whose operation is signing, you can avoid the export step entirely: store the credential as a Turnkey [private key](/features/wallets#private-keys) and sign each request inside the enclave with [sign_raw_payload](/api-reference/activities/sign-raw-payload), so the recipient never receives plaintext private-key material. + + ## Key implementation decisions | Decision | What to consider | Learn more | | :--- | :--- | :--- | -| **Key classification** | Bind static properties at import time (`exchange`, `permissions`, `environment`, ...) so policies target classes of keys, such as all withdrawal-capable keys, instead of individual IDs. | [Secret Storage](/features/secrets) | +| **Key classification** | Bind static properties at import time (`exchange`, `permissions`, `environment`, ...) so policies target classes of keys, such as all withdrawal-capable keys, instead of individual IDs. | [Secret storage](/features/secrets) | | **Service identity** | Model each trading service or environment as a Turnkey user with its own API key or session keys, so retrieval permission is scoped per service. | [Sessions](/features/authentication/sessions) | | **Approval requirements** | Trade-only keys can allow unilateral retrieval by the trading service. Withdrawal-capable or production keys can require human or multi-party approval. | [Policy Engine](/features/policies/overview) | -| **Recipient control** | The export payload is encrypted to a single ephemeral public key, so only the service that generated it can read the credential, even when other parties approve. | | +| **Recipient control** | The export payload is encrypted to a single ephemeral public key, so only the service that generated it can read the credential, even when other parties approve. | [Enclave secure channels](/security/enclave-secure-channels) | | **Rotation and revocation** | Rotate by importing the new key and deleting the old one. Revoke a service's access instantly by updating policy or removing its credentials. | | ## Example: exchange trading key for a trading firm @@ -59,7 +62,7 @@ A trading firm holds a long-lived JWT for an execution management system that au "policyName": "Trading service can retrieve trade-only exchange keys", "effect": "EFFECT_ALLOW", "consensus": "approvers.any(u, u.id == '')", - "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'trade'" + "condition": "secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'trade' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'" } ``` @@ -104,7 +107,7 @@ A trading firm holds a long-lived JWT for an execution management system that au "policyName": "Withdrawal-capable keys require a human approver", "effect": "EFFECT_ALLOW", "consensus": "approvers.any(u, u.id == '') && approvers.any(u, u.tags.contains('risk-admin'))", - "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'withdraw'" + "condition": "secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'withdraw' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'" } ``` @@ -119,6 +122,6 @@ A trading firm holds a long-lived JWT for an execution management system that au ## Next steps
- +
diff --git a/solutions/key-management/overview.mdx b/solutions/key-management/overview.mdx index dac42781..9b741882 100644 --- a/solutions/key-management/overview.mdx +++ b/solutions/key-management/overview.mdx @@ -69,15 +69,15 @@ Key management serves different needs depending on how your application uses cry description="Import and recover wallets with end-to-end encryption, quorum-controlled access, and a cryptographic audit trail for treasury recovery, provider migration, and failover." />
diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index dba8067e..657e08b6 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -1,18 +1,17 @@ --- -title: "Programmable Credential Access" +title: "Programmable credential access" description: "A password manager built for machines: policy-gated, programmable access to secrets for humans, services, and AI agents." tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' +import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' - - **Closed beta**: the Secrets API is currently in closed beta. [Contact us](https://www.turnkey.com/contact-us) to get onboarded. - + When an AI agent needs a password, a card number, or an API key, teams face an unpleasant choice: hand the agent unfettered access to credentials, or put a human in the loop for every single request. Turnkey gives you a third option: a programmable access layer that evaluates every credential request against policies you control, inside a [secure enclave](/security/secure-enclaves) that never releases plaintext unless the policy allows it. -Policies and tags are fully dynamic: update them at runtime to widen or narrow access on the fly, without re-importing anything. This solution builds on [Secret Storage](/features/secrets). +Policies and tags are fully dynamic: update them at runtime to widen or narrow access on the fly, without re-importing anything. This solution builds on [Secret storage](/features/secrets). ## Access patterns @@ -28,10 +27,10 @@ One policy engine supports the full spectrum of trust models: | Decision | What to consider | Learn more | | :--- | :--- | :--- | -| **Secret classification** | Bind static properties at import time (`kind`, `environment`, `requiresConsensus`, ...) so policies target classes of secrets instead of individual IDs. | [Secret Storage](/features/secrets) | +| **Secret classification** | Bind static properties at import time (`kind`, `environment`, `requiresConsensus`, ...) so policies target classes of secrets instead of individual IDs. | [Secret storage](/features/secrets) | | **Agent identity** | Model each agent type or role as a durable Turnkey user. A scoped, expiring session key authenticates each ephemeral instance of that user, making credential delegation easy to express in policy. | [Sessions](/features/authentication/sessions) | | **Consensus requirements** | Decide which secret classes need one agent role, several, or a specific combination of user tags (e.g. one `browser-agent` *and* one `payment-agent`). | [Policy Engine](/features/policies/overview) | -| **Recipient control** | The export payload is encrypted to a single ephemeral public key. Decide which party generates that key. That party, and only that party, can read the secret. | | +| **Recipient control** | The export payload is encrypted to a single ephemeral public key. Decide which party generates that key. That party, and only that party, can read the secret. | [Enclave secure channels](/security/enclave-secure-channels) | | **Revocation** | Invalidate a session key to revoke one agent instance, or delete the agent-role user to revoke every instance of that role. | [Sessions](/features/authentication/sessions) | ## Example: multi-agent consensus for payments @@ -52,7 +51,7 @@ Model a browser agent and a payment agent as durable Turnkey users, with session "policyName": "Require two agent roles for credit card access", "effect": "EFFECT_ALLOW", "consensus": "approvers.any(u, u.tags.contains('browser-agent')) && approvers.any(u, u.tags.contains('payment-agent'))", - "condition": "activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS' && secret.static_properties['requiresConsensus'] == 'true' && secret.static_properties['kind'] == 'creditCard'" + "condition": "secret.static_properties['requiresConsensus'] == 'true' && secret.static_properties['kind'] == 'creditCard' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'" } ``` @@ -126,6 +125,6 @@ Model a browser agent and a payment agent as durable Turnkey users, with session ## Next steps
- +
diff --git a/solutions/overview.mdx b/solutions/overview.mdx index d3664ff1..f3b43a01 100644 --- a/solutions/overview.mdx +++ b/solutions/overview.mdx @@ -83,15 +83,15 @@ Enterprise-grade security for your most sensitive keys — hardware-backed with description="Non-custodial wallet recovery with instant policy enforcement." />
diff --git a/welcome.mdx b/welcome.mdx index d8c49e03..30814138 100644 --- a/welcome.mdx +++ b/welcome.mdx @@ -270,7 +270,7 @@ mode: "custom" - Programmable Credential Access + Programmable credential access From e4038150d55d1e9024e6e5cf986fae84f2d27376 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 18:19:36 -0400 Subject: [PATCH 20/29] Add MPC keyshare storage solution page --- docs.json | 3 +- features/secrets.mdx | 1 + .../solutions/dark/mpc-keyshare-storage.svg | 9 +++ .../solutions/light/mpc-keyshare-storage.svg | 9 +++ .../key-management/mpc-keyshare-storage.mdx | 78 +++++++++++++++++++ solutions/key-management/overview.mdx | 6 ++ solutions/overview.mdx | 6 ++ 7 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 images/solutions/dark/mpc-keyshare-storage.svg create mode 100644 images/solutions/light/mpc-keyshare-storage.svg create mode 100644 solutions/key-management/mpc-keyshare-storage.mdx diff --git a/docs.json b/docs.json index fbf2ee07..bbaaeec6 100644 --- a/docs.json +++ b/docs.json @@ -227,7 +227,8 @@ "solutions/key-management/encryption-key-storage", "solutions/key-management/enterprise-disaster-recovery", "solutions/key-management/programmable-credential-access", - "solutions/key-management/api-key-storage" + "solutions/key-management/api-key-storage", + "solutions/key-management/mpc-keyshare-storage" ] } ] diff --git a/features/secrets.mdx b/features/secrets.mdx index 71d1fb75..19533571 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -96,5 +96,6 @@ Turnkey is a signing and encryption platform running inside secure enclaves, ori
+
diff --git a/images/solutions/dark/mpc-keyshare-storage.svg b/images/solutions/dark/mpc-keyshare-storage.svg new file mode 100644 index 00000000..e313e2ca --- /dev/null +++ b/images/solutions/dark/mpc-keyshare-storage.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/solutions/light/mpc-keyshare-storage.svg b/images/solutions/light/mpc-keyshare-storage.svg new file mode 100644 index 00000000..2a491da0 --- /dev/null +++ b/images/solutions/light/mpc-keyshare-storage.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/solutions/key-management/mpc-keyshare-storage.mdx b/solutions/key-management/mpc-keyshare-storage.mdx new file mode 100644 index 00000000..d533584f --- /dev/null +++ b/solutions/key-management/mpc-keyshare-storage.mdx @@ -0,0 +1,78 @@ +--- +title: "MPC keyshare storage" +description: "Enclave-protected, policy-gated backup and recovery for MPC keyshare bundles, stored as opaque secrets." +tag: "Beta" +--- + +import { FeatureCard } from '/snippets/feature-card.mdx' +import { SolutionCard } from '/snippets/solution-card.mdx' +import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' + + + +Regulated custodians and institutions that sign with MPC systems distribute keyshares across parties, and typically rely on an independent backup or recovery provider so that losing one party's share does not mean losing funds. Turnkey Secrets can serve as that independent recovery store: an MPC party's keyshare bundle is imported as an opaque secret, held inside a [secure enclave](/security/secure-enclaves), and released only through a [policy-gated](/features/policies/overview) export encrypted to a designated recovery participant. You can use it instead of, or in addition to, another recovery store. For example, a custodian running an MPC-CMP based signing stack such as Fireblocks might store a party's keyshare bundle in Turnkey Secrets alongside, or instead of, a dedicated recovery service such as Coincover (illustrative ecosystem examples, not partnerships or product commitments). This solution builds on [Secret storage](/features/secrets). + +## Opaque by design + +Turnkey treats every keyshare as an opaque byte blob. Turnkey never parses, validates, interprets, or derives anything from the bundle's contents, and makes no assumptions about curve, seed format, or addresses. Whatever bytes your MPC provider exports are the bytes Turnkey stores and returns. + +This is also why storing a share with Turnkey does not change your signing trust model: an MPC shard is not a standalone private key. A single Fireblocks keyshare, for example, is useless by itself for signing; producing a signature still requires the MPC protocol and its signing quorum. Turnkey does not participate in MPC signing, and never validates or reconstructs key material. + +## What a keyshare bundle contains + +Bundle formats and versions vary across providers and protocol versions, so treat the following as indicative rather than guaranteed: + +- **ECDSA/secp256k1 (e.g. MPC-CMP)**: a party's bundle can include an additive scalar share, Paillier secret material, ring-Pedersen/Damgard-Fujisaki auxiliary parameters, a chain code, and per-party public metadata. Operational bundles are commonly single-digit kilobytes, dominated by the auxiliary material and the party count and modulus choices. +- **EdDSA/Ed25519**: bundles are substantially smaller, typically an additive or expanded scalar share, a chain code, and per-party public shares, without Paillier or ring-Pedersen material. An expanded Ed25519 scalar share has no corresponding seed and must not be forced into seed-based key formats. +- **Recovery material**: backup artifacts may be sub-kilobyte when they include only scalar shares plus chain code, but actual exported artifact sizes and formats vary by provider and version. + +Because the storage layer is opaque, none of this variation matters to Turnkey: bundles of any of these shapes are stored and returned byte-for-byte. + +## What Turnkey contributes + +| Need | How Turnkey solves it | +| :--- | :--- | +| Keyshare bundle never sits plaintext in your infrastructure | Enclave-protected opaque storage: the bundle is end-to-end encrypted into the enclave at import and only ever leaves re-encrypted to a designated recipient | +| Recovery must not be unilateral | Policy-gated export with independently controllable approvals: require consensus from risk officers or recovery operators that you manage separately from the MPC system | +| Only the recovery participant may read the released share | The export payload is encrypted to a single ephemeral public key generated by the designated recovery participant; approvers cannot read it | +| Recovery events must be auditable | Every import, export, and approval is a signed, attributable activity, logged and queryable | + +## Classifying and gating keyshares + +Bind [static properties](/features/secrets#static-properties) at import time so policies target classes of keyshares instead of individual IDs: + +```typescript +const secretId = await turnkey.apiClient().importSecret({ + plaintext: keyshareBundle, // Uint8Array, opaque to Turnkey + name: "treasury-signer-party-2-keyshare", + staticProperties: { + kind: "mpcKeyshare", + provider: "fireblocks", + curve: "secp256k1", + environment: "production", + recoveryRole: "backupParty", + }, +}); +``` + +Gate export on those properties. For example, require two recovery operators to approve before any production keyshare is released: + +```json +{ + "policyName": "Production MPC keyshares require two recovery operators", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.filter(u, u.tags.contains('recovery-operator')).count() >= 2", + "condition": "secret.static_properties['kind'] == 'mpcKeyshare' && secret.static_properties['environment'] == 'production' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'" +} +``` + +At recovery time, the designated recovery participant generates an ephemeral keypair and retrieves the bundle with `exportSecret`, or with the proposal helpers described in [Programmable credential access](/solutions/key-management/programmable-credential-access) when the export requires additional approvals. Only that participant can decrypt the released bundle, which then re-enters your MPC provider's own recovery procedure. + +## Next steps + +
+ + + + +
diff --git a/solutions/key-management/overview.mdx b/solutions/key-management/overview.mdx index 9b741882..91876bec 100644 --- a/solutions/key-management/overview.mdx +++ b/solutions/key-management/overview.mdx @@ -80,6 +80,12 @@ Key management serves different needs depending on how your application uses cry href="/solutions/key-management/api-key-storage" description="Programmatically store and gate access to your most sensitive API keys." /> + ## Ready to build? diff --git a/solutions/overview.mdx b/solutions/overview.mdx index f3b43a01..99a4ba34 100644 --- a/solutions/overview.mdx +++ b/solutions/overview.mdx @@ -94,4 +94,10 @@ Enterprise-grade security for your most sensitive keys — hardware-backed with href="/solutions/key-management/api-key-storage" description="Programmatically store and gate access to your most sensitive API keys." /> + From ee5e63e7f92a278bb042a814841e298a06a99d69 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 18:28:40 -0400 Subject: [PATCH 21/29] Clarify API key plaintext exposure --- solutions/key-management/api-key-storage.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/solutions/key-management/api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx index d0c28072..5284879e 100644 --- a/solutions/key-management/api-key-storage.mdx +++ b/solutions/key-management/api-key-storage.mdx @@ -11,7 +11,7 @@ import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' An exchange API key that can place orders, cancel orders, or move funds is a direct line to your balance sheet. It usually sits in an environment variable, a config file, or a vault that decrypts inside the infrastructure you are trying to protect. An attacker who compromises that infrastructure gets the key too. -Turnkey removes the plaintext from your infrastructure entirely. The credential lives encrypted inside a [secure enclave](/security/secure-enclaves), and every retrieval is evaluated by the [policy engine](/features/policies/overview) before the enclave releases anything. You decide which service identities can retrieve which keys, under what conditions, and with how many approvals. This solution builds on [Secret storage](/features/secrets). +Turnkey keeps plaintext credentials out of your persistent storage and infrastructure intermediaries. The credential lives encrypted inside a [secure enclave](/security/secure-enclaves), and every retrieval is evaluated by the [policy engine](/features/policies/overview) before the enclave releases anything to an authorized recipient. You decide which service identities can retrieve which keys, under what conditions, and with how many approvals. This solution builds on [Secret storage](/features/secrets). Typical credentials this pattern protects: @@ -24,7 +24,7 @@ Typical credentials this pattern protects: | | Vault in your infrastructure | Turnkey | | :--- | :--- | :--- | -| Where plaintext exists | Decrypted inside your infrastructure, where a vault admin or a compromised host can read it | Only inside the enclave and on the single authorized recipient | +| Where plaintext exists | Decrypted inside your infrastructure, where a vault admin or a compromised host can read it | Inside the enclave and transiently in the authorized recipient's memory after export | | Access control | Enforced by software you operate | Enforced by the policy engine inside the enclave, independent of your infrastructure | | Multi-party approval | Bolted on, if available | Native consensus: require m-of-n approvals before a key is released | | Who can read a released secret | Anyone who sees the response | Only the holder of the ephemeral key the payload is encrypted to, which approvers cannot read | @@ -46,11 +46,11 @@ Typical credentials this pattern protects: ## Example: exchange trading key for a trading firm -A trading firm holds a long-lived JWT for an execution management system that authorizes placing and canceling orders on crypto exchanges. The plaintext should never exist in the firm's own infrastructure. +A trading firm holds a long-lived JWT for an execution management system that authorizes placing and canceling orders on crypto exchanges. The plaintext should not be stored at rest in the firm's infrastructure or exposed to intermediaries; after export, it exists transiently in the authorized trading service's memory while in use. | Need | How Turnkey solves it | | :--- | :--- | -| Plaintext never lives in the firm's infrastructure | The firm imports the JWT once over an end-to-end encrypted channel, and it stays inside the enclave. Retrieval re-encrypts it to a single ephemeral key | +| No plaintext at rest or in intermediaries | The firm imports the JWT once over an end-to-end encrypted channel. Retrieval re-encrypts it to the authorized trading service's ephemeral key | | Only the trading service can retrieve it | Policy scopes retrieval of `kind == 'exchangeApiKey'` secrets to the trading service user | | High-risk keys need oversight | A consensus policy requires human approval before any withdrawal-capable key is released | | Every access is attributable | Each retrieval and approval is a signed activity, logged and queryable | @@ -84,11 +84,11 @@ A trading firm holds a long-lived JWT for an execution management system that au }); ``` - You can now delete the plaintext from wherever it existed before. + You can now delete any persisted plaintext copy from wherever it existed before. - The trading service authenticates with its own credentials and retrieves the key when it boots or opens a session. `exportSecret` generates the ephemeral keypair, submits the export activity, decrypts the result, and zeroizes the key: + The trading service authenticates with its own credentials and retrieves the key when it boots or opens a session. `exportSecret` generates the ephemeral keypair, submits the export activity, and decrypts the result into the authorized service's memory. Symmetric, bearer, HMAC, and OAuth credentials necessarily exist there transiently while in use: ```typescript const emsJwt = await turnkey.apiClient().exportSecret({ From a641d1e9fbbd2ce7016831245a7f99f0385a87a7 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Wed, 12 Aug 2026 12:32:59 -0400 Subject: [PATCH 22/29] Apply review feedback: drop MPC keyshare provider mentions and tighten dense sentences --- features/secrets.mdx | 4 ++-- solutions/key-management/api-key-storage.mdx | 2 +- solutions/key-management/mpc-keyshare-storage.mdx | 15 +-------------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/features/secrets.mdx b/features/secrets.mdx index 19533571..3168fe96 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -16,7 +16,7 @@ Plaintext only ever exists inside the enclave and on the client that imported or ## How it works -The Secrets methods use the same enclave secure-channel pattern as [wallet import](/features/wallets/import-wallets) and [wallet export](/features/wallets/export-wallets): every transfer is HPKE-encrypted to a single-use target key, so plaintext appears only inside the enclave and on the client holding the matching private key. See [Enclave secure channels](/security/enclave-secure-channels) for the canonical protocol details. +The Secrets methods use the same enclave secure-channel pattern as [wallet import](/features/wallets/import-wallets) and [wallet export](/features/wallets/export-wallets). Every transfer is HPKE-encrypted to a single-use target key, so plaintext appears only inside the enclave and on the client holding the matching private key. See [Enclave secure channels](/security/enclave-secure-channels) for the canonical protocol details. What is specific to Secrets: @@ -85,7 +85,7 @@ Because export is an activity, it composes with everything the policy engine sup Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. Secret storage is built from the same primitives: - **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys. -- **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key, with the organization, secret ID, and cipher suite bound into the authenticated data, so no one can substitute ciphertext across secrets or organizations. +- **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key. The organization, secret ID, and cipher suite are bound into the authenticated data, so no one can substitute ciphertext across secrets or organizations. - **Signed provenance**: the enclave quorum key signs every stored secret and ingress key. Enclaves refuse anything they didn't produce. - **Quantum resistant internally, agile in transit**: secrets rest under AES-256-GCM, a quantum resistant cipher. The transport cipher suite is a field in import and export requests, designed to be extended over time, so Turnkey can adopt new transport protocols as they mature. - **Forward secrecy**: ingress and egress target keys are single-use. Compromising one exposes at most one payload. diff --git a/solutions/key-management/api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx index 5284879e..d4877dc2 100644 --- a/solutions/key-management/api-key-storage.mdx +++ b/solutions/key-management/api-key-storage.mdx @@ -31,7 +31,7 @@ Typical credentials this pattern protects: | Audit trail | Vault logs you maintain | Every retrieval and approval is a signed, attributable Turnkey activity | - Exporting a secret necessarily places plaintext on the authorized recipient while the credential is in use. For symmetric credentials (HMAC keys, bearer tokens, OAuth secrets) this is unavoidable: the service must present the secret itself. For asymmetric API credentials whose operation is signing, you can avoid the export step entirely: store the credential as a Turnkey [private key](/features/wallets#private-keys) and sign each request inside the enclave with [sign_raw_payload](/api-reference/activities/sign-raw-payload), so the recipient never receives plaintext private-key material. + Exporting a secret necessarily places plaintext on the authorized recipient while the credential is in use. For symmetric credentials (HMAC keys, bearer tokens, OAuth secrets) this is unavoidable: the service must present the secret itself. For asymmetric API credentials whose operation is signing, you can avoid the export step entirely. Store the credential as a Turnkey [private key](/features/wallets#private-keys) and sign each request inside the enclave with [sign_raw_payload](/api-reference/activities/sign-raw-payload), so the recipient never receives plaintext private-key material. ## Key implementation decisions diff --git a/solutions/key-management/mpc-keyshare-storage.mdx b/solutions/key-management/mpc-keyshare-storage.mdx index d533584f..8e03e393 100644 --- a/solutions/key-management/mpc-keyshare-storage.mdx +++ b/solutions/key-management/mpc-keyshare-storage.mdx @@ -10,24 +10,12 @@ import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' -Regulated custodians and institutions that sign with MPC systems distribute keyshares across parties, and typically rely on an independent backup or recovery provider so that losing one party's share does not mean losing funds. Turnkey Secrets can serve as that independent recovery store: an MPC party's keyshare bundle is imported as an opaque secret, held inside a [secure enclave](/security/secure-enclaves), and released only through a [policy-gated](/features/policies/overview) export encrypted to a designated recovery participant. You can use it instead of, or in addition to, another recovery store. For example, a custodian running an MPC-CMP based signing stack such as Fireblocks might store a party's keyshare bundle in Turnkey Secrets alongside, or instead of, a dedicated recovery service such as Coincover (illustrative ecosystem examples, not partnerships or product commitments). This solution builds on [Secret storage](/features/secrets). +Regulated custodians and institutions that sign with MPC systems distribute keyshares across parties, and typically rely on an independent backup or recovery provider so that losing one party's share does not mean losing funds. Turnkey Secrets can serve as that independent recovery store. An MPC party's keyshare bundle is imported as an opaque secret, held inside a [secure enclave](/security/secure-enclaves), and released only through a [policy-gated](/features/policies/overview) export encrypted to a designated recovery participant. You can use it instead of, or in addition to, another recovery store. This solution builds on [Secret storage](/features/secrets). ## Opaque by design Turnkey treats every keyshare as an opaque byte blob. Turnkey never parses, validates, interprets, or derives anything from the bundle's contents, and makes no assumptions about curve, seed format, or addresses. Whatever bytes your MPC provider exports are the bytes Turnkey stores and returns. -This is also why storing a share with Turnkey does not change your signing trust model: an MPC shard is not a standalone private key. A single Fireblocks keyshare, for example, is useless by itself for signing; producing a signature still requires the MPC protocol and its signing quorum. Turnkey does not participate in MPC signing, and never validates or reconstructs key material. - -## What a keyshare bundle contains - -Bundle formats and versions vary across providers and protocol versions, so treat the following as indicative rather than guaranteed: - -- **ECDSA/secp256k1 (e.g. MPC-CMP)**: a party's bundle can include an additive scalar share, Paillier secret material, ring-Pedersen/Damgard-Fujisaki auxiliary parameters, a chain code, and per-party public metadata. Operational bundles are commonly single-digit kilobytes, dominated by the auxiliary material and the party count and modulus choices. -- **EdDSA/Ed25519**: bundles are substantially smaller, typically an additive or expanded scalar share, a chain code, and per-party public shares, without Paillier or ring-Pedersen material. An expanded Ed25519 scalar share has no corresponding seed and must not be forced into seed-based key formats. -- **Recovery material**: backup artifacts may be sub-kilobyte when they include only scalar shares plus chain code, but actual exported artifact sizes and formats vary by provider and version. - -Because the storage layer is opaque, none of this variation matters to Turnkey: bundles of any of these shapes are stored and returned byte-for-byte. - ## What Turnkey contributes | Need | How Turnkey solves it | @@ -47,7 +35,6 @@ const secretId = await turnkey.apiClient().importSecret({ name: "treasury-signer-party-2-keyshare", staticProperties: { kind: "mpcKeyshare", - provider: "fireblocks", curve: "secp256k1", environment: "production", recoveryRole: "backupParty", From 9ce97570206ae7c5d0548284637a91d9dbb071bd Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 13 Aug 2026 12:01:54 -0400 Subject: [PATCH 23/29] Add non-custodial delegated access variant for trading on behalf of users --- solutions/key-management/api-key-storage.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/solutions/key-management/api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx index d4877dc2..8433b9e2 100644 --- a/solutions/key-management/api-key-storage.mdx +++ b/solutions/key-management/api-key-storage.mdx @@ -30,10 +30,6 @@ Typical credentials this pattern protects: | Who can read a released secret | Anyone who sees the response | Only the holder of the ephemeral key the payload is encrypted to, which approvers cannot read | | Audit trail | Vault logs you maintain | Every retrieval and approval is a signed, attributable Turnkey activity | - - Exporting a secret necessarily places plaintext on the authorized recipient while the credential is in use. For symmetric credentials (HMAC keys, bearer tokens, OAuth secrets) this is unavoidable: the service must present the secret itself. For asymmetric API credentials whose operation is signing, you can avoid the export step entirely. Store the credential as a Turnkey [private key](/features/wallets#private-keys) and sign each request inside the enclave with [sign_raw_payload](/api-reference/activities/sign-raw-payload), so the recipient never receives plaintext private-key material. - - ## Key implementation decisions | Decision | What to consider | Learn more | @@ -119,6 +115,12 @@ A trading firm holds a long-lived JWT for an execution management system that au +### Variant: trading on behalf of your users + +Some firms run trading strategies on behalf of other people rather than on their own exchange accounts. The same pattern supports a non-custodial setup: each end user gets their own [sub-organization](/features/sub-organizations) and imports their exchange API key into it directly, so the plaintext never passes through your infrastructure on the way in. A [delegated access](/features/policies/delegated-access/overview) user you control, scoped by policy to retrieving that key and nothing else, lets your trading strategy pull the credential at runtime while the end user retains control of their sub-organization. + +For a trust-minimized deployment, run the trading strategy itself inside [Turnkey Verifiable Cloud](/features/verifiable-cloud/overview). The strategy executes in a verifiable secure enclave, so end users can verify exactly what code receives their API key, and the plaintext exists only inside that enclave while the strategy runs, keeping the arrangement non-custodial end to end. + ## Next steps
From b23d07cb63a1163ec0c79fc8510e2c6c1f9b0e68 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 13 Aug 2026 12:05:50 -0400 Subject: [PATCH 24/29] Cross-link solutions in Next steps instead of enclave secure channels and drop noisy code comment --- solutions/key-management/api-key-storage.mdx | 3 +++ solutions/key-management/mpc-keyshare-storage.mdx | 4 ++-- solutions/key-management/programmable-credential-access.mdx | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/solutions/key-management/api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx index 8433b9e2..3b68e9b1 100644 --- a/solutions/key-management/api-key-storage.mdx +++ b/solutions/key-management/api-key-storage.mdx @@ -5,6 +5,7 @@ tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' +import { SolutionCard } from '/snippets/solution-card.mdx' import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' @@ -126,4 +127,6 @@ For a trust-minimized deployment, run the trading strategy itself inside [Turnke
+ +
diff --git a/solutions/key-management/mpc-keyshare-storage.mdx b/solutions/key-management/mpc-keyshare-storage.mdx index 8e03e393..3b7c955c 100644 --- a/solutions/key-management/mpc-keyshare-storage.mdx +++ b/solutions/key-management/mpc-keyshare-storage.mdx @@ -31,7 +31,7 @@ Bind [static properties](/features/secrets#static-properties) at import time so ```typescript const secretId = await turnkey.apiClient().importSecret({ - plaintext: keyshareBundle, // Uint8Array, opaque to Turnkey + plaintext: keyshareBundle, name: "treasury-signer-party-2-keyshare", staticProperties: { kind: "mpcKeyshare", @@ -61,5 +61,5 @@ At recovery time, the designated recovery participant generates an ephemeral key - +
diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index 657e08b6..35a5d567 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -5,6 +5,7 @@ tag: "Beta" --- import { FeatureCard } from '/snippets/feature-card.mdx' +import { SolutionCard } from '/snippets/solution-card.mdx' import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' @@ -127,4 +128,6 @@ Model a browser agent and a payment agent as durable Turnkey users, with session
+ +
From 533223537cb3b95b1de798973a75231a7eb1041a Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 13 Aug 2026 12:07:12 -0400 Subject: [PATCH 25/29] Remove remaining code comments from solution snippets --- solutions/key-management/api-key-storage.mdx | 2 -- solutions/key-management/encryption-key-storage.mdx | 3 --- 2 files changed, 5 deletions(-) diff --git a/solutions/key-management/api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx index 3b68e9b1..60e1d044 100644 --- a/solutions/key-management/api-key-storage.mdx +++ b/solutions/key-management/api-key-storage.mdx @@ -91,8 +91,6 @@ A trading firm holds a long-lived JWT for an execution management system that au const emsJwt = await turnkey.apiClient().exportSecret({ secretId, }); - - // Use the JWT for REST or WebSocket calls, keep it in memory only ``` diff --git a/solutions/key-management/encryption-key-storage.mdx b/solutions/key-management/encryption-key-storage.mdx index f02d74f2..f004c7ad 100644 --- a/solutions/key-management/encryption-key-storage.mdx +++ b/solutions/key-management/encryption-key-storage.mdx @@ -71,10 +71,8 @@ A common pattern for applications: encrypt recovery bundles, store them in your Use the public key to encrypt sensitive data on your side. Turnkey never sees the plaintext or the encrypted result: ```typescript - // Using P-256 ECIES encryption const encryptedBundle = await encryptWithPublicKey(publicKey, sensitiveData); - // Store in YOUR infrastructure await saveToYourStorage(encryptedBundle); ``` @@ -116,7 +114,6 @@ A common pattern for applications: encrypt recovery bundles, store them in your ```typescript const plaintext = await decryptWithPrivateKey(decryptionKey, encryptedBundle); - // Use the decrypted data (sign transactions, access credentials, etc.) ``` When done, clear the decryption key and any decrypted data from memory: From f424a5d1fd549c5f644067a7c225507b221a3110 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 13 Aug 2026 12:08:03 -0400 Subject: [PATCH 26/29] Revert "Remove remaining code comments from solution snippets" This reverts commit 533223537cb3b95b1de798973a75231a7eb1041a. --- solutions/key-management/api-key-storage.mdx | 2 ++ solutions/key-management/encryption-key-storage.mdx | 3 +++ 2 files changed, 5 insertions(+) diff --git a/solutions/key-management/api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx index 60e1d044..3b68e9b1 100644 --- a/solutions/key-management/api-key-storage.mdx +++ b/solutions/key-management/api-key-storage.mdx @@ -91,6 +91,8 @@ A trading firm holds a long-lived JWT for an execution management system that au const emsJwt = await turnkey.apiClient().exportSecret({ secretId, }); + + // Use the JWT for REST or WebSocket calls, keep it in memory only ``` diff --git a/solutions/key-management/encryption-key-storage.mdx b/solutions/key-management/encryption-key-storage.mdx index f004c7ad..f02d74f2 100644 --- a/solutions/key-management/encryption-key-storage.mdx +++ b/solutions/key-management/encryption-key-storage.mdx @@ -71,8 +71,10 @@ A common pattern for applications: encrypt recovery bundles, store them in your Use the public key to encrypt sensitive data on your side. Turnkey never sees the plaintext or the encrypted result: ```typescript + // Using P-256 ECIES encryption const encryptedBundle = await encryptWithPublicKey(publicKey, sensitiveData); + // Store in YOUR infrastructure await saveToYourStorage(encryptedBundle); ``` @@ -114,6 +116,7 @@ A common pattern for applications: encrypt recovery bundles, store them in your ```typescript const plaintext = await decryptWithPrivateKey(decryptionKey, encryptedBundle); + // Use the decrypted data (sign transactions, access credentials, etc.) ``` When done, clear the decryption key and any decrypted data from memory: From 6e053b95ebebee457b8ef0c93f392d94c708fb53 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 13 Aug 2026 18:02:12 -0400 Subject: [PATCH 27/29] Drop Opaque by design section from MPC keyshare storage page --- solutions/key-management/mpc-keyshare-storage.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/solutions/key-management/mpc-keyshare-storage.mdx b/solutions/key-management/mpc-keyshare-storage.mdx index 3b7c955c..8da3427a 100644 --- a/solutions/key-management/mpc-keyshare-storage.mdx +++ b/solutions/key-management/mpc-keyshare-storage.mdx @@ -12,10 +12,6 @@ import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' Regulated custodians and institutions that sign with MPC systems distribute keyshares across parties, and typically rely on an independent backup or recovery provider so that losing one party's share does not mean losing funds. Turnkey Secrets can serve as that independent recovery store. An MPC party's keyshare bundle is imported as an opaque secret, held inside a [secure enclave](/security/secure-enclaves), and released only through a [policy-gated](/features/policies/overview) export encrypted to a designated recovery participant. You can use it instead of, or in addition to, another recovery store. This solution builds on [Secret storage](/features/secrets). -## Opaque by design - -Turnkey treats every keyshare as an opaque byte blob. Turnkey never parses, validates, interprets, or derives anything from the bundle's contents, and makes no assumptions about curve, seed format, or addresses. Whatever bytes your MPC provider exports are the bytes Turnkey stores and returns. - ## What Turnkey contributes | Need | How Turnkey solves it | From 4d31e775c43bb4ba1b269aee6f2bebb3a689ef54 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 13 Aug 2026 18:02:13 -0400 Subject: [PATCH 28/29] Frame dynamic policies as allow once then allow always --- solutions/key-management/programmable-credential-access.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx index 35a5d567..2c32490b 100644 --- a/solutions/key-management/programmable-credential-access.mdx +++ b/solutions/key-management/programmable-credential-access.mdx @@ -12,7 +12,7 @@ import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' When an AI agent needs a password, a card number, or an API key, teams face an unpleasant choice: hand the agent unfettered access to credentials, or put a human in the loop for every single request. Turnkey gives you a third option: a programmable access layer that evaluates every credential request against policies you control, inside a [secure enclave](/security/secure-enclaves) that never releases plaintext unless the policy allows it. -Policies and tags are fully dynamic: update them at runtime to widen or narrow access on the fly, without re-importing anything. This solution builds on [Secret storage](/features/secrets). +Because policies and tags are fully dynamic, you can start restrictive and loosen as trust builds, without re-importing anything. Start with an agent that must ask for every secret: it requests access, a human approves that one request, and the export completes. When the same request keeps coming back, the human adds a policy that lets the agent export that class of secret on its own, and the prompts stop. Allow always and tightening back up are both just policy edits. This solution builds on [Secret storage](/features/secrets). ## Access patterns From 03474b93d87c87dd0c94cfebcfae62fe503dce50 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 13 Aug 2026 18:12:20 -0400 Subject: [PATCH 29/29] Drop coordinator ciphertext sentence from secrets overview --- features/secrets.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/secrets.mdx b/features/secrets.mdx index 3168fe96..9111f970 100644 --- a/features/secrets.mdx +++ b/features/secrets.mdx @@ -12,8 +12,7 @@ import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx' Turnkey Secrets lets you store arbitrary sensitive data (passwords, credit card details, API keys, SSNs) encrypted end-to-end between your client and Turnkey's [secure enclaves](/security/secure-enclaves). The [policy engine](/features/policies/overview) evaluates every export, so you control exactly who can retrieve a secret, under what conditions, and with how many approvals. We designed the secret storage API for flexibility and programmability. -Plaintext only ever exists inside the enclave and on the client that imported or exported it. Turnkey's coordinator, database, and public API only ever see ciphertext. - +Plaintext only ever exists inside the enclave and on the client that imported or exported it. ## How it works The Secrets methods use the same enclave secure-channel pattern as [wallet import](/features/wallets/import-wallets) and [wallet export](/features/wallets/export-wallets). Every transfer is HPKE-encrypted to a single-use target key, so plaintext appears only inside the enclave and on the client holding the matching private key. See [Enclave secure channels](/security/enclave-secure-channels) for the canonical protocol details.