Composite Hybrid Keys (X25519+ML-KEM-768 · Ed25519+ML-DSA-44)

Use a single kid that carries BOTH a classical and a post-quantum primitive — the platform combines them per composite JOSE draft.

🛡️ Composite Hybrid Keys (X25519+ML-KEM-768 · Ed25519+ML-DSA-44)

Scenario: provision composite hybrid keys — a single kid that combines a classical primitive with a post-quantum primitive, so any encrypt/sign operation uses both and either alone is sufficient to break neither. This is the CNSA 2.0 / BSI-recommended migration pattern.

Two flavors:

  • COMPOSITE_KEM_COMBINE — for encryption. Combines a classical KEM (e.g. X25519) with a PQC KEM (e.g. ML-KEM-768). The shared secret is derived from both.
  • COMPOSITE_SIGNATURE — for signatures. Emits TWO signatures (classical + PQC); verify passes if both check out.

Output tokens use JWE / JWS General JSON Serialization (not compact) because they carry multiple recipients / signatures.

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 KEM_KID="hybrid-encryption-key"
export SIG_KID="hybrid-signature-key"
# ADMIN_TOKEN + APP_TOKEN as in AES-256

# Step 2a: create composite KEM key (classical + PQC encryption)
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\":       \"$KEM_KID\",
    \"kty\":       \"COMPOSITE_KEM_COMBINE\",
    \"algorithm\": \"X25519+ML-KEM-768\",
    \"purpose\":   \"ENCRYPT_DECRYPT\",
    \"keyOps\":    [\"encrypt\",\"decrypt\"]
  }" | jq

# Step 2b: create composite signature key (classical + PQC signature)
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\":       \"$SIG_KID\",
    \"kty\":       \"COMPOSITE_SIGNATURE\",
    \"algorithm\": \"Ed25519+ML-DSA-44\",
    \"purpose\":   \"SIGN_VERIFY\",
    \"keyOps\":    [\"sign\",\"verify\"]
  }" | jq

# Step 3: encrypt using the composite KEM — response uses JWE General JSON with multiple recipients
export DATA_B64=$(printf "%s" "Hybrid classical+PQC payload" | base64 -w0)
curl -s -X POST "$BASE_URL/api/v3/crypto/encrypt" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"kid\":\"$KEM_KID\", \"data\":\"$DATA_B64\" }" \
  | tee /tmp/enc.json \
  | jq '{keyRequested, algorithmUsed, "recipients_count": (.jweToken.recipients | length)}'
# → algorithmUsed: "X25519+ML-KEM-768+A256GCM"    recipients_count: 2 (X25519 + ML-KEM-768)

# Step 4: decrypt — the platform requires BOTH classical + PQC components to succeed
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/enc.json) }" \
  | jq -r .decryptedData | base64 -d

# Step 5: sign using the composite signature — response has TWO signatures
curl -s -X POST "$BASE_URL/api/v3/crypto/sign" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"kid\":\"$SIG_KID\", \"data\":\"$DATA_B64\" }" \
  | tee /tmp/sig.json \
  | jq '{algorithmUsed, "signatures_count": (.jwsToken.signatures | length)}'
# → algorithmUsed: "Ed25519+ML-DSA-44"    signatures_count: 2

# Step 6: verify — passes only if BOTH signatures are valid
curl -s -X POST "$BASE_URL/api/v3/crypto/verify" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"jwsToken\": $(jq -c .jwsToken /tmp/sig.json) }" | jq
import base64, requests

BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
KEM_KID, SIG_KID = "hybrid-encryption-key", "hybrid-signature-key"

# Admin creates 2 composite keys
for spec in [
    {"kid": KEM_KID, "kty": "COMPOSITE_KEM_COMBINE", "algorithm": "X25519+ML-KEM-768",
     "purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"]},
    {"kid": SIG_KID, "kty": "COMPOSITE_SIGNATURE",   "algorithm": "Ed25519+ML-DSA-44",
     "purpose": "SIGN_VERIFY",     "keyOps": ["sign","verify"]},
]:
    requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys",
                  headers=admin, json=spec).raise_for_status()

# Composite encrypt (2 recipients in the JWE)
data_b64 = base64.b64encode(b"Hybrid classical+PQC payload").decode()
enc = requests.post(f"{BASE}/api/v3/crypto/encrypt", headers=app,
                    json={"kid": KEM_KID, "data": data_b64}).json()
