Skip to main content

Importing your own keys

If you already hold symmetric keys -- in a KMIP key manager, an HSM, a file -- you can bring them into the key server through the REST Gateway with POST /api/v1/keys/import. Each key you import is stored under a hidden link the key server mints. You use that hidden link (the t-Tag) to encrypt and decrypt under the key exactly as you would under a key the key server created.

To move a whole estate out of a KMIP key manager, the KMIP migration client does all of this for you, key by key: see Migrating keys from a KMIP key manager.

There are two formats for your own keys:

FormatWhat you sendWhen to use it
WRAPPEDthe key bytes wrapped to the RSA public key of the key server's trust certificatealways, when the source can wrap -- the key never crosses the network or passes through the Gateway in the clear
RAWthe key bytes in the clear (inside TLS)only on a trusted path, when the source cannot wrap -- the key passes through the Gateway's memory

A third format, NATIVE, moves a key between two key servers that trust each other; it is not for your own keys.

Before you start​

  • Your session needs a role that grants Import Key (the Exchanger role has it). You become the owner of every key you import. The same role lets you read the import key below.
  • A scoped credential cannot import: it names the keys it may reach, and a new key is none of them. Import with your service's own session.
  • The key server creates keys for these ciphers, and imports keys for the same ones: AES-128-CBC, AES-192-CBC and AES-256-CBC (a deployment at the highest security level allows AES-256-CBC only). The key must be exactly the cipher's key length -- 16, 24 or 32 bytes.

Get the import key​

A WRAPPED import is wrapped to the public key of the key server's trust certificate. GET /api/v1/trust/local-certificate returns it as certificatePem:

{
"configured": true,
"certificatePem": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----\n",
"certificate": { "subject": "...", "publicKeyType": "RSA", "thumbprint": "..." }
}

The service that imports reads it itself, with its own session. The Exchanger role that grants Import Key also grants this read, so a service holding Encryptor + Exchanger needs nobody else to fetch it. The certificate is public material -- the private key never leaves the key server. A Security Officer may read it too; any other principal is answered 403. Establishing or replacing the trust certificate stays the Security Officer's.

Read it at the start of every import run rather than keeping a copy: the answer is always the certificate the key server unwraps with now.

  • configured: false (and certificatePem: null) -- the key server has no trust certificate yet, and a WRAPPED import is refused 409 TRUST_NOT_ESTABLISHED. Establish one with POST /api/v1/trust/local-certificate/accept (see the API Reference).
  • An EC trust certificate (publicKeyType other than RSA) cannot unwrap either method below, and a WRAPPED import is refused 409 TRUST_KEY_NOT_RSA. Establish trust with an RSA certificate.

Save certificatePem as import-cert.pem and take its public key out of it:

openssl x509 -in import-cert.pem -pubkey -noout > import-pub.pem

Wrap a key​

The recipes below use the openssl command line on a test key generated for the purpose. With a KMIP key manager or an HSM, ask it to wrap the key to import-pub.pem by the same method instead: the key bytes then never leave it in the clear.

openssl rand -out key.bin 32 # a 32-byte AES-256 key, for this example

rsa-oaep-sha256​

RSA-OAEP with SHA-256 and MGF1-SHA-256 (no label), of the key bytes. The result is exactly the RSA key's size (256 bytes for RSA-2048):

openssl pkeyutl -encrypt -pubin -inkey import-pub.pem \
-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 \
-in key.bin -out wrapped.bin

rsa-aes-kwp-sha256​

RSA-OAEP-SHA256 of a one-time AES-256 key, followed by that key's RFC 5649 wrap (AES key wrap with padding) of the key bytes -- the layout of PKCS#11 CKM_RSA_AES_KEY_WRAP, which most cloud key-import services also use:

openssl rand -out kek.bin 32
openssl pkeyutl -encrypt -pubin -inkey import-pub.pem \
-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 \
-in kek.bin -out kek.wrapped
openssl enc -id-aes256-wrap-pad -K "$(od -An -tx1 -v kek.bin | tr -d ' \n')" -iv A65959A6 \
-in key.bin -out key.wrapped
cat kek.wrapped key.wrapped > wrapped.bin
rm -f kek.bin

-K puts the one-time key on the command line for the moment openssl enc runs; on a shared machine, wrap with a tool that reads the key from a file or a token instead.

Import​

Send each key with its cipher, the method, the wrapped bytes in canonical base64, and optionally the IV it was used with (16 bytes; absent, the key server mints one) and a sourceId -- your own name for the key, such as its identifier in the system it came from:

POST /api/v1/keys/import
{
"format": "WRAPPED",
"keys": [
{
"cipher": "AES-256-CBC",
"method": "rsa-oaep-sha256",
"wrappedKey": "<base64 of wrapped.bin>",
"sourceId": "finance-ledger-2026"
}
]
}

RAW takes key, the key bytes in canonical base64, in place of method and wrappedKey:

{ "format": "RAW", "keys": [ { "cipher": "AES-128-CBC", "key": "<base64 of key.bin>", "sourceId": "legacy-0042" } ] }

Do not send hiddenLink: the key server mints every link, and a key carrying one is refused 400. So no import can reach a key that already exists, and nobody can claim an identifier before its key does.

The answer​

Every key imported: 201, one result per key, in request order.

{
"format": "WRAPPED",
"keysRequested": 1,
"results": [
{ "sourceId": "finance-ledger-2026", "hiddenLink": "<base64 hidden link>", "outcome": "imported" }
]
}

A key the key server refused is { "sourceId": ..., "outcome": "refused", "code": ... }, with no hiddenLink: nothing was stored for it. The other keys of the request are imported independently. When any key is refused the request's status is the first refusal's, and the error body carries every result:

Status and codecode in the resultMeaning
400 KEY_UNWRAP_FAILED1536the wrapped key did not unwrap under the trust certificate -- wrapped to another key, by another method, or damaged
400 INVALID_ARGUMENT1283the key server does not accept the method, the cipher, the IV length or the key length
400 BAD_REQUEST--the Gateway refused the body before sending anything: a missing or malformed field, or hiddenLink
403--your role does not grant Import Key, or the session is a scoped credential
409 TRUST_NOT_ESTABLISHED--WRAPPED, and the key server has no trust certificate
409 TRUST_KEY_NOT_RSA--WRAPPED, and the trust certificate's key is EC

The key server writes one audit record per imported key, naming its hidden link -- never the key.

Keep your mapping​

The hidden links are the key server's; the mapping from sourceId to hiddenLink is yours to keep. The key server does not store sourceId. Record each result as it comes back: importing the same key again gives it a second, different hidden link.

After the import, encrypt and decrypt with the hidden link as the t-Tag (POST /api/v1/crypto/encrypt, POST /api/v1/crypto/decrypt), grant access to it, and expire or remove it, exactly as with a key the key server created.