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

ModeBehavior
STRICTReject expired / invalid certs (default for production)
IMPORT_ONLYAccept expired certs, restrict keyOps to [decrypt,verify]
SKIPNo validation (testing / recovery only)

Where to go next


Did this page help you?