Re-Sign RSA-2048 → ML-DSA-65 (PQC signature migration)
Migrate an existing JWS signed with RSA-2048 to a fresh ML-DSA-65 signature — payload bytes preserved. The parallel of key rotation but for signatures, not envelopes.
✍️ Re-Sign RSA-2048 → ML-DSA-65 (PQC signature migration)
Scenario: you have an existing JWS token signed with RSA-2048 — perhaps stored in a database, embedded in a document, distributed to counterparties. Your PQC deadline arrives. Re-sign the payload with ML-DSA-65 (NIST FIPS 204, Level 3), producing a new JWS with the same payload bytes but a post-quantum signature. No re-transmission of the payload required.
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 OLD_KID="rsa2048-legacy-sig"
export NEW_KID="mldsa65-successor-sig"
# ADMIN_TOKEN and APP_TOKEN acquired as in the 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\":\"$OLD_KID\", \"kty\":\"RSA\", \"algorithm\":\"RS256\",
\"purpose\":\"SIGN_VERIFY\", \"keyOps\":[\"sign\",\"verify\"] }" | 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\":\"$NEW_KID\", \"kty\":\"ML-DSA\", \"algorithm\":\"ML-DSA-65\",
\"purpose\":\"SIGN_VERIFY\", \"keyOps\":[\"sign\",\"verify\"] }" | jq
# Step 3: app signs with the OLD (RSA) key — this represents the legacy JWS
export DATA_B64=$(printf "%s" "Long-lived contract — v1" | base64 -w0)
export OLD_JWS=$(curl -s -X POST "$BASE_URL/api/v3/crypto/sign" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"kid\":\"$OLD_KID\", \"data\":\"$DATA_B64\" }" | jq -r .jwsToken)
# Step 4: re-sign the JWS with the NEW (ML-DSA-65) key
# The endpoint verifies the incoming JWS under OLD_KID, then produces
# a fresh JWS with the same payload under NEW_KID.
curl -s -X POST "$BASE_URL/api/v3/crypto/resign" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"jwsToken\":\"$OLD_JWS\", \"newKid\":\"$NEW_KID\" }" \
| tee /tmp/resigned.json | jq '{algorithmUsed, keyRequested}'
# → algorithmUsed: "ML-DSA-65" keyRequested: "mldsa65-successor-sig"
# Step 5: verify the NEW JWS
export NEW_JWS=$(jq -r .jwsToken /tmp/resigned.json)
curl -s -X POST "$BASE_URL/api/v3/crypto/verify" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"jwsToken\":\"$NEW_JWS\" }" | jq
# → { "isValid": true, "algorithmUsed": "ML-DSA-65", ... }import base64, requests
BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
OLD_KID, NEW_KID = "rsa2048-legacy-sig", "mldsa65-successor-sig"
# admin / app tokens as in the AES-256 recipe
# Admin provisions both keys
for spec in [
{"kid": OLD_KID, "kty": "RSA", "algorithm": "RS256"},
{"kid": NEW_KID, "kty": "ML-DSA", "algorithm": "ML-DSA-65"},
]:
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys", headers=admin,
json={**spec, "purpose": "SIGN_VERIFY", "keyOps": ["sign","verify"]}
).raise_for_status()
# App signs with legacy RSA
data_b64 = base64.b64encode(b"Long-lived contract - v1").decode()
old_jws = requests.post(f"{BASE}/api/v3/crypto/sign", headers=app,
json={"kid": OLD_KID, "data": data_b64}).json()["jwsToken"]
# Re-sign into ML-DSA-65
resigned = requests.post(f"{BASE}/api/v3/crypto/resign", headers=app,
json={"jwsToken": old_jws, "newKid": NEW_KID}).json()
new_jws = resigned["jwsToken"]
print(f"Re-signed as {resigned['algorithmUsed']}")
# Verify the NEW signature
ver = requests.post(f"{BASE}/api/v3/crypto/verify", headers=app,
json={"jwsToken": new_jws}).json()
assert ver["isValid"] and ver["algorithmUsed"] == "ML-DSA-65"
print("PQC re-sign PASSED")import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;
public class ReSignRsaToMlDsa {
static final String BASE_URL = "https://staging.ankatech.co";
static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
static final String OLD_KID = "rsa2048-legacy-sig";
static final String NEW_KID = "mldsa65-successor-sig";
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 provisions both keys
for (String[] spec : new String[][] {
{ OLD_KID, "RSA", "RS256" },
{ NEW_KID, "ML-DSA", "ML-DSA-65" },
}) {
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\": \"SIGN_VERIFY\", \"keyOps\": [\"sign\",\"verify\"] }",
spec[0], spec[1], spec[2]))).build(),
HttpResponse.BodyHandlers.ofString());
}
// Sign with legacy RSA
var dataB64 = Base64.getEncoder().encodeToString("Long-lived contract - v1".getBytes());
var sigResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/sign"))
.header("Authorization", appAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{ \"kid\": \"" + OLD_KID + "\", \"data\": \"" + dataB64 + "\" }")).build(),
HttpResponse.BodyHandlers.ofString());
String oldJws = json.readTree(sigResp.body()).get("jwsToken").asText();
// Re-sign into ML-DSA-65
var reResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/resign"))
.header("Authorization", appAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(String.format(
"{ \"jwsToken\": \"%s\", \"newKid\": \"%s\" }", oldJws, NEW_KID))).build(),
HttpResponse.BodyHandlers.ofString());
JsonNode re = json.readTree(reResp.body());
System.out.println("Re-signed as " + re.get("algorithmUsed").asText());
// Verify the NEW signature
String newJws = re.get("jwsToken").asText();
var verResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/verify"))
.header("Authorization", appAuth).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{ \"jwsToken\": \"" + newJws + "\" }")).build(),
HttpResponse.BodyHandlers.ofString());
JsonNode ver = json.readTree(verResp.body());
if (!ver.get("isValid").asBoolean() || !ver.get("algorithmUsed").asText().equals("ML-DSA-65"))
throw new RuntimeException("PQC re-sign failed");
System.out.println("PQC re-sign PASSED");
}
}const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const OLD_KID = "rsa2048-legacy-sig", NEW_KID = "mldsa65-successor-sig";
// admin / app / jsonH as in AES-256 recipe
// Admin provisions both keys
for (const spec of [
{ kid: OLD_KID, kty: "RSA", algorithm: "RS256" },
{ kid: NEW_KID, kty: "ML-DSA", algorithm: "ML-DSA-65" },
]) {
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
method: "POST", headers: jsonH(admin),
body: JSON.stringify({ ...spec, purpose: "SIGN_VERIFY", keyOps: ["sign","verify"] }),
});
}
// App signs with legacy RSA
const dataB64 = Buffer.from("Long-lived contract - v1").toString("base64");
const oldJws = (await (await fetch(`${BASE_URL}/api/v3/crypto/sign`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ kid: OLD_KID, data: dataB64 }),
})).json()).jwsToken;
// Re-sign into ML-DSA-65
const resigned = await (await fetch(`${BASE_URL}/api/v3/crypto/resign`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ jwsToken: oldJws, newKid: NEW_KID }),
})).json();
console.log(`Re-signed as ${resigned.algorithmUsed}`);
// Verify NEW signature
const ver = await (await fetch(`${BASE_URL}/api/v3/crypto/verify`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ jwsToken: resigned.jwsToken }),
})).json();
if (!ver.isValid || ver.algorithmUsed !== "ML-DSA-65") throw new Error("Fail");
console.log("PQC re-sign PASSED");using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class ReSignRsaToMlDsa
{
const string BASE_URL = "https://staging.ankatech.co";
const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
const string OLD_KID = "rsa2048-legacy-sig";
const string NEW_KID = "mldsa65-successor-sig";
static async Task Main()
{
// ADMIN_TOKEN + APP_TOKEN acquired as in the AES-256 recipe
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 provisions both keys
foreach (var spec in new[] {
new { kid = OLD_KID, kty = "RSA", algorithm = "RS256" },
new { kid = NEW_KID, kty = "ML-DSA", algorithm = "ML-DSA-65" },
}) {
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
new StringContent(JsonSerializer.Serialize(new {
spec.kid, spec.kty, spec.algorithm,
purpose = "SIGN_VERIFY", keyOps = new[] { "sign", "verify" },
}), Encoding.UTF8, "application/json"));
}
// Sign with legacy RSA
var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("Long-lived contract - v1"));
var sigResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/sign",
new StringContent(JsonSerializer.Serialize(new { kid = OLD_KID, data = dataB64 }),
Encoding.UTF8, "application/json"));
var oldJws = JsonDocument.Parse(await sigResp.Content.ReadAsStringAsync())
.RootElement.GetProperty("jwsToken").GetString();
// Re-sign into ML-DSA-65
var reResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/resign",
new StringContent(JsonSerializer.Serialize(new { jwsToken = oldJws, newKid = NEW_KID }),
Encoding.UTF8, "application/json"));
var re = JsonDocument.Parse(await reResp.Content.ReadAsStringAsync()).RootElement;
Console.WriteLine($"Re-signed as {re.GetProperty("algorithmUsed").GetString()}");
// Verify NEW signature
var newJws = re.GetProperty("jwsToken").GetString();
var verResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/verify",
new StringContent(JsonSerializer.Serialize(new { jwsToken = newJws }),
Encoding.UTF8, "application/json"));
var ver = JsonDocument.Parse(await verResp.Content.ReadAsStringAsync()).RootElement;
if (!ver.GetProperty("isValid").GetBoolean()
|| ver.GetProperty("algorithmUsed").GetString() != "ML-DSA-65")
throw new Exception("PQC re-sign failed");
Console.WriteLine("PQC re-sign PASSED");
}
}What just happened
- The
re-signendpoint is a two-step operation the platform runs atomically: verify the incoming JWS underOLD_KID, then produce a fresh JWS with the same payload bytes underNEW_KID. - Payload never leaves the platform — you send only the JWS token, not the raw content.
- The old JWS remains valid — this recipe doesn't invalidate history, it just produces a PQC-signed alternative.
Where to go next
- RSA → ML-KEM Rotation — the encryption counterpart (same idea, KEM instead of signature).
POST /api/v3/crypto/resign— endpoint reference.- ML-DSA-87 Sign / Verify — PQC signatures from scratch.
Updated 10 days ago
Did this page help you?