P-521 → ML-KEM-768 Re-Encrypt
Re-encrypt an existing JWE from P-521 (ECC) to ML-KEM-768 (PQC), without the plaintext ever leaving the platform.
🔄 P-521 → ML-KEM-768 Re-Encrypt
Scenario: you have JWE tokens encrypted with P-521 (NIST elliptic curve, ECIES). Migrate the ciphertext to ML-KEM-768 (PQC) using the re-encrypt endpoint — the platform decrypts and re-encrypts internally, plaintext never surfaces. Different from key rotation: rotation replaces the key under a stable kid; re-encrypt creates a fresh ciphertext under a different target kid.
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="p521-source-key"
export TGT_KID="mlkem768-target-key"
# ADMIN_TOKEN + APP_TOKEN as in AES-256 recipe
# Step 2: admin provisions both keys
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\":\"EC\", \"algorithm\":\"ECDH-ES+A256KW\",
\"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"] }" | jq
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\":\"$TGT_KID\", \"kty\":\"ML-KEM\", \"algorithm\":\"ML-KEM-768\",
\"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"] }" | jq
# Step 3: app encrypts with the classical key (represents existing ciphertext)
export DATA_B64=$(printf "%s" "Legacy P-521 ciphertext" | base64 -w0)
export SRC_JWE=$(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 -c .jweToken)
# Step 4: re-encrypt to the PQC target — plaintext never surfaces
curl -s -X POST "$BASE_URL/api/v3/crypto/reencrypt" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"jweToken\": $SRC_JWE, \"newKid\":\"$TGT_KID\" }" \
| tee /tmp/re.json | jq '{keyRequested, algorithmUsed, materialVersion}'
# → algorithmUsed: "ML-KEM-768+A256GCM" keyRequested: "mlkem768-target-key"
# Step 5: decrypt the re-encrypted JWE with the target key
curl -s -X POST "$BASE_URL/api/v3/crypto/decrypt" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"jweToken\": $(jq -c .jweToken /tmp/re.json) }" \
| jq -r .decryptedData | base64 -d
# → Legacy P-521 ciphertextimport base64, requests
BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
SRC_KID, TGT_KID = "p521-source-key", "mlkem768-target-key"
# admin / app tokens as in AES-256
for spec in [
{"kid": SRC_KID, "kty": "EC", "algorithm": "ECDH-ES+A256KW"},
{"kid": TGT_KID, "kty": "ML-KEM", "algorithm": "ML-KEM-768"},
]:
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys", headers=admin,
json={**spec, "purpose": "ENCRYPT_DECRYPT",
"keyOps": ["encrypt","decrypt"]}).raise_for_status()
# Encrypt with source key
src = requests.post(f"{BASE}/api/v3/crypto/encrypt", headers=app,
json={"kid": SRC_KID, "data": base64.b64encode(b"Legacy P-521 ciphertext").decode()}).json()
# Re-encrypt to the PQC target
re = requests.post(f"{BASE}/api/v3/crypto/reencrypt", headers=app,
json={"jweToken": src["jweToken"], "newKid": TGT_KID}).json()
print(f"Re-encrypted with {re['algorithmUsed']}")
# Decrypt with target key
dec = requests.post(f"{BASE}/api/v3/crypto/decrypt", headers=app,
json={"jweToken": re["jweToken"]}).json()
assert base64.b64decode(dec["decryptedData"]) == b"Legacy P-521 ciphertext"
print("Re-encrypt round-trip PASSED")import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;
public class P521ToMlKemReencrypt {
static final String BASE_URL = "https://staging.ankatech.co";
static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
static final String SRC_KID = "p521-source-key";
static final String TGT_KID = "mlkem768-target-key";
static final HttpClient http = HttpClient.newHttpClient();
static final ObjectMapper json = new ObjectMapper();
public static void main(String[] args) throws Exception {
// ADMIN_TOKEN + APP_TOKEN acquired as in the AES-256 recipe
String adminAuth = "Bearer " + System.getenv("ADMIN_TOKEN");
String appAuth = "Bearer " + System.getenv("APP_TOKEN");
// Admin creates both keys
for (String[] spec : new String[][] {
{ SRC_KID, "EC", "ECDH-ES+A256KW" },
{ TGT_KID, "ML-KEM", "ML-KEM-768" },
}) {
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(String.format(
"{ \"kid\": \"%s\", \"kty\": \"%s\", \"algorithm\": \"%s\","
+ " \"purpose\": \"ENCRYPT_DECRYPT\", \"keyOps\": [\"encrypt\",\"decrypt\"] }",
spec[0], spec[1], spec[2]))).build(),
HttpResponse.BodyHandlers.ofString());
}
// Encrypt with SRC
var dataB64 = Base64.getEncoder().encodeToString("Legacy P-521 ciphertext".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 src = json.readTree(encResp.body());
// Re-encrypt to TGT (PQC)
var reResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/reencrypt"))
.header("Authorization", appAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(String.format(
"{ \"jweToken\": %s, \"newKid\": \"%s\" }", src.get("jweToken"), TGT_KID))).build(),
HttpResponse.BodyHandlers.ofString());
JsonNode re = json.readTree(reResp.body());
System.out.println("Re-encrypted with " + re.get("algorithmUsed").asText());
// Decrypt with TGT
var decResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/decrypt"))
.header("Authorization", appAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{ \"jweToken\": " + re.get("jweToken") + " }")).build(),
HttpResponse.BodyHandlers.ofString());
String plaintext = new String(Base64.getDecoder().decode(
json.readTree(decResp.body()).get("decryptedData").asText()));
if (!"Legacy P-521 ciphertext".equals(plaintext)) throw new RuntimeException("Fail");
System.out.println("Re-encrypt round-trip PASSED");
}
}const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const SRC_KID = "p521-source-key", TGT_KID = "mlkem768-target-key";
// Admin creates both keys
for (const spec of [
{ kid: SRC_KID, kty: "EC", algorithm: "ECDH-ES+A256KW" },
{ kid: TGT_KID, kty: "ML-KEM", algorithm: "ML-KEM-768" },
]) {
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
method: "POST", headers: jsonH(admin),
body: JSON.stringify({ ...spec, purpose: "ENCRYPT_DECRYPT", keyOps: ["encrypt","decrypt"] }),
});
}
// App encrypts with SRC
const src = await (await fetch(`${BASE_URL}/api/v3/crypto/encrypt`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ kid: SRC_KID, data: Buffer.from("Legacy P-521 ciphertext").toString("base64") }),
})).json();
// Re-encrypt to TGT (PQC)
const re = await (await fetch(`${BASE_URL}/api/v3/crypto/reencrypt`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ jweToken: src.jweToken, newKid: TGT_KID }),
})).json();
console.log(`Re-encrypted with ${re.algorithmUsed}`);
// Decrypt with TGT
const dec = await (await fetch(`${BASE_URL}/api/v3/crypto/decrypt`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ jweToken: re.jweToken }),
})).json();
if (Buffer.from(dec.decryptedData, "base64").toString() !== "Legacy P-521 ciphertext")
throw new Error("Fail");
console.log("Re-encrypt round-trip PASSED");using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class P521ToMlKemReencrypt
{
const string BASE_URL = "https://staging.ankatech.co";
const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
const string SRC_KID = "p521-source-key";
const string TGT_KID = "mlkem768-target-key";
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 both keys
foreach (var spec in new[] {
new { kid = SRC_KID, kty = "EC", algorithm = "ECDH-ES+A256KW" },
new { kid = TGT_KID, kty = "ML-KEM", algorithm = "ML-KEM-768" },
}) {
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
new StringContent(JsonSerializer.Serialize(new {
spec.kid, spec.kty, spec.algorithm,
purpose = "ENCRYPT_DECRYPT", keyOps = new[] { "encrypt", "decrypt" },
}), Encoding.UTF8, "application/json"));
}
// Encrypt with SRC
var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("Legacy P-521 ciphertext"));
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 src = JsonDocument.Parse(await encResp.Content.ReadAsStringAsync()).RootElement;
var srcJwe = src.GetProperty("jweToken").GetRawText();
// Re-encrypt to TGT (PQC)
var reResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/reencrypt",
new StringContent($"{{ \"jweToken\": {srcJwe}, \"newKid\": \"{TGT_KID}\" }}",
Encoding.UTF8, "application/json"));
var re = JsonDocument.Parse(await reResp.Content.ReadAsStringAsync()).RootElement;
Console.WriteLine($"Re-encrypted with {re.GetProperty("algorithmUsed").GetString()}");
// Decrypt with TGT
var reJwe = re.GetProperty("jweToken").GetRawText();
var decResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/decrypt",
new StringContent($"{{ \"jweToken\": {reJwe} }}",
Encoding.UTF8, "application/json"));
var dec = JsonDocument.Parse(await decResp.Content.ReadAsStringAsync()).RootElement;
var plaintext = Encoding.UTF8.GetString(
Convert.FromBase64String(dec.GetProperty("decryptedData").GetString()));
if (plaintext != "Legacy P-521 ciphertext") throw new Exception("Fail");
Console.WriteLine("Re-encrypt round-trip PASSED");
}
}Re-encrypt vs Rotation
| Aspect | Re-encrypt | Rotation |
|---|---|---|
| Endpoint | /api/v3/crypto/reencrypt | /api/v3/admin/…/keys/{kid}/rotations |
| App identity | Application (client_creds) | Admin (password) |
| Target kid | Different kid you specify | Same kid, new material version |
| App code changes | Yes — must reference new kid | No — same kid keeps working |
| Use case | Ciphertext migration in bulk | Zero-touch algorithm change |
Prefer rotation when you can — the app code stays untouched. Re-encrypt is for migrations where you want each ciphertext to be independently addressable under a new kid.
Where to go next
- RSA → ML-KEM Rotation — same PQC migration but with zero app changes.
POST /api/v3/crypto/reencrypt— endpoint reference.
Updated 10 days ago
Did this page help you?