Skip to content

Key export

export_private_key removes a key from Kryard's custody — but the plaintext private key is never returned. You supply a recipient public key; the signer decrypts the key inside its boundary, HPKE-seals it to your recipient key, and returns only the sealed bundle. Only the holder of the matching recipient private key can open it.

Security-sensitive and gated

Export is disabled by default in the signer (ALLOW_EXPORT) — a disabled request fails with SIGNER_ERROR (403). Once exported, the key's exported flag is set as audit metadata. Export still goes through policy + the signer's evaluatedInputHash re-check.

The flow

  1. Generate a recipient keypair on the caller side — a P-256 keypair. Keep the private key local; it never leaves you.
  2. Submit export_private_key with the recipient public key as targetPublicKey.
  3. The signer envelope-decrypts the key, HPKE-seals it to targetPublicKey, zeroizes the plaintext, and returns the export bundle.
  4. HPKE-open the bundle with your recipient private key to recover the raw key.

Request

POST /public/v1/submit/export_private_key

ParameterTypeNotes
privateKeyId or signWithstringThe key to export (id, or address via signWith). Required.
targetPublicKeystringRecipient P-256 public key, hex — uncompressed (65-byte 0x04-prefixed) or compressed (33-byte 0x02/0x03). Required.

Example — SDK

All-in-one — the SDK generates an ephemeral P-256 recipient, submits the export, opens the bundle, and zeroizes the recipient, returning the raw key:

ts
const { privateKey } = await client.exportPrivateKey({
  signWith: "b3f1…",   // private-key id or address
});
// privateKey: raw key hex (no 0x) — handle securely and discard when done.

Bring your own recipient — keep the recipient private key under your control and open the bundle yourself (see Decrypting the bundle):

ts
const { privateKeyId, exportBundle } = await client.exportPrivateKeyBundle({
  signWith: "b3f1…",
  targetPublicKey: recipientPublicKeyHex,  // your P-256 recipient public key, SEC1 hex
});

Example — raw HTTP

json
{
  "type": "ACTIVITY_TYPE_EXPORT_PRIVATE_KEY",
  "timestampMs": "1748956800000",
  "organizationId": "org-…",
  "parameters": {
    "privateKeyId": "b3f1…",
    "targetPublicKey": "04a1b2…"
  }
}

Response — the export bundle

result.exportPrivateKeyResult.exportBundle is an RFC-9180 HPKE bundle. It contains no plaintext — only the HPKE encapsulated key and ciphertext:

json
{
  "activity": {
    "status": "ACTIVITY_STATUS_COMPLETED",
    "result": {
      "exportPrivateKeyResult": {
        "privateKeyId": "b3f1…",
        "exportBundle": {
          "encappedPublic": "BASE64(enc)",
          "ciphertext": "BASE64(ct)",
          "info": "kryard-export-v1",
          "kemKdfAead": "P256_HKDF_SHA256/HKDF_SHA256/AES256GCM"
        }
      }
    }
  }
}
FieldMeaning
encappedPublicbase64 of the HPKE encapsulated key (enc).
ciphertextbase64 of the sealed private key (ct).
infothe HPKE info label, kryard-export-v1 — binds the bundle to the export-protocol version.
kemKdfAeadthe HPKE suite identifier (below).

HPKE suite

The bundle is sealed under a fixed RFC-9180 suite (Turnkey's suite):

ComponentValue
KEMKEM_P256_HKDF_SHA256
KDFKDF_HKDF_SHA256
AEADAEAD_AES256GCM
infokryard-export-v1
aad (additional authenticated data)the privateKeyId

The aad binds the ciphertext to the exact key id, so a bundle cannot be silently retargeted to a different key.

Clean RFC-9180, not Turnkey's wrapper

This is a standard RFC-9180 HPKE bundle using Turnkey's cryptographic suite — open it with Kryard's own decryptExportBundle (below) or any RFC-9180 HPKE library. It is not byte-compatible with Turnkey's client wrapper, which expects their exact enclave-signed format.

Decrypting the bundle

The SDK ships the matching opener. generateRecipientKeyPair gives you a P-256 recipient (use its publicKeyHex as targetPublicKey); decryptExportBundle opens the bundle with the suite, info, and aad = privateKeyId already wired:

ts
import { generateRecipientKeyPair, decryptExportBundle } from "@kryard/sdk";

const recipient = await generateRecipientKeyPair();

const { privateKeyId, exportBundle } = await client.exportPrivateKeyBundle({
  signWith: "b3f1…",
  targetPublicKey: recipient.publicKeyHex,
});

const keyBytes = await decryptExportBundle(exportBundle, recipient.privateKey, privateKeyId);
// keyBytes: the raw private key — handle securely, then keyBytes.fill(0).

TIP

client.exportPrivateKey({ signWith }) does exactly this for you — generate → export → decrypt → zeroize — and returns { privateKey }. Use the manual flow above only when the recipient key must stay under your control.

Under the hood it's a plain RFC-9180 open, so it's equivalent to doing it by hand with any HPKE library (same suite, info = "kryard-export-v1", aad = privateKeyId):

ts
// Equivalent manual open with @hpke/core.
import { CipherSuite, Aes256Gcm, HkdfSha256, DhkemP256HkdfSha256 } from "@hpke/core";

const suite = new CipherSuite({
  kem: new DhkemP256HkdfSha256(),
  kdf: new HkdfSha256(),
  aead: new Aes256Gcm(),
});

const recipientCtx = await suite.createRecipientContext({
  recipientKey: recipientPrivateKey,                  // your P-256 private key
  enc: base64Decode(exportBundle.encappedPublic),
  info: new TextEncoder().encode(exportBundle.info),  // "kryard-export-v1"
});

const privateKeyBytes = await recipientCtx.open(
  base64Decode(exportBundle.ciphertext),
  new TextEncoder().encode(privateKeyId),             // aad
);
// privateKeyBytes is the raw key — handle it securely and zeroize when done.

Wallet infrastructure for gasless, sponsored transactions — a Turnkey-compatible signer and a managed EIP-7702 relay.