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) }" | jqimport 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
algorithmUsedfield 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
- ML-KEM-1024 Encrypt / Decrypt — PQC-only encryption.
- ML-DSA-87 Sign / Verify — PQC-only signatures.
GET /algorithms?category=HYBRID— discover all supported composite combinations.
Updated 10 days ago
Did this page help you?