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


Did this page help you?