From 7bc093608d34d23071dff931a2c9771efb1ae53c Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Tue, 11 Aug 2026 18:15:55 -0400 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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: