POST /keys/export
Export a key wrapped to the client's ephemeral public key
Wraps the key identified by ttag to the client's ephemeral P-256
public key (ECDH → HKDF → AES-GCM) and returns the wrap envelope. The
caller must be the owner or have been granted access, otherwise the kS
denies the export with 403.
Authentication
Requires the X-Session header (the opaque session id from POST /login). See Authentication.
Request
| Field | Type | Required | Description |
|---|---|---|---|
ttag | string (base64) | yes | Base64 key handle returned by POST /keys. |
clientPubKey | string (base64) | yes | Base64 of the client's ephemeral raw uncompressed P-256 point (65 bytes, `0x04 |
Example
{
"ttag": "dGFnLWJhc2U2NC1leGFtcGxl",
"clientPubKey": "BFq2...base64-65-byte-point"
}
Responses
200
Wrapped key returned.
| Field | Type | Required | Description |
|---|---|---|---|
wrapped | WrappedKey | yes | ECDH-P256 → HKDF-SHA256 → AES-GCM wrap envelope. The client derives the same shared secret from gatewayPubKey + its own ephemeral private key, then AES-GCM-decrypts ciphertext (with iv/authTag) to unwrap the data key. The raw key never crosses the wire in clear. |
algo | string | yes | Cipher label for the delivered data key (not the wrap envelope). NOTE: the gateway currently hardcodes this to aes-128-cbc; it is NOT derived from the key and does NOT track keyLen. The demo only creates AES-128 keys (NID 419 → 16 bytes), so here algo and keyLen happen to coincide. For an AES-192/256 key, keyLen would be 24/32 while algo would still report aes-128-cbc. Treat keyLen as authoritative for the delivered key length. |
keyLen | integer (16, 24, 32) | yes | Length in bytes of the delivered data key, derived from the export blob NID (419 → 16, 423 → 24, 427 → 32). Authoritative for the key length (see the caveat on algo). |
wrapped object
| Field | Type | Required | Description |
|---|---|---|---|
gatewayPubKey | string (base64) | yes | Base64 of the gateway's ephemeral P-256 public point. |
salt | string (base64) | yes | Base64 HKDF salt (16 bytes). |
iv | string (base64) | yes | Base64 AES-GCM initialization vector. |
ciphertext | string (base64) | yes | Base64 AES-GCM ciphertext of the wrapped data key. |
authTag | string (base64) | yes | Base64 AES-GCM authentication tag. |
Example
{
"wrapped": {
"gatewayPubKey": "...",
"salt": "...",
"iv": "...",
"ciphertext": "...",
"authTag": "..."
},
"algo": "aes-128-cbc",
"keyLen": 16
}
400
Validation error: missing fields, invalid JSON, or body > 1 MB.
401
Authentication required. Two shapes share this status: a session-layer 401 (a plain Error, no reauth flag) means a missing or unknown/expired X-Session — re-login for a new session id; a token-layer 401 (ReauthError with reauth: true) means the server-held token expired — re-run POST /login and retry.
403
kS RBAC/ACL denial — a real authorization refusal. Surface it; retrying will not help until access is granted.
500
Other kS operation error.
Error handling
This endpoint can return the statuses below. For request bodies and the client action per status, see the full error contract.
| Status | Meaning |
|---|---|
400 | Validation error: missing fields, invalid JSON, or body > 1 MB. |
401 | Authentication required. Two shapes share this status: a session-layer 401 (a plain Error, no reauth flag) means a missing or unknown/expired X-Session — re-login for a new session id; a token-layer 401 (ReauthError with reauth: true) means the server-held token expired — re-run POST /login and retry. |
403 | kS RBAC/ACL denial — a real authorization refusal. Surface it; retrying will not help until access is granted. |
500 | Other kS operation error. |
A 401 with { "reauth": true } means the server-held token expired — re-run POST /login and retry. A 403 is a real authorization denial — surface it; retrying will not help.