External Key Interoperability (encrypt & verify without import)

Use a partner's public key directly on encrypt/verify, without provisioning it as a tenant key. Ideal for one-off exchanges.

🤝 External Key Interoperability (encrypt & verify without import)

Scenario: a counterparty hands you a public key (X.509 PEM or JWK) — you need to encrypt for them or verify their JWS, but you do NOT want to provision that key permanently in your tenant. The interoperability endpoints accept the public key material inline on the call, keep zero state, and return the standard JWE / verification result.

Perfect for one-off B2B exchanges, ad-hoc secure delivery, or CI pipelines that verify third-party signatures.

Prerequisites

Same app identity as the AES-256 recipeno admin operations required for this recipe, only the app token.

You also need the partner's public key in Base64 SubjectPublicKeyInfo (X.509) format. Extract with openssl x509 -pubkey -noout from their cert and strip the PEM headers.


Step-by-step — encrypt for a partner

The endpoint is multipart/form-data: one metadata JSON part + one binary file part.

export BASE_URL="https://staging.ankatech.co"
export APP_TOKEN="…"    # obtained via client_credentials as in AES-256

# Payload file
echo -n "Confidential B2B payload" > /tmp/plaintext.bin

# Encrypt with the partner's public key (X25519 in this example)
curl -s -X POST "$BASE_URL/api/v3/interoperability/encrypt" \
  -H "Authorization: Bearer $APP_TOKEN" \
  -F "metadata={
        \"kty\":       \"OKP\",
        \"algorithm\": \"ECDH-ES+A256KW\",
        \"publicKey\": \"MCowBQYDK2VuAyEA…partner-b64…\"
      };type=application/json" \
  -F "file=@/tmp/plaintext.bin" \
  --output /tmp/ciphertext.bin

echo "Ciphertext size: $(wc -c < /tmp/ciphertext.bin) bytes"
# Send /tmp/ciphertext.bin to the partner — they decrypt with THEIR private key.

Step-by-step — verify a partner's JWS

# The partner sends you (a) a detached General-JSON JWS + (b) the payload file
curl -s -X POST "$BASE_URL/api/v3/interoperability/verify" \
  -H "Authorization: Bearer $APP_TOKEN" \
  -F "metadata={
        \"signatureBase64\": \"eyJwcm90ZWN0ZWQiOi…partner-jws…\",
        \"publicKey\":       \"MIIBIjANBgkq…partner-b64…\"
      };type=application/json" \
  -F "file=@/tmp/signed-payload.bin" \
  | jq
# → { "isValid": true, "algorithmUsed": "ML-DSA-65", ... }
import requests

BASE = "https://staging.ankatech.co"
# app token as in AES-256

# Encrypt for a partner
files = {
    "metadata": (None, '{"kty":"OKP","algorithm":"ECDH-ES+A256KW",'
                       '"publicKey":"MCowBQYDK2VuAyEA…partner-b64…"}',
                 "application/json"),
    "file":     ("payload.bin", b"Confidential B2B payload", "application/octet-stream"),
}
r = requests.post(f"{BASE}/api/v3/interoperability/encrypt", headers=app, files=files)
r.raise_for_status()
with open("/tmp/ciphertext.bin", "wb") as f:
    f.write(r.content)
print(f"Ciphertext {len(r.content)} bytes")

# Verify a partner's JWS
files = {
    "metadata": (None, '{"signatureBase64":"eyJwcm90…partner-jws…",'
                       '"publicKey":"MIIBIjAN…partner-b64…"}',
                 "application/json"),
    "file":     ("signed.bin", open("/tmp/signed-payload.bin","rb").read(), "application/octet-stream"),
}
result = requests.post(f"{BASE}/api/v3/interoperability/verify",
                       headers=app, files=files).json()
assert result["isValid"], "Partner signature invalid"
print(f"Verified — algorithm: {result['algorithmUsed']}")
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;

public class ExternalKeyInterop {
    static final String BASE_URL = "https://staging.ankatech.co";
    static final HttpClient http = HttpClient.newHttpClient();

