RSA-2048 → ML-KEM-1024 Token Rotation (highest PQC level)
Rotate an RSA-2048 key into ML-KEM-1024 (NIST Level 5 PQC). Same transparent redirect as the ML-KEM-768 variant, but targeting the highest PQC tier.
🔀 RSA-2048 → ML-KEM-1024 Token Rotation (highest PQC level)
Scenario: identical to the RSA → ML-KEM Rotation recipe, except the successor uses ML-KEM-1024 (NIST FIPS 203, security Level 5) instead of ML-KEM-768 (Level 3). Choose this variant when your threat model demands the highest post-quantum tier available today.
Prerequisites
Same two identities as the AES-256 recipe.
Step-by-step
export BASE_URL="https://staging.ankatech.co"
export TENANT_ID="00000000-0000-0000-0000-000000000003"
export SRC_KID="rsa2048-payments-src"
export SUCC_KID="mlkem1024-payments-succ"
# ADMIN_TOKEN + APP_TOKEN as in AES-256 recipe
# Step 2: admin creates RSA-2048 source
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d "{ \"kid\":\"$SRC_KID\", \"kty\":\"RSA\", \"algorithm\":\"RSA-OAEP-256\",
\"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"] }" | jq
# Step 3: admin rotates to ML-KEM-1024 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-1024\",
\"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"]
},
\"acknowledgeCapabilityReduction\": true
}" | jq
# Step 4-5: app encrypts+decrypts using ORIGINAL kid — transparent PQC redirect
export DATA_B64=$(printf "%s" "Level 5 secured" | base64 -w0)
enc=$(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\" }")
echo "algorithmUsed = $(echo "$enc" | jq -r .algorithmUsed)"
# → "ML-KEM-1024+A256GCM"
curl -s -X POST "$BASE_URL/api/v3/crypto/decrypt" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"jweToken\": $(echo "$enc" | jq .jweToken) }" \
| jq -r .decryptedData | base64 -d# Identical to the /docs/rsa-to-ml-kem-rotation recipe.
# The only differences are on the admin's rotation call:
# newKey.kty = "ML-KEM"
# newKey.algorithm = "ML-KEM-1024" # ← Level 5 instead of ML-KEM-768 (Level 3)
# The application code is unchanged.import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;
public class RsaToMlKem1024Rotation {
static final String BASE_URL = "https://staging.ankatech.co";
static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
static final String SRC_KID = "rsa2048-payments-src";
static final String SUCC_KID = "mlkem1024-payments-succ";
static final HttpClient http = HttpClient.newHttpClient();
static final ObjectMapper json = new ObjectMapper();
public static void main(String[] args) throws Exception {
String adminAuth = "Bearer " + System.getenv("ADMIN_TOKEN");
String appAuth = "Bearer " + System.getenv("APP_TOKEN");
// Admin creates RSA-2048 source
http.send(HttpRequest.newBuilder(URI.create(
BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys"))
.header("Authorization", adminAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("""
{ "kid": "%s", "kty": "RSA", "algorithm": "RSA-OAEP-256",
"purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"] }""".formatted(SRC_KID))).build(),
HttpResponse.BodyHandlers.ofString());
// Admin rotates to ML-KEM-1024 successor (Level 5 instead of 3)
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("""
{ "newKey": { "kid": "%s", "kty": "ML-KEM", "algorithm": "ML-KEM-1024",
"purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"] },
"acknowledgeCapabilityReduction": true }""".formatted(SUCC_KID))).build(),
HttpResponse.BodyHandlers.ofString());
// App encrypts using ORIGINAL kid — transparent PQC redirect
var dataB64 = Base64.getEncoder().encodeToString("Level 5 secured".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());
System.out.println("keyRequested=" + enc.get("keyRequested").asText()
+ " algorithmUsed=" + enc.get("algorithmUsed").asText());
}
}// Identical to the /docs/rsa-to-ml-kem-rotation recipe.
// Only changes: newKey.kty="ML-KEM", newKey.algorithm="ML-KEM-1024" on the rotation call.using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class RsaToMlKem1024Rotation
{
const string BASE_URL = "https://staging.ankatech.co";
const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
const string SRC_KID = "rsa2048-payments-src";
const string SUCC_KID = "mlkem1024-payments-succ";
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")}");
// Admin creates RSA-2048 source
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
new StringContent(JsonSerializer.Serialize(new {
kid = SRC_KID, kty = "RSA", algorithm = "RSA-OAEP-256",
purpose = "ENCRYPT_DECRYPT", keyOps = new[] { "encrypt", "decrypt" },
}), Encoding.UTF8, "application/json"));
// Admin rotates to ML-KEM-1024 successor (Level 5)
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-1024",
purpose = "ENCRYPT_DECRYPT", keyOps = new[] { "encrypt", "decrypt" },
},
acknowledgeCapabilityReduction = true,
}), Encoding.UTF8, "application/json"));
// App encrypts using ORIGINAL kid — transparent redirect
var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("Level 5 secured"));
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;
Console.WriteLine($"keyRequested={enc.GetProperty("keyRequested").GetString()}"
+ $" algorithmUsed={enc.GetProperty("algorithmUsed").GetString()}");
}
}Why ML-KEM-1024 vs ML-KEM-768
| Aspect | ML-KEM-768 (Level 3) | ML-KEM-1024 (Level 5) |
|---|---|---|
| Classical equivalent | ~AES-192 | ~AES-256 |
| Public key size | ~1184 bytes | ~1568 bytes |
| Ciphertext size | ~1088 bytes | ~1568 bytes |
| Encapsulation cost | Faster | ~40% slower |
Pick 1024 when regulatory or contractual requirements pin you to NIST Level 5.
Where to go next
- RSA → ML-KEM-768 Rotation — Level 3 counterpart with full explanation.
POST /keys/{kid}/rotations— endpoint reference.
Updated 10 days ago
Did this page help you?