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:
| Format | What you send | When to use it |
|---|---|---|
WRAPPED | the key bytes wrapped to the RSA public key of the key server's trust certificate | always, when the source can wrap -- the key never crosses the network or passes through the Gateway in the clear |
RAW | the 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-CBCandAES-256-CBC(a deployment at the highest security level allowsAES-256-CBConly). 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(andcertificatePem: null) -- the key server has no trust certificate yet, and aWRAPPEDimport is refused409 TRUST_NOT_ESTABLISHED. Establish one withPOST /api/v1/trust/local-certificate/accept(see the API Reference).- An EC trust certificate (
publicKeyTypeother thanRSA) cannot unwrap either method below, and aWRAPPEDimport is refused409 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 code | code in the result | Meaning |
|---|---|---|
400 KEY_UNWRAP_FAILED | 1536 | the wrapped key did not unwrap under the trust certificate -- wrapped to another key, by another method, or damaged |
400 INVALID_ARGUMENT | 1283 | the 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.