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/encrypt with metadata.publicKey = <your KEM publicKey>.
    They send you the resulting envelope; you POST /api/v3/crypto/decrypt on your side with kid = pubkey-shared-encryption.

  • To verify a JWS you signed:
    POST /api/v3/interoperability/verify with metadata.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


Did this page help you?