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/signreturns HTTP 412 Precondition Failed for thatkid— 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
- Key Lifecycle Management — full ACTIVE → SUSPENDED → REVOKED → ARCHIVED flow.
- Key Revocation Validation — programmatic verification of the revoked state.
POST /keys/{kid}/revoke— endpoint reference.
Updated 10 days ago
Did this page help you?