B2B Public Key Distribution (ML-KEM-768 + ML-DSA-87)
Publish YOUR public keys so partners can encrypt for you or verify your signatures — ML-KEM-768 (KEM) + ML-DSA-87 (Sign) PQC pair.
🤝 B2B Public Key Distribution (ML-KEM-768 + ML-DSA-87)
Scenario: you want to publish two PQC public keys to a partner so they can (a) encrypt for you with ML-KEM-768 and (b) verify your signatures with ML-DSA-87. Provision the keys in your tenant, export the public halves, and hand them over. The partner then uses the External Key Interoperability endpoints on their side — no cross-tenant provisioning required.
Prerequisites
Same two identities as the AES-256 recipe. Admin needs admin.keys.read (for export).
Step-by-step
export BASE_URL="https://staging.ankatech.co"
export TENANT_ID="00000000-0000-0000-0000-000000000003"
export KEM_KID="pubkey-shared-encryption"
export SIG_KID="pubkey-shared-signature"
# ADMIN_TOKEN + APP_TOKEN as in AES-256
# Step 2: admin provisions the two PQC keys (exportable=true is REQUIRED for public export)
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\":\"ML-KEM\", \"algorithm\":\"ML-KEM-768\",
\"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"],
\"exportable\": true }" | 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\":\"$SIG_KID\", \"kty\":\"ML-DSA\", \"algorithm\":\"ML-DSA-87\",
\"purpose\":\"SIGN_VERIFY\", \"keyOps\":[\"sign\",\"verify\"],
\"exportable\": true }" | jq
# Step 3: export the public keys — for handing to the partner
curl -s -X GET "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/$KEM_KID/export" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
| tee /tmp/kem-pubkey.json | jq '{kid, algorithm, publicKey}'
curl -s -X GET "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/$SIG_KID/export" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
| tee /tmp/sig-pubkey.json | jq '{kid, algorithm, publicKey}'
# Step 4: package for the partner (JSON bundle they can import into their side)
jq -n \
--slurpfile kem /tmp/kem-pubkey.json \
--slurpfile sig /tmp/sig-pubkey.json \
'{
tenant: "acme-corp",
keys: {
encryption: { kty: $kem[0].kty, algorithm: $kem[0].algorithm,
kid: $kem[0].kid, publicKey: $kem[0].publicKey },
signature: { kty: $sig[0].kty, algorithm: $sig[0].algorithm,
kid: $sig[0].kid, publicKey: $sig[0].publicKey }
}
}' > /tmp/partner-bundle.json
cat /tmp/partner-bundle.json | jq
# Send /tmp/partner-bundle.json to the partner over your secure channel.The partner's side (for reference)
The partner uses External Key Interoperability:
-
To encrypt a payload for you:
POST /api/v3/interoperability/encryptwithmetadata.publicKey = <your KEM publicKey>.
They send you the resulting envelope; youPOST /api/v3/crypto/decrypton your side withkid = pubkey-shared-encryption. -
To verify a JWS you signed:
POST /api/v3/interoperability/verifywithmetadata.publicKey = <your SIG publicKey>and the JWS you sent.
import requests
BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
KEM_KID, SIG_KID = "pubkey-shared-encryption", "pubkey-shared-signature"
# Admin provisions both keys (exportable=True required)
for spec in [
{"kid": KEM_KID, "kty": "ML-KEM", "algorithm": "ML-KEM-768",
"purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"], "exportable": True},
{"kid": SIG_KID, "kty": "ML-DSA", "algorithm": "ML-DSA-87",
"purpose": "SIGN_VERIFY", "keyOps": ["sign","verify"], "exportable": True},
]:
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys",
headers=admin, json=spec).raise_for_status()
# Export the public halves for the partner
kem = requests.get(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/{KEM_KID}/export",
headers=admin).json()
sig = requests.get(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/{SIG_KID}/export",
headers=admin).json()
# Package a partner bundle
bundle = {
"tenant": "acme-corp",
"keys": {
"encryption": {"kty": kem["kty"], "algorithm": kem["algorithm"],
"kid": kem["kid"], "publicKey": kem["publicKey"]},
"signature": {"kty": sig["kty"], "algorithm": sig["algorithm"],
"kid": sig["kid"], "publicKey": sig["publicKey"]},
},
}
import json; open("/tmp/partner-bundle.json","w").write(json.dumps(bundle, indent=2))
print(f"Partner bundle ready — KEM alg={kem['algorithm']}, SIG alg={sig['algorithm']}")import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
public class B2bPublicKeyDistribution {
static final String BASE_URL = "https://staging.ankatech.co";
static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
static final String KEM_KID = "pubkey-shared-encryption";
static final String SIG_KID = "pubkey-shared-signature";
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");
// Admin provisions both keys (exportable=true required)
for (String[] spec : new String[][] {
{ KEM_KID, "ML-KEM", "ML-KEM-768", "ENCRYPT_DECRYPT", "\"encrypt\",\"decrypt\"" },
{ SIG_KID, "ML-DSA", "ML-DSA-87", "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], \"exportable\": true }",
spec[0], spec[1], spec[2], spec[3], spec[4]))).build(),
HttpResponse.BodyHandlers.ofString());
}
// Export public keys
JsonNode kem = json.readTree(http.send(HttpRequest.newBuilder(URI.create(
BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys/" + KEM_KID + "/export"))
.header("Authorization", adminAuth).GET().build(),
HttpResponse.BodyHandlers.ofString()).body());
JsonNode sig = json.readTree(http.send(HttpRequest.newBuilder(URI.create(
BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys/" + SIG_KID + "/export"))
.header("Authorization", adminAuth).GET().build(),
HttpResponse.BodyHandlers.ofString()).body());
// Package partner bundle
ObjectNode bundle = json.createObjectNode();
bundle.put("tenant", "acme-corp");
ObjectNode keys = bundle.putObject("keys");
for (String[] pair : new String[][] { {"encryption", KEM_KID}, {"signature", SIG_KID} }) {
JsonNode src = pair[1].equals(KEM_KID) ? kem : sig;
ObjectNode k = keys.putObject(pair[0]);
k.put("kty", src.get("kty").asText());
k.put("algorithm", src.get("algorithm").asText());
k.put("kid", src.get("kid").asText());
k.put("publicKey", src.get("publicKey").asText());
}
Files.writeString(Path.of("/tmp/partner-bundle.json"),
json.writerWithDefaultPrettyPrinter().writeValueAsString(bundle));
System.out.println("Bundle ready — KEM=" + kem.get("algorithm").asText()
+ ", SIG=" + sig.get("algorithm").asText());
}
}const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const KEM_KID = "pubkey-shared-encryption", SIG_KID = "pubkey-shared-signature";
for (const spec of [
{ kid: KEM_KID, kty: "ML-KEM", algorithm: "ML-KEM-768",
purpose: "ENCRYPT_DECRYPT", keyOps: ["encrypt","decrypt"], exportable: true },
{ kid: SIG_KID, kty: "ML-DSA", algorithm: "ML-DSA-87",
purpose: "SIGN_VERIFY", keyOps: ["sign","verify"], exportable: true },
]) {
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
method: "POST", headers: jsonH(admin), body: JSON.stringify(spec),
});
}
const kem = await (await fetch(
`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys/${KEM_KID}/export`,
{ headers: admin })).json();
const sig = await (await fetch(
`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys/${SIG_KID}/export`,
{ headers: admin })).json();
const bundle = {
tenant: "acme-corp",
keys: {
encryption: { kty: kem.kty, algorithm: kem.algorithm, kid: kem.kid, publicKey: kem.publicKey },
signature: { kty: sig.kty, algorithm: sig.algorithm, kid: sig.kid, publicKey: sig.publicKey },
},
};
require("fs").writeFileSync("/tmp/partner-bundle.json", JSON.stringify(bundle, null, 2));
console.log(`Bundle ready — KEM=${kem.algorithm}, SIG=${sig.algorithm}`);using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class B2bPublicKeyDistribution
{
const string BASE_URL = "https://staging.ankatech.co";
const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
const string KEM_KID = "pubkey-shared-encryption";
const string SIG_KID = "pubkey-shared-signature";
static async Task Main()
{
var adminHttp = new HttpClient();
adminHttp.DefaultRequestHeaders.Add("Authorization",
$"Bearer {Environment.GetEnvironmentVariable("ADMIN_TOKEN")}");
// Admin provisions both keys (exportable=true required)
foreach (var spec in new[] {
new { kid = KEM_KID, kty = "ML-KEM", algorithm = "ML-KEM-768",
purpose = "ENCRYPT_DECRYPT", keyOps = new[] { "encrypt", "decrypt" },
exportable = true },
new { kid = SIG_KID, kty = "ML-DSA", algorithm = "ML-DSA-87",
purpose = "SIGN_VERIFY", keyOps = new[] { "sign", "verify" },
exportable = true },
}) {
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
new StringContent(JsonSerializer.Serialize(spec),
Encoding.UTF8, "application/json"));
}
// Export public keys
var kem = JsonDocument.Parse(await (await adminHttp.GetAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys/{KEM_KID}/export"))
.Content.ReadAsStringAsync()).RootElement;
var sig = JsonDocument.Parse(await (await adminHttp.GetAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys/{SIG_KID}/export"))
.Content.ReadAsStringAsync()).RootElement;
// Package partner bundle
var bundle = new {
tenant = "acme-corp",
keys = new {
encryption = new {
kty = kem.GetProperty("kty").GetString(),
algorithm = kem.GetProperty("algorithm").GetString(),
kid = kem.GetProperty("kid").GetString(),
publicKey = kem.GetProperty("publicKey").GetString(),
},
signature = new {
kty = sig.GetProperty("kty").GetString(),
algorithm = sig.GetProperty("algorithm").GetString(),
kid = sig.GetProperty("kid").GetString(),
publicKey = sig.GetProperty("publicKey").GetString(),
},
},
};
File.WriteAllText("/tmp/partner-bundle.json",
JsonSerializer.Serialize(bundle, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine($"Bundle ready — KEM={kem.GetProperty("algorithm").GetString()}"
+ $", SIG={sig.GetProperty("algorithm").GetString()}");
}
}Distribution channel
Partner bundles carry public keys — no secret material — so plaintext email is technically safe. In practice, distribute over:
- Signed email (S/MIME or DKIM-verified) — proves the bundle came from you.
- Encrypted 1Password / Bitwarden shared vault — access-controlled, audited.
- Well-known URL (
https://acme-corp.com/.well-known/ankasecure-keys.json) — best for automated fetches; use HTTPS and pin the certificate.
Where to go next
- External Key Interoperability — the recipe your partner runs on their side.
- ML-KEM-1024 Encrypt / Decrypt — the base encryption recipe.
- ML-DSA-87 Sign / Verify — the base signature recipe.
Updated 10 days ago