print(f"Encrypted with {enc['algorithmUsed']}, "
      f"recipients={len(enc['jweToken']['recipients'])}")

# Composite decrypt (needs BOTH components)
dec = requests.post(f"{BASE}/api/v3/crypto/decrypt", headers=app,
                    json={"jweToken": enc["jweToken"]}).json()
assert base64.b64decode(dec["decryptedData"]) == b"Hybrid classical+PQC payload"

# Composite sign (2 signatures in the JWS)
sig = requests.post(f"{BASE}/api/v3/crypto/sign", headers=app,
                    json={"kid": SIG_KID, "data": data_b64}).json()
print(f"Signed with {sig['algorithmUsed']}, "
      f"signatures={len(sig['jwsToken']['signatures'])}")

# Composite verify (both must check out)
ver = requests.post(f"{BASE}/api/v3/crypto/verify", headers=app,
                    json={"jwsToken": sig["jwsToken"]}).json()
assert ver["isValid"], "Composite verify failed"
print("Composite round-trip PASSED (encrypt+decrypt and sign+verify)")
import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;

public class CompositeHybridKeys {
    static final String BASE_URL  = "https://staging.ankatech.co";
    static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
    static final String KEM_KID   = "hybrid-encryption-key";
    static final String SIG_KID   = "hybrid-signature-key";

    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");

        // Two composite keys
        for (String[] spec : new String[][] {
            { KEM_KID, "COMPOSITE_KEM_COMBINE", "X25519+ML-KEM-768", "ENCRYPT_DECRYPT", "\"encrypt\",\"decrypt\"" },
            { SIG_KID, "COMPOSITE_SIGNATURE",   "Ed25519+ML-DSA-44", "SIGN_VERIFY",     "\"sign\",\"verify\""    },
        }) {
            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\": \"%s\", \"keyOps\": [%s] }",
                    spec[0], spec[1], spec[2], spec[3], spec[4]))).build(),
                HttpResponse.BodyHandlers.ofString());
        }

        var dataB64 = Base64.getEncoder().encodeToString("Hybrid classical+PQC payload".getBytes());

        // Composite encrypt (2 recipients)
        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\": \"" + KEM_KID + "\", \"data\": \"" + dataB64 + "\" }")).build(),
            HttpResponse.BodyHandlers.ofString());
        JsonNode enc = json.readTree(encResp.body());
        System.out.println("Encrypted: " + enc.get("algorithmUsed").asText()
            + ", recipients=" + enc.get("jweToken").get("recipients").size());

        // Composite decrypt
        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\": " + enc.get("jweToken") + " }")).build(),
            HttpResponse.BodyHandlers.ofString());
        String plaintext = new String(Base64.getDecoder().decode(
            json.readTree(decResp.body()).get("decryptedData").asText()));
        if (!"Hybrid classical+PQC payload".equals(plaintext)) throw new RuntimeException("Decrypt failed");

        // Composite sign (2 signatures)
        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\": \"" + SIG_KID + "\", \"data\": \"" + dataB64 + "\" }")).build(),
            HttpResponse.BodyHandlers.ofString());
        JsonNode sig = json.readTree(sigResp.body());
        System.out.println("Signed: " + sig.get("algorithmUsed").asText()
            + ", signatures=" + sig.get("jwsToken").get("signatures").size());

        // Composite verify
        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\": " + sig.get("jwsToken") + " }")).build(),
            HttpResponse.BodyHandlers.ofString());
        if (!json.readTree(verResp.body()).get("isValid").asBoolean())
            throw new RuntimeException("Verify failed");
        System.out.println("Composite round-trip PASSED");
    }
}
const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const KEM_KID = "hybrid-encryption-key", SIG_KID = "hybrid-signature-key";

for (const spec of [
  { kid: KEM_KID, kty: "COMPOSITE_KEM_COMBINE", algorithm: "X25519+ML-KEM-768",
    purpose: "ENCRYPT_DECRYPT", keyOps: ["encrypt","decrypt"] },
  { kid: SIG_KID, kty: "COMPOSITE_SIGNATURE",   algorithm: "Ed25519+ML-DSA-44",
    purpose: "SIGN_VERIFY",     keyOps: ["sign","verify"] },
]) {
  await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
    method: "POST", headers: jsonH(admin), body: JSON.stringify(spec),
  });
}

