ML-DSA-87 Sign / Verify (post-quantum signatures)
Post-quantum digital signatures with ML-DSA-87 (FIPS 204, formerly Dilithium-5). Sign a payload, verify the JWS, all quantum-safe.
✍️ ML-DSA-87 Sign / Verify (post-quantum signatures)
Scenario: provision an ML-DSA-87 key (NIST FIPS 204 post-quantum signature, security level 5, formerly Dilithium-5) and use it to sign a payload as a JWS token, then verify the signature. Fully quantum-safe today.
Prerequisites
Same two identities as the AES-256 recipe. The app identity needs runtime permissions for crypto.sign + crypto.verify — ask your admin.
Step-by-step
export BASE_URL="https://staging.ankatech.co"
export ADMIN_EMAIL="[email protected]" ADMIN_PASSWORD="••••••••••••"
export TENANT_ID="00000000-0000-0000-0000-000000000003"
export CLIENT_ID="00000000-0000-0000-0000-000000000000"
export CLIENT_SECRET="••••••••••••••••••••••••••••••••"
export KID="my-mldsa87-key"
# Step 1: get both tokens
export ADMIN_TOKEN=$(curl -s -X POST "$BASE_URL/api/v3/auth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" --data-urlencode "username=$ADMIN_EMAIL" \
--data-urlencode "password=$ADMIN_PASSWORD" --data-urlencode "tenant_id=$TENANT_ID" \
| jq -r .access_token)
export APP_TOKEN=$(curl -s -X POST "$BASE_URL/api/v3/auth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=$CLIENT_ID" --data-urlencode "client_secret=$CLIENT_SECRET" \
| jq -r .access_token)
# Step 2: admin creates ML-DSA-87 signing key
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\":\"$KID\", \"kty\":\"ML-DSA\", \"algorithm\":\"ML-DSA-87\",
\"purpose\":\"SIGN_VERIFY\", \"keyOps\":[\"sign\",\"verify\"],
\"exportable\": true
}" | jq
# Step 3: app signs — returns a JWS (compact serialization for simple keys)
export DATA_B64=$(printf "%s" "Contract v2 — signed by acme-app" | base64 -w0)
curl -s -X POST "$BASE_URL/api/v3/crypto/sign" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"kid\":\"$KID\", \"data\":\"$DATA_B64\" }" \
| tee /tmp/sign.json | jq '{algorithmUsed, keyRequested}'
# Step 4: app verifies the JWS — the platform resolves the public key from `kid`
export JWS=$(jq -r .jwsToken /tmp/sign.json)
curl -s -X POST "$BASE_URL/api/v3/crypto/verify" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d "{ \"jwsToken\": \"$JWS\" }" | jq
# → { "isValid": true, "algorithmUsed": "ML-DSA-87", ... }import base64, requests
BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
KID = "my-mldsa87-key"
def tok(f): return requests.post(f"{BASE}/api/v3/auth/token", data=f).json()["access_token"]
admin = {"Authorization": f"Bearer {tok({'grant_type':'password','username':'[email protected]','password':'•••','tenant_id':TENANT_ID})}"}
app = {"Authorization": f"Bearer {tok({'grant_type':'client_credentials','client_id':'…','client_secret':'…'})}"}
# Admin creates ML-DSA-87 signing key
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys", headers=admin, json={
"kid": KID, "kty": "ML-DSA", "algorithm": "ML-DSA-87",
"purpose": "SIGN_VERIFY", "keyOps": ["sign","verify"], "exportable": True,
}).raise_for_status()
# App signs
data_b64 = base64.b64encode(b"Contract v2 — signed by acme-app").decode()
sig = requests.post(f"{BASE}/api/v3/crypto/sign", headers=app,
json={"kid": KID, "data": data_b64}).json()
print("Signed with:", sig["algorithmUsed"])
# App verifies
ver = requests.post(f"{BASE}/api/v3/crypto/verify", headers=app,
json={"jwsToken": sig["jwsToken"]}).json()
assert ver["isValid"], "Signature verification failed"
print("Verified OK — signature is valid")import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;
public class MlDsa87SignVerify {
static final String BASE_URL = "https://staging.ankatech.co";
static final String ADMIN_EMAIL = "[email protected]";
static final String ADMIN_PASSWORD = "••••••••••••";
static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
static final String CLIENT_ID = "00000000-0000-0000-0000-000000000000";
static final String CLIENT_SECRET = "••••••••••••••••••••••••••••••••";
static final String KID = "my-mldsa87-key";
static final HttpClient http = HttpClient.newHttpClient();
static final ObjectMapper json = new ObjectMapper();
static String token(String body) throws Exception {
var r = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/auth/token"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(),
HttpResponse.BodyHandlers.ofString());
return json.readTree(r.body()).get("access_token").asText();
}
public static void main(String[] args) throws Exception {
var adminAuth = "Bearer " + token("grant_type=password&username=" + ADMIN_EMAIL
+ "&password=" + ADMIN_PASSWORD + "&tenant_id=" + TENANT_ID);
var appAuth = "Bearer " + token("grant_type=client_credentials&client_id="
+ CLIENT_ID + "&client_secret=" + CLIENT_SECRET);
// Admin creates ML-DSA-87 signing key
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": "ML-DSA", "algorithm": "ML-DSA-87",
"purpose": "SIGN_VERIFY", "keyOps": ["sign","verify"],
"exportable": true }""".formatted(KID))).build(),
HttpResponse.BodyHandlers.ofString());
// App signs
var dataB64 = Base64.getEncoder().encodeToString(
"Contract v2 — signed by acme-app".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\": \"" + KID + "\", \"data\": \"" + dataB64 + "\" }")).build(),
HttpResponse.BodyHandlers.ofString());
JsonNode sig = json.readTree(sigResp.body());
System.out.println("Signed with: " + sig.get("algorithmUsed").asText());
// App verifies
var jws = sig.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\": \"" + jws + "\" }")).build(),
HttpResponse.BodyHandlers.ofString());
JsonNode ver = json.readTree(verResp.body());
if (!ver.get("isValid").asBoolean()) throw new RuntimeException("Verify failed");
System.out.println("Verified OK");
}
}const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const KID = "my-mldsa87-key";
const tok = async (f) => (await (await fetch(`${BASE_URL}/api/v3/auth/token`, {
method: "POST", headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: new URLSearchParams(f)})).json()).access_token;
const admin = { "Authorization": `Bearer ${await tok({grant_type:"password", username:"[email protected]", password:"•••", tenant_id:TENANT_ID})}` };
const app = { "Authorization": `Bearer ${await tok({grant_type:"client_credentials", client_id:"…", client_secret:"…"})}` };
const jsonH = (a) => ({ ...a, "Content-Type": "application/json" });
// Admin creates ML-DSA-87
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
method: "POST", headers: jsonH(admin),
body: JSON.stringify({
kid: KID, kty: "ML-DSA", algorithm: "ML-DSA-87",
purpose: "SIGN_VERIFY", keyOps: ["sign","verify"], exportable: true,
}),
});
// App signs
const dataB64 = Buffer.from("Contract v2 — signed by acme-app").toString("base64");
const sig = await (await fetch(`${BASE_URL}/api/v3/crypto/sign`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ kid: KID, data: dataB64 }),
})).json();
console.log("Signed with:", sig.algorithmUsed);
// App verifies
const ver = await (await fetch(`${BASE_URL}/api/v3/crypto/verify`, {
method: "POST", headers: jsonH(app),
body: JSON.stringify({ jwsToken: sig.jwsToken }),
})).json();
if (!ver.isValid) throw new Error("Verify failed");
console.log("Verified OK");using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class MlDsa87SignVerify
{
const string BASE_URL = "https://staging.ankatech.co";
const string ADMIN_EMAIL = "[email protected]";
const string ADMIN_PASSWORD = "••••••••••••";
const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
const string CLIENT_ID = "00000000-0000-0000-0000-000000000000";
const string CLIENT_SECRET = "••••••••••••••••••••••••••••••••";
const string KID = "my-mldsa87-key";
static async Task<string> GetToken(HttpClient http, IEnumerable<KeyValuePair<string,string>> fields)
{
var r = await http.PostAsync($"{BASE_URL}/api/v3/auth/token",
new FormUrlEncodedContent(fields));
return JsonDocument.Parse(await r.Content.ReadAsStringAsync())
.RootElement.GetProperty("access_token").GetString();
}
static async Task Main()
{
var http = new HttpClient();
var adminToken = await GetToken(http, new[] {
new KeyValuePair<string,string>("grant_type", "password"),
new KeyValuePair<string,string>("username", ADMIN_EMAIL),
new KeyValuePair<string,string>("password", ADMIN_PASSWORD),
new KeyValuePair<string,string>("tenant_id", TENANT_ID),
});
var appToken = await GetToken(http, new[] {
new KeyValuePair<string,string>("grant_type", "client_credentials"),
new KeyValuePair<string,string>("client_id", CLIENT_ID),
new KeyValuePair<string,string>("client_secret", CLIENT_SECRET),
});
var adminHttp = new HttpClient();
adminHttp.DefaultRequestHeaders.Add("Authorization", $"Bearer {adminToken}");
var appHttp = new HttpClient();
appHttp.DefaultRequestHeaders.Add("Authorization", $"Bearer {appToken}");
// Admin creates ML-DSA-87 signing key
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
new StringContent(JsonSerializer.Serialize(new {
kid = KID, kty = "ML-DSA", algorithm = "ML-DSA-87",
purpose = "SIGN_VERIFY", keyOps = new[] { "sign", "verify" },
exportable = true,
}), Encoding.UTF8, "application/json"));
// App signs
var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("Contract v2 — signed by acme-app"));
var sigResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/sign",
new StringContent(JsonSerializer.Serialize(new { kid = KID, data = dataB64 }),
Encoding.UTF8, "application/json"));
var sig = JsonDocument.Parse(await sigResp.Content.ReadAsStringAsync()).RootElement;
Console.WriteLine($"Signed with: {sig.GetProperty("algorithmUsed").GetString()}");
// App verifies
var jws = sig.GetProperty("jwsToken").GetString();
var verResp = await appHttp.PostAsync(
$"{BASE_URL}/api/v3/crypto/verify",
new StringContent(JsonSerializer.Serialize(new { jwsToken = jws }),
Encoding.UTF8, "application/json"));
var ver = JsonDocument.Parse(await verResp.Content.ReadAsStringAsync()).RootElement;
if (!ver.GetProperty("isValid").GetBoolean()) throw new Exception("Verify failed");
Console.WriteLine("Verified OK");
}
}Why ML-DSA-87
- Standard: NIST FIPS 204 (2024), formerly Dilithium-5
- Security level: NIST Level 5 — the highest tier
- Design: post-quantum secure — resistant to Shor's and future quantum attacks
- Trade-off: larger signatures (~4.6 KB) vs classical (RSA ~256 B, ECDSA ~64 B) — acceptable for most workloads
For classical-to-PQC signature migration, see Re-Sign RSA → ML-DSA (PQC migration).
Where to go next
- Re-Sign RSA → ML-DSA (PQC migration) — migrate existing signatures.
- Sign-then-encrypt nested — combine sign + encrypt in one call.
POST /api/v3/crypto/sign·/verify— endpoint refs.
Updated 10 days ago
Did this page help you?