PKCS#12 Import & RSA-2048 → ML-KEM-768 Migration
Import a legacy PKCS#12 keystore holding an RSA-2048 key, migrate to ML-KEM-768 via rotation. Legacy → PQC in three steps.
📦 PKCS#12 Import & RSA-2048 → ML-KEM-768 Migration
Scenario: you have an existing PKCS#12 (.p12 / .pfx) keystore from a legacy PKI — say an RSA-2048 key issued years ago. Import it into your tenant keystore, then rotate to ML-KEM-768 so the app can enjoy PQC without re-issuing anything on the legacy side.
Prerequisites
Same two identities as the AES-256 recipe. Admin needs admin.keys.orchestrate (which typically bundles import + rotate).
The PKCS#12 file may hold multiple aliases — this recipe assumes exactly one private key. For multi-alias handling see analyzeKeystore first.
Step-by-step
export BASE_URL="https://staging.ankatech.co"
export TENANT_ID="00000000-0000-0000-0000-000000000003"
export P12_PATH="/path/to/legacy-rsa.p12"
export P12_PASSWORD="the-p12-password"
export SRC_KID="legacy-rsa-imported" # kid to assign after import
export SUCC_KID="mlkem768-modern" # kid for the PQC successor
# ADMIN_TOKEN + APP_TOKEN as in AES-256
# Step 2: analyze the PKCS#12 first (read-only, no persistence) — confirm contents
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/import-keystore/analyze" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-F "metadata={
\"password\": \"$P12_PASSWORD\"
};type=application/json" \
-F "keystore=@$P12_PATH" \
| jq '{format, aliases, algorithms}'
# → { format: "PKCS12", aliases: ["legacy-key"], algorithms: ["RSA-2048"] }
# Step 3: import into the tenant keystore
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/import-keystore" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-F "metadata={
\"password\": \"$P12_PASSWORD\",
\"kidMappings\": { \"legacy-key\": \"$SRC_KID\" },
\"validationMode\": \"STRICT\"
};type=application/json" \
-F "keystore=@$P12_PATH" \
| jq '{importedKeys, skipped, failed}'
# Step 4: sanity check — encrypt with the legacy RSA
export DATA_B64=$(printf "%s" "Legacy RSA encrypt" | base64 -w0)
curl -s -X POST "$BASE_URL/api/v3/crypto/encrypt" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"kid\":\"$SRC_KID\", \"data\":\"$DATA_B64\" }" | jq .algorithmUsed
# → "RSA-OAEP-256"
# Step 5: rotate the imported RSA into an ML-KEM-768 successor
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/$SRC_KID/rotations" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d "{
\"newKey\": {
\"kid\":\"$SUCC_KID\", \"kty\":\"ML-KEM\", \"algorithm\":\"ML-KEM-768\",
\"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"]
},
\"acknowledgeCapabilityReduction\": true
}" | jq
# Step 6: verify the transparent redirect on encrypt using the ORIGINAL (imported) kid
curl -s -X POST "$BASE_URL/api/v3/crypto/encrypt" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"kid\":\"$SRC_KID\", \"data\":\"$DATA_B64\" }" | jq '{keyRequested, algorithmUsed}'
# → keyRequested: "legacy-rsa-imported" algorithmUsed: "ML-KEM-768+A256GCM"import base64, requests
BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
SRC_KID, SUCC_KID = "legacy-rsa-imported", "mlkem768-modern"
P12_PATH, P12_PASSWORD = "/path/to/legacy-rsa.p12", "the-p12-password"
# Import PKCS#12
with open(P12_PATH, "rb") as f:
p12_bytes = f.read()
# Analyze first (no persistence)
r = requests.post(
f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/import-keystore/analyze",
headers=admin,
files={
"metadata": (None, f'{{"password":"{P12_PASSWORD}"}}', "application/json"),
"keystore": ("legacy.p12", p12_bytes, "application/x-pkcs12"),
},
).json()
print(f"Aliases in P12: {r['aliases']}")
# Import
requests.post(
f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/import-keystore",
headers=admin,
files={
"metadata": (None,
f'{{"password":"{P12_PASSWORD}","kidMappings":{{"legacy-key":"{SRC_KID}"}},'
f'"validationMode":"STRICT"}}', "application/json"),
"keystore": ("legacy.p12", p12_bytes, "application/x-pkcs12"),
},
).raise_for_status()
# Rotate to ML-KEM-768
requests.post(
f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/{SRC_KID}/rotations",
headers=admin, json={
"newKey": {"kid": SUCC_KID, "kty": "ML-KEM", "algorithm": "ML-KEM-768",
"purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"]},
"acknowledgeCapabilityReduction": True,
},
).raise_for_status()
# Verify transparent redirect
enc = requests.post(f"{BASE}/api/v3/crypto/encrypt", headers=app,
json={"kid": SRC_KID,
"data": base64.b64encode(b"post-rotate").decode()}).json()
assert "ML-KEM" in enc["algorithmUsed"]
print(f"Legacy RSA → {enc['algorithmUsed']} (transparent PQC)")import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;
public class Pkcs12ImportMigration {
static final String BASE_URL = "https://staging.ankatech.co";
static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
static final String SRC_KID = "legacy-rsa-imported";
static final String SUCC_KID = "mlkem768-modern";
static final String P12_PATH = "/path/to/legacy-rsa.p12";
static final String P12_PASSWORD = "the-p12-password";
static final HttpClient http = HttpClient.newHttpClient();
static final ObjectMapper json = new ObjectMapper();
static byte[] multipart(String boundary, String metadataJson, byte[] fileBytes,
String filePartName, String fileName) throws Exception {
var out = new java.io.ByteArrayOutputStream();
out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"metadata\""
+ "\r\nContent-Type: application/json\r\n\r\n" + metadataJson + "\r\n").getBytes());
out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + filePartName
+ "\"; filename=\"" + fileName + "\"\r\nContent-Type: application/x-pkcs12\r\n\r\n").getBytes());
out.write(fileBytes);
out.write(("\r\n--" + boundary + "--\r\n").getBytes());
return out.toByteArray();
}
public static void main(String[] args) throws Exception {
String adminAuth = "Bearer " + System.getenv("ADMIN_TOKEN");
String appAuth = "Bearer " + System.getenv("APP_TOKEN");
String boundary = "----" + System.currentTimeMillis();
byte[] p12 = Files.readAllBytes(Path.of(P12_PATH));
// Import
var importBody = multipart(boundary,
String.format("{\"password\":\"%s\",\"kidMappings\":{\"legacy-key\":\"%s\"},"
+ "\"validationMode\":\"STRICT\"}", P12_PASSWORD, SRC_KID),
p12, "keystore", "legacy.p12");
http.send(HttpRequest.newBuilder(URI.create(
BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys/import-keystore"))
.header("Authorization", adminAuth)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(importBody)).build(),
HttpResponse.BodyHandlers.ofString());
// Rotate to ML-KEM-768
http.send(HttpRequest.newBuilder(URI.create(
BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys/" + SRC_KID + "/rotations"))
.header("Authorization", adminAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(String.format("""
{ "newKey": { "kid": "%s", "kty": "ML-KEM", "algorithm": "ML-KEM-768",
"purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"] },
"acknowledgeCapabilityReduction": true }""", SUCC_KID))).build(),
HttpResponse.BodyHandlers.ofString());
// Verify transparent redirect
var dataB64 = Base64.getEncoder().encodeToString("post-rotate".getBytes());
var encResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/encrypt"))
.header("Authorization", appAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{ \"kid\": \"" + SRC_KID + "\", \"data\": \"" + dataB64 + "\" }")).build(),
HttpResponse.BodyHandlers.ofString());
JsonNode enc = json.readTree(encResp.body());
if (!enc.get("algorithmUsed").asText().contains("ML-KEM"))
throw new RuntimeException("Redirect failed");
System.out.println("Legacy RSA → " + enc.get("algorithmUsed").asText() + " (transparent PQC)");
}
}const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const SRC_KID = "legacy-rsa-imported", SUCC_KID = "mlkem768-modern";
const P12_PASSWORD = "the-p12-password";
import { readFileSync } from "node:fs";
import { FormData, Blob } from "node:buffer";
const p12 = readFileSync("/path/to/legacy.p12");
// Import
const importFd = new FormData();
importFd.append("metadata", new Blob([JSON.stringify({
password: P12_PASSWORD,
kidMappings: { "legacy-key": SRC_KID },
validationMode: "STRICT",
})], { type: "application/json" }));
importFd.append("keystore", new Blob([p12], { type: "application/x-pkcs12" }), "legacy.p12");
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys/import-keystore`, {
method: "POST", headers: admin, body: importFd,
});
// Rotate to ML-KEM-768
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys/${SRC_KID}/rotations`, {
method: "POST", headers: jsonH(admin),
body: JSON.stringify({
newKey: { kid: SUCC_KID, kty: "ML-KEM", algorithm: "ML-KEM-768",
purpose: "ENCRYPT_DECRYPT", keyOps: ["encrypt","decrypt"] },
acknowledgeCapabilityReduction: true,
}),
});
// Verify transparent redirect
const enc = await (await fetch(`${BASE_URL}/api/v3/crypto/encrypt`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ kid: SRC_KID, data: Buffer.from("post-rotate").toString("base64") }),
})).json();
if (!enc.algorithmUsed.includes("ML-KEM")) throw new Error("Redirect failed");
console.log(`Legacy RSA → ${enc.algorithmUsed} (transparent PQC)`);using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class Pkcs12ImportMigration
{
const string BASE_URL = "https://staging.ankatech.co";
const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
const string SRC_KID = "legacy-rsa-imported";
const string SUCC_KID = "mlkem768-modern";
const string P12_PATH = "/path/to/legacy-rsa.p12";
const string P12_PASSWORD = "the-p12-password";
static async Task Main()
{
var adminHttp = new HttpClient();
adminHttp.DefaultRequestHeaders.Add("Authorization",
$"Bearer {Environment.GetEnvironmentVariable("ADMIN_TOKEN")}");
var appHttp = new HttpClient();
appHttp.DefaultRequestHeaders.Add("Authorization",
$"Bearer {Environment.GetEnvironmentVariable("APP_TOKEN")}");
var p12 = File.ReadAllBytes(P12_PATH);
// Import PKCS#12 via multipart
var importForm = new MultipartFormDataContent();
importForm.Add(new StringContent(
$"{{\"password\":\"{P12_PASSWORD}\",\"kidMappings\":{{\"legacy-key\":\"{SRC_KID}\"}},"
+ "\"validationMode\":\"STRICT\"}",
Encoding.UTF8, "application/json"), "metadata");
var p12Part = new ByteArrayContent(p12);
p12Part.Headers.ContentType = new MediaTypeHeaderValue("application/x-pkcs12");
importForm.Add(p12Part, "keystore", "legacy.p12");
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys/import-keystore", importForm);
// Rotate to ML-KEM-768
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys/{SRC_KID}/rotations",
new StringContent(JsonSerializer.Serialize(new {
newKey = new {
kid = SUCC_KID, kty = "ML-KEM", algorithm = "ML-KEM-768",
purpose = "ENCRYPT_DECRYPT", keyOps = new[] { "encrypt", "decrypt" },
},
acknowledgeCapabilityReduction = true,
}), Encoding.UTF8, "application/json"));
// Verify transparent redirect
var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("post-rotate"));
var encResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/encrypt",
new StringContent(JsonSerializer.Serialize(new { kid = SRC_KID, data = dataB64 }),
Encoding.UTF8, "application/json"));
var enc = JsonDocument.Parse(await encResp.Content.ReadAsStringAsync()).RootElement;
if (!enc.GetProperty("algorithmUsed").GetString().Contains("ML-KEM"))
throw new Exception("Redirect failed");
Console.WriteLine($"Legacy RSA → {enc.GetProperty("algorithmUsed").GetString()} (transparent PQC)");
}
}Validation modes for import
| Mode | Behavior |
|---|---|
STRICT | Reject expired / invalid certs (default for production) |
IMPORT_ONLY | Accept expired certs, restrict keyOps to [decrypt,verify] |
SKIP | No validation (testing / recovery only) |
Where to go next
- PKCS#7 → JOSE Migration — for legacy CMS payloads, not keystores.
- RSA → ML-KEM Rotation — the rotation step in more detail.
POST /keys/import-keystore— endpoint reference.
Updated 10 days ago
Did this page help you?