Migrating keys from a KMIP key manager
If your symmetric keys live in a KMIP key manager, the KMIP migration client moves them into
the key server for you. It reads each key from the source over KMIP 1.4, has the source wrap it
to the key server's own trust certificate, and imports it through the REST Gateway
(POST /api/v1/keys/import, described in Importing your own keys).
Each key arrives under a hidden link the key server mints, and the client keeps a map from each
source key to its new hidden link, so you can run it again safely.
The client ships with the Console and Gateway release:
- Linux:
/opt/pisoft/tricryption/console-gateway/integrations/kmip-migrate/ - Windows:
C:\Program Files\PiSoft\Tricryption\console-gateway\integrations\kmip-migrate\
It needs Python 3.8 or later and nothing else -- it uses the standard library only. The
installed directory is read-only: run migrate.py from there and keep its map somewhere writable.
What it does and does not do
- It reads the source: it lists the symmetric keys, reads their attributes, and fetches each key.
- It registers one public key on the source -- the key server's import key -- and activates it, so that the source can wrap keys to it. That is the only object it creates there.
- It never deletes, revokes or changes a source key. The source keys stay where they are; once you have verified the migration and moved your applications over, retiring them is yours to do in the key manager.
- It imports AES-128, AES-192 and AES-256 keys, each as the key server's AES-CBC cipher of that length. A KMIP symmetric key does not name a mode; the client lists any other key and leaves it.
- It migrates keys that are Pre-Active, Active or Deactivated at the source. A Compromised or Destroyed key is reported and never imported.
What you need
| The source's address | --source HOST:PORT. KMIP's registered port is 5696. |
| The source's CA certificate | --ca FILE (PEM). The source's server certificate must chain to it and name HOST. |
| A client certificate the source trusts | --cert FILE --key FILE (PEM), for an identity that may list, read and fetch the keys and register an object. KMIP servers authenticate clients by their TLS certificate. |
| The Gateway | --gateway URL, and --gateway-ca FILE when its certificate is from a private CA. |
| A Gateway credential | --credential-file FILE: JSON, readable by you only (mode 600), holding {"user": "...", "password": "..."} or a session, {"session": "..."}. The principal needs the Import Key right -- the Exchanger role has it -- and becomes the owner of every key it imports. A scoped credential cannot import. |
| The import key | read from the Gateway by the client with your credential; --import-cert FILE only for a run that cannot ask the Gateway -- see below. |
| A map file | --map FILE. The first run creates it. It holds no secret. |
The import key
The client wraps every key to the public key of the key server's trust certificate -- the
certificate GET /api/v1/trust/local-certificate returns as certificatePem. The trust certificate
must be RSA.
- The client reads it itself, with your Gateway credential: the Exchanger role that grants Import Key also grants this read, so no Security Officer step is needed.
--import-cert FILEis for a run that cannot ask the Gateway for it -- an air-gapped source, for example. The certificate is public material: export the PEM once and pass it in. If the client can also read the certificate and the two differ, it stops: export it again.- If the key server has no trust certificate yet, establish one first (see Importing your own keys).
1. Plan
plan lists every symmetric key the source holds, and imports nothing:
$ python3 migrate.py plan --source kms.example.com:5696 --ca kms-ca.pem \
--cert migrator.pem --key migrator.key --map keys.map.json
plan: source kms.example.com:5696 answers KMIP 2.0, 1.4, 1.3, 1.2, 1.1, 1.0; 4 symmetric key(s)
KEY 0f0e7a61-2c43-4b8e-9d1a-3b5c7e9f1a20 AES-256 state=Active release=yes name=ledger-2026: to migrate as AES-256-CBC
KEY 5b2d9c14-8e7f-4a36-b0c2-1d4e6f8a9b3c AES-128 state=Active release=yes name=archive-2019: to migrate as AES-128-CBC
KEY 9c4f1e2a-7b6d-4c58-a3e9-0f2b4d6e8a1c AES-256 state=Active release=wrapped only (Sensitive=true) name=payroll: to migrate as AES-256-CBC
KEY d3a8b5f7-1c2e-4f90-8b6a-5e7c9d0f2a4b AES-256 state=Active release=no (Extractable=false) name=hsm-bound: will not migrate (the source will not release it)
release reads the key's KMIP attributes. no (Extractable=false) -- the source will not release
the key at all, wrapped or not. wrapped only (Sensitive=true) -- the source releases it only
wrapped, which is how the client asks for every key anyway.
Use plan to agree the list with the key manager's owner before you import anything.
2. Apply
apply imports every key that is not yet in the map, one key at a time:
$ python3 migrate.py apply --source kms.example.com:5696 --ca kms-ca.pem \
--cert migrator.pem --key migrator.key \
--gateway https://gateway.example.com:8443 --credential-file importer.json \
--map keys.map.json
apply: source kms.example.com:5696 answers KMIP 2.0, 1.4, 1.3, 1.2, 1.1, 1.0; Gateway https://gateway.example.com:8443
apply: the import key is the trust certificate GET /api/v1/trust/local-certificate serves
apply: registered the key server's import key (RSA-2048, SPKI sha256 6f1c2a9d4e8b3f07) on the source as 7e1d3c5b-9a2f-4e68-b0d4-2c6e8a1f3b5d and activated it
apply: 4 symmetric key(s) to consider
KEY 0f0e7a61-2c43-4b8e-9d1a-3b5c7e9f1a20 imported WRAPPED as AES-256-CBC -> AAAAAQAAAAEbZmFrZS1oaWRkZW4tbGluay1vbmU=
KEY 5b2d9c14-8e7f-4a36-b0c2-1d4e6f8a9b3c imported WRAPPED as AES-128-CBC -> AAAAAQAAAAEbZmFrZS1oaWRkZW4tbGluay10d28=
KEY 9c4f1e2a-7b6d-4c58-a3e9-0f2b4d6e8a1c imported WRAPPED as AES-256-CBC -> AAAAAQAAAAEbZmFrZS1oaWRkZW4tbGluay10aHI=
KEY d3a8b5f7-1c2e-4f90-8b6a-5e7c9d0f2a4b NOT MIGRATED: the source refuses to release it -- Not Extractable: DENIED (Result Reason 23)
apply: 3 imported, 0 already in the map, 1 not migrated (d3a8b5f7-1c2e-4f90-8b6a-5e7c9d0f2a4b)
For each key the client:
- asks the source for the key wrapped under the registered import key, by this Key Wrapping Specification: wrapping method Encrypt; padding OAEP, hashing algorithm SHA-256, mask generator MGF1 with SHA-256; encoding option No Encoding. It checks that the answer was wrapped that way;
- imports it as
WRAPPEDwith methodrsa-oaep-sha256, and the source's unique identifier as thesourceId; - writes the map before it moves to the next key, so an interrupted run loses nothing.
The key bytes never leave the source in the clear: only the key server's private key can unwrap them.
--only UID (repeatable) imports just the keys you name -- a pilot, before the whole estate.
If the source cannot wrap that way
The key server unwraps RSA-OAEP with SHA-256 and MGF1-SHA-256 only. If your source refuses the wrapped request, or wraps with other parameters, the client reports the key and leaves it:
KEY 5b2d9c14-8e7f-4a36-b0c2-1d4e6f8a9b3c NOT MIGRATED: the source could not wrap it -- Feature Not Supported: ...; it can only come in the clear (RAW) and --allow-clear was not given
You then have two choices:
- Wrap the key yourself outside the client, by one of the two methods the key server accepts,
and import it as described in Importing your own keys. Many
key managers and HSMs can export a key as
CKM_RSA_AES_KEY_WRAP, which is thersa-aes-kwp-sha256method. - Re-run with
--allow-clear. The client then fetches the key in the clear and imports it asRAW, and says so for every such key (IN THE CLEAR: ...). The key passes through the client's and the Gateway's memory, inside TLS. Use it only on a path you trust, and only for keys you accept that for. A key the source marks Sensitive is never released in the clear, so it cannot come this way.
Keys the source will not release
A key whose KMIP Extractable attribute is false -- typical of a key generated inside an HSM -- cannot be migrated by any client: the source refuses to hand it out. The client reports it with the source's own reason and carries on; the run ends with exit status 3 so that a script notices. Re-encrypt the data under a new key instead, using the key manager to decrypt and the key server to encrypt.
3. Verify
verify proves that each imported key is the same key: for sample data encrypted under the key
while it was still in the source, it decrypts the ciphertext through the Gateway, under the
hidden link the map names, and compares the result's SHA-256 with the original's.
Make the samples before you migrate, with the source's own Encrypt operation (AES-CBC, PKCS#5 padding), and record them in a JSON file -- the plaintext as its SHA-256 only:
{
"samples": [
{
"sourceId": "0f0e7a61-2c43-4b8e-9d1a-3b5c7e9f1a20",
"iv": "<base64 of the 16-byte IV>",
"ciphertext": "<base64 of the ciphertext>",
"plaintextSha256": "<hex SHA-256 of the plaintext>"
}
]
}
$ python3 migrate.py verify --gateway https://gateway.example.com:8443 \
--credential-file importer.json --map keys.map.json --samples samples.json
VERIFY 0f0e7a61-2c43-4b8e-9d1a-3b5c7e9f1a20 equal: plaintext sha256 3a7bd3e2360a3d29 through AAAAAQAAAAEbZmFrZS1oaWRkZW4tbGluay1vbmU=
VERIFY 5b2d9c14-8e7f-4a36-b0c2-1d4e6f8a9b3c equal: plaintext sha256 b5bb9d8014a0f9b1 through AAAAAQAAAAEbZmFrZS1oaWRkZW4tbGluay10d28=
VERIFY 9c4f1e2a-7b6d-4c58-a3e9-0f2b4d6e8a1c equal: plaintext sha256 7d865e959b246691 through AAAAAQAAAAEbZmFrZS1oaWRkZW4tbGluay10aHI=
VERIFY d3a8b5f7-1c2e-4f90-8b6a-5e7c9d0f2a4b not migrated (no map entry)
verify: 3 of 3 migrated sample(s) decrypt to the recorded plaintext; 1 sample(s) not migrated
A key that does not decrypt its sample is reported NOT EQUAL, with the Gateway's answer, and
verify exits 3. Do not move an application to a key that has not verified.
Running it again
The map makes apply safe to repeat. A key already in the map is skipped, so a second run imports
nothing and writes nothing at the key server:
KEY 0f0e7a61-2c43-4b8e-9d1a-3b5c7e9f1a20 skipped: already migrated -> AAAAAQAAAAEbZmFrZS1oaWRkZW4tbGluay1vbmU=
...
apply: 0 imported, 3 already in the map, 1 not migrated (d3a8b5f7-1c2e-4f90-8b6a-5e7c9d0f2a4b)
Run apply again after the key manager gains keys, after an interrupted run, or after you have
fixed a key that was not migrated.
Keep the map. It is the only record of which source key became which hidden link -- the key server does not store the source's identifier. Back it up with your other migration records. Removing an entry and running again imports that key a second time, under a second, separate hidden link.
Exit status
| Status | Meaning |
|---|---|
0 | every key was migrated (or already was); every sample verified |
3 | the run completed, and the keys it names were not migrated -- or a sample did not verify |
2 | the run stopped: a usage error, the source or the Gateway unreachable, a login refused |
Sources
The client speaks KMIP 1.4 in its binary TTLV encoding over mutual TLS, and is written for any KMIP 1.4 server that can wrap a symmetric key under a registered RSA public key by OAEP with SHA-256 -- Thales CipherTrust Manager among them.
It has been tested against Cosmian KMS 5.28.0: planning, a pilot, the whole estate, verification
against ciphertexts the source made, a second run that imported nothing, an unexportable key, the
clear path, and a client certificate the source does not trust (refused at the TLS handshake). No
other key manager has been tested; run plan and a pilot with --only against yours first.