RSA-3072 Sign, then Rapid Revocation

Provision an RSA-3072 signing key, sign a payload, then revoke the key immediately. Verify that the revoked key can no longer be used.

🚫 RSA-3072 Sign, then Rapid Revocation

Scenario: provision an RSA-3072 signing key, produce a JWS, and then immediately revoke the key. Verify that (a) previously issued signatures are still verifiable (RFC compliance), but (b) any new sign / verify request against the revoked kid returns 412 Precondition Failed.

Useful for compromise-response drills and for regulatory scenarios that require rapid revocation SLAs.

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 KID="rsa3072-sign-key"
# (ADMIN_TOKEN and APP_TOKEN acquired as in the AES-256 recipe)

# Step 2: admin creates the signing key
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\":\"$KID\", \"kty\":\"RSA\", \"algorithm\":\"RS256\",
    \"purpose\":\"SIGN_VERIFY\", \"keyOps\":[\"sign\",\"verify\"]
  }" | jq

# Step 3: app signs (works fine while key is ACTIVE)
export DATA_B64=$(printf "%s" "Signed before revoke" | base64 -w0)
curl -s -X POST "$BASE_URL/api/v3/crypto/sign" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"kid\":\"$KID\", \"data\":\"$DATA_B64\" }" \
  | tee /tmp/sig-before.json | jq .algorithmUsed

# Step 4: admin revokes the key IMMEDIATELY
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/$KID/revoke" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq
# → { "kid": "...", "status": "REVOKED", "revokedAt": "..." }

# Step 5: app tries to sign AGAIN — expect 412 Precondition Failed
curl -s -o /tmp/sig-after.json -w "HTTP %{http_code}\n" \
  -X POST "$BASE_URL/api/v3/crypto/sign" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"kid\":\"$KID\", \"data\":\"$DATA_B64\" }"
cat /tmp/sig-after.json | jq
# → HTTP 412  { "type": ".../key-lifecycle-blocked", "title": "Key Not Usable", "status": 412, ... }

# Step 6: verifying the OLD JWS still works — signature was legally emitted before revoke
export OLD_JWS=$(jq -r .jwsToken /tmp/sig-before.json)
curl -s -X POST "$BASE_URL/api/v3/crypto/verify" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"jwsToken\":\"$OLD_JWS\" }" | jq
# → { "isValid": true, "warnings": ["key kid=... is REVOKED, but the signature predates revocation"], ... }
import base64, requests

BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
KID = "rsa3072-sign-key"
# admin / app tokens acquired as in the AES-256 recipe

# Admin creates the signing key
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys", headers=admin, json={
    "kid": KID, "kty": "RSA", "algorithm": "RS256",
    "purpose": "SIGN_VERIFY", "keyOps": ["sign","verify"],
}).raise_for_status()

# App signs — ACTIVE
sig = requests.post(f"{BASE}/api/v3/crypto/sign", headers=app,
                    json={"kid": KID, "data": base64.b64encode(b"Signed before revoke").decode()}).json()
old_jws = sig["jwsToken"]
print(f"Signed with {sig['algorithmUsed']}")

# Admin revokes immediately
r = requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/{KID}/revoke", headers=admin)
r.raise_for_status()
print(f"Key status: {r.json()['status']}")   # → REVOKED

# App tries to sign again → expected 412
r2 = requests.post(f"{BASE}/api/v3/crypto/sign", headers=app,
                   json={"kid": KID, "data": "aGVsbG8="})
assert r2.status_code == 412, f"Expected 412, got {r2.status_code}"
print("Sign after revoke blocked (412) — correct")

# Verify OLD JWS still works
ver = requests.post(f"{BASE}/api/v3/crypto/verify", headers=app,
                    json={"jwsToken": old_jws}).json()
assert ver["isValid"]
print(f"Old signature still verifies (warnings: {ver.get('warnings', [])})")
import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;

public class Rsa3072SignRevoke {
    static final String BASE_URL = "https://staging.ankatech.co";
    static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
    static final String KID = "rsa3072-sign-key";
    // ADMIN_TOKEN + APP_TOKEN acquired as in the AES-256 recipe

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