const dataB64 = Buffer.from("Hybrid classical+PQC payload").toString("base64");

// Composite encrypt / decrypt
const enc = await (await fetch(`${BASE_URL}/api/v3/crypto/encrypt`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ kid: KEM_KID, data: dataB64 }),
})).json();
console.log(`Encrypted: ${enc.algorithmUsed}, recipients=${enc.jweToken.recipients.length}`);

const dec = await (await fetch(`${BASE_URL}/api/v3/crypto/decrypt`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ jweToken: enc.jweToken }),
})).json();
if (Buffer.from(dec.decryptedData, "base64").toString() !== "Hybrid classical+PQC payload")
  throw new Error("Decrypt failed");

// Composite sign / verify
const sig = await (await fetch(`${BASE_URL}/api/v3/crypto/sign`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ kid: SIG_KID, data: dataB64 }),
})).json();
console.log(`Signed: ${sig.algorithmUsed}, signatures=${sig.jwsToken.signatures.length}`);

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("Composite round-trip PASSED");
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class CompositeHybridKeys
{
    const string BASE_URL  = "https://staging.ankatech.co";
    const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
    const string KEM_KID   = "hybrid-encryption-key";
    const string SIG_KID   = "hybrid-signature-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")}");

        // Two composite keys
        foreach (var spec in new[] {
            new { kid = KEM_KID, kty = "COMPOSITE_KEM_COMBINE", algorithm = "X25519+ML-KEM-768",
                  purpose = "ENCRYPT_DECRYPT", keyOps = new[] { "encrypt", "decrypt" } },
            new { kid = SIG_KID, kty = "COMPOSITE_SIGNATURE",   algorithm = "Ed25519+ML-DSA-44",
                  purpose = "SIGN_VERIFY",     keyOps = new[] { "sign", "verify" } },
        }) {
            await adminHttp.PostAsync(
                $"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
                new StringContent(JsonSerializer.Serialize(spec),
                                  Encoding.UTF8, "application/json"));
        }

        var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("Hybrid classical+PQC payload"));

        // Composite encrypt / decrypt
        var encResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/encrypt",
            new StringContent(JsonSerializer.Serialize(new { kid = KEM_KID, data = dataB64 }),
                              Encoding.UTF8, "application/json"));
        var enc = JsonDocument.Parse(await encResp.Content.ReadAsStringAsync()).RootElement;
        Console.WriteLine($"Encrypted: {enc.GetProperty("algorithmUsed").GetString()}"
            + $", recipients={enc.GetProperty("jweToken").GetProperty("recipients").GetArrayLength()}");

        var jweToken = enc.GetProperty("jweToken").GetRawText();
        var decResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/decrypt",
            new StringContent($"{{ \"jweToken\": {jweToken} }}",
                              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 != "Hybrid classical+PQC payload") throw new Exception("Decrypt failed");

        // Composite sign / verify
        var sigResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/sign",
            new StringContent(JsonSerializer.Serialize(new { kid = SIG_KID, data = dataB64 }),
                              Encoding.UTF8, "application/json"));
        var sig = JsonDocument.Parse(await sigResp.Content.ReadAsStringAsync()).RootElement;
        Console.WriteLine($"Signed: {sig.GetProperty("algorithmUsed").GetString()}"
            + $", signatures={sig.GetProperty("jwsToken").GetProperty("signatures").GetArrayLength()}");

        var jwsToken = sig.GetProperty("jwsToken").GetRawText();
        var verResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/verify",
            new StringContent($"{{ \"jwsToken\": {jwsToken} }}",
                              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("Composite round-trip PASSED");
    }
}

Why hybrid

  • Defense in depth: an attacker must break BOTH the classical and the PQC primitive to compromise the operation. Today's classical algorithms are proven; PQC is standardised but relatively new — hedging both is the CNSA 2.0 recommendation.
  • Downgrade resistance: the platform's algorithmUsed field always names both components — an intermediary cannot silently strip the PQC half.
  • Zero-touch migration: apps written against composite kids need no code change if BSI/ANSSI advisories later mandate PQC-only or classical-only.

Where to go next


Did this page help you?