    /** Minimal multipart/form-data builder (no external deps). */
    static byte[] multipart(String boundary, Map<String, byte[]> jsonParts,
                            Map<String, byte[]> fileParts) throws Exception {
        var out = new java.io.ByteArrayOutputStream();
        for (var e : jsonParts.entrySet()) {
            out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\""
                + e.getKey() + "\"\r\nContent-Type: application/json\r\n\r\n").getBytes());
            out.write(e.getValue());
            out.write("\r\n".getBytes());
        }
        for (var e : fileParts.entrySet()) {
            out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\""
                + e.getKey() + "\"; filename=\"" + e.getKey()
                + "\"\r\nContent-Type: application/octet-stream\r\n\r\n").getBytes());
            out.write(e.getValue());
            out.write("\r\n".getBytes());
        }
        out.write(("--" + boundary + "--\r\n").getBytes());
        return out.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        String appAuth = "Bearer " + System.getenv("APP_TOKEN");
        String boundary = "----" + System.currentTimeMillis();

        // Encrypt for a partner
        var encBody = multipart(boundary,
            Map.of("metadata", ("{\"kty\":\"OKP\",\"algorithm\":\"ECDH-ES+A256KW\","
                + "\"publicKey\":\"MCowBQYDK2VuAyEA…partner-b64…\"}").getBytes()),
            Map.of("file", "Confidential B2B payload".getBytes()));
        var encResp = http.send(HttpRequest.newBuilder(URI.create(
            BASE_URL + "/api/v3/interoperability/encrypt"))
            .header("Authorization", appAuth)
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .POST(HttpRequest.BodyPublishers.ofByteArray(encBody)).build(),
            HttpResponse.BodyHandlers.ofByteArray());
        Files.write(Path.of("/tmp/ciphertext.bin"), encResp.body());
        System.out.println("Ciphertext " + encResp.body().length + " bytes");

        // Verify a partner's JWS
        var verBody = multipart(boundary,
            Map.of("metadata", ("{\"signatureBase64\":\"eyJwcm90…partner-jws…\","
                + "\"publicKey\":\"MIIBIjAN…partner-b64…\"}").getBytes()),
            Map.of("file", Files.readAllBytes(Path.of("/tmp/signed-payload.bin"))));
        var verResp = http.send(HttpRequest.newBuilder(URI.create(
            BASE_URL + "/api/v3/interoperability/verify"))
            .header("Authorization", appAuth)
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .POST(HttpRequest.BodyPublishers.ofByteArray(verBody)).build(),
            HttpResponse.BodyHandlers.ofString());
        System.out.println("Verify response: " + verResp.body());
    }
}
const BASE_URL = "https://staging.ankatech.co";
// app token as in AES-256

// Encrypt for a partner (Node.js — use undici / form-data package)
import { FormData, Blob } from "node:buffer";

const encFd = new FormData();
encFd.append("metadata", new Blob([JSON.stringify({
  kty: "OKP", algorithm: "ECDH-ES+A256KW",
  publicKey: "MCowBQYDK2VuAyEA…partner-b64…",
})], { type: "application/json" }));
encFd.append("file", new Blob([Buffer.from("Confidential B2B payload")]));

const encResp = await fetch(`${BASE_URL}/api/v3/interoperability/encrypt`, {
  method: "POST", headers: app, body: encFd,
});
const ciphertext = new Uint8Array(await encResp.arrayBuffer());
console.log(`Ciphertext ${ciphertext.length} bytes`);

// Verify a partner's JWS
const verFd = new FormData();
verFd.append("metadata", new Blob([JSON.stringify({
  signatureBase64: "eyJwcm90…partner-jws…",
  publicKey:       "MIIBIjAN…partner-b64…",
})], { type: "application/json" }));
verFd.append("file", new Blob([/* signed payload bytes */]));

const ver = await (await fetch(`${BASE_URL}/api/v3/interoperability/verify`, {
  method: "POST", headers: app, body: verFd,
})).json();
if (!ver.isValid) throw new Error("Partner signature invalid");
console.log(`Verified — algorithm: ${ver.algorithmUsed}`);
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

class ExternalKeyInterop
{
    const string BASE_URL = "https://staging.ankatech.co";

    static async Task Main()
    {
        var appHttp = new HttpClient();
        appHttp.DefaultRequestHeaders.Add("Authorization",
            $"Bearer {Environment.GetEnvironmentVariable("APP_TOKEN")}");

        // Encrypt for a partner
        var encContent = new MultipartFormDataContent();
        var encMeta = new StringContent(
            "{\"kty\":\"OKP\",\"algorithm\":\"ECDH-ES+A256KW\","
            + "\"publicKey\":\"MCowBQYDK2VuAyEA…partner-b64…\"}",
            Encoding.UTF8, "application/json");
        encContent.Add(encMeta, "metadata");
        var encFile = new ByteArrayContent(Encoding.UTF8.GetBytes("Confidential B2B payload"));
        encFile.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        encContent.Add(encFile, "file", "payload.bin");

        var encResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/interoperability/encrypt", encContent);
        var ciphertext = await encResp.Content.ReadAsByteArrayAsync();
        File.WriteAllBytes("/tmp/ciphertext.bin", ciphertext);
        Console.WriteLine($"Ciphertext {ciphertext.Length} bytes");

        // Verify a partner's JWS
        var verContent = new MultipartFormDataContent();
        var verMeta = new StringContent(
            "{\"signatureBase64\":\"eyJwcm90…partner-jws…\","
            + "\"publicKey\":\"MIIBIjAN…partner-b64…\"}",
            Encoding.UTF8, "application/json");
        verContent.Add(verMeta, "metadata");
        var signed = new ByteArrayContent(File.ReadAllBytes("/tmp/signed-payload.bin"));
        signed.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        verContent.Add(signed, "file", "signed.bin");

        var verResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/interoperability/verify", verContent);
        Console.WriteLine($"Verify response: {await verResp.Content.ReadAsStringAsync()}");
    }
}

Why the interop endpoints

  • Zero state — the partner's public key is never persisted, no cleanup needed.
  • One request — no create-key + encrypt roundtrip; useful for one-off flows.
  • Streaming — the file part streams both ways, so multi-GB payloads are supported.
  • Any algorithm — as long as the partner's key advertises a supported kty + algorithm, it works.

Where to go next


Did this page help you?