        // Admin creates the RSA-3072 signing key
        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("""
                { "kid": "%s", "kty": "RSA", "algorithm": "RS256",
                  "purpose": "SIGN_VERIFY", "keyOps": ["sign","verify"] }""".formatted(KID))).build(),
            HttpResponse.BodyHandlers.ofString());

        // Sign — ACTIVE
        var dataB64 = Base64.getEncoder().encodeToString("Signed before revoke".getBytes());
        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\": \"" + KID + "\", \"data\": \"" + dataB64 + "\" }")).build(),
            HttpResponse.BodyHandlers.ofString());
        String oldJws = json.readTree(sigResp.body()).get("jwsToken").asText();

        // Admin revokes immediately
        http.send(HttpRequest.newBuilder(URI.create(
            BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys/" + KID + "/revoke"))
            .header("Authorization", adminAuth)
            .POST(HttpRequest.BodyPublishers.noBody()).build(),
            HttpResponse.BodyHandlers.ofString());

        // Sign again → expect 412
        var sig2 = 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\": \"" + KID + "\", \"data\": \"aGVsbG8=\" }")).build(),
            HttpResponse.BodyHandlers.ofString());
        if (sig2.statusCode() != 412) throw new RuntimeException("Expected 412, got " + sig2.statusCode());
        System.out.println("Sign after revoke blocked (412) — correct");

        // Verify OLD JWS still works
        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\": \"" + oldJws + "\" }")).build(),
            HttpResponse.BodyHandlers.ofString());
        JsonNode ver = json.readTree(verResp.body());
        System.out.println("Old signature still verifies: " + ver.get("isValid").asBoolean()
            + ", warnings=" + ver.get("warnings"));
    }
}
const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const KID = "rsa3072-sign-key";
// admin / app / jsonH as in the AES-256 recipe

// Admin creates the signing key
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
  method: "POST", headers: jsonH(admin),
  body: JSON.stringify({
    kid: KID, kty: "RSA", algorithm: "RS256",
    purpose: "SIGN_VERIFY", keyOps: ["sign","verify"],
  }),
});

// Sign — ACTIVE
const sig = await (await fetch(`${BASE_URL}/api/v3/crypto/sign`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ kid: KID, data: Buffer.from("Signed before revoke").toString("base64") }),
})).json();

// Admin revokes immediately
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys/${KID}/revoke`, {
  method: "POST", headers: admin,
});

// Sign again — expect 412
const r = await fetch(`${BASE_URL}/api/v3/crypto/sign`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ kid: KID, data: "aGVsbG8=" }),
});
if (r.status !== 412) throw new Error(`Expected 412, got ${r.status}`);
console.log("Sign after revoke blocked (412) — correct");

// Old JWS verification still works
const ver = await (await fetch(`${BASE_URL}/api/v3/crypto/verify`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ jwsToken: sig.jwsToken }),
})).json();
console.log(`Old signature still verifies (warnings: ${JSON.stringify(ver.warnings || [])})`);
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Rsa3072SignRevoke
{
    const string BASE_URL  = "https://staging.ankatech.co";
    const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
    const string KID       = "rsa3072-sign-key";

    static async Task Main()
    {
        // ADMIN_TOKEN + APP_TOKEN acquired as in the AES-256 recipe
        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")}");

        // Admin creates the RSA-3072 signing key
        await adminHttp.PostAsync(
            $"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
            new StringContent(JsonSerializer.Serialize(new {
                kid = KID, kty = "RSA", algorithm = "RS256",
                purpose = "SIGN_VERIFY", keyOps = new[] { "sign", "verify" },
            }), Encoding.UTF8, "application/json"));

        // Sign — ACTIVE
        var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("Signed before revoke"));
        var sigResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/sign",
            new StringContent(JsonSerializer.Serialize(new { kid = KID, data = dataB64 }),
                              Encoding.UTF8, "application/json"));
        var sig = JsonDocument.Parse(await sigResp.Content.ReadAsStringAsync()).RootElement;
        var oldJws = sig.GetProperty("jwsToken").GetString();

        // Admin revokes immediately
        await adminHttp.PostAsync(
            $"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys/{KID}/revoke",
            new StringContent(""));

        // Sign again → expect 412
        var sig2 = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/sign",
            new StringContent(JsonSerializer.Serialize(new { kid = KID, data = "aGVsbG8=" }),
                              Encoding.UTF8, "application/json"));
        if ((int)sig2.StatusCode != 412) throw new Exception($"Expected 412, got {(int)sig2.StatusCode}");
        Console.WriteLine("Sign after revoke blocked (412) — correct");

        // Verify OLD JWS still works
        var verResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/verify",
            new StringContent(JsonSerializer.Serialize(new { jwsToken = oldJws }),
                              Encoding.UTF8, "application/json"));
        var ver = JsonDocument.Parse(await verResp.Content.ReadAsStringAsync()).RootElement;
        Console.WriteLine($"Old signature still verifies: {ver.GetProperty("isValid").GetBoolean()}");
    }
}

What just happened

  • The admin created and immediately revoked a signing key — the whole cycle takes two API calls.
  • Post-revocation, POST /crypto/sign returns HTTP 412 Precondition Failed for that kid — the key's lifecycle state prevents the operation.
  • Historical signatures remain valid — RFC 5280-style thinking: revoking a key does not retroactively invalidate signatures it lawfully produced. The verify response includes a warning that the key is now revoked.

Where to go next


Did this page help you?