Why Error Handling in Crypto APIs Is Security-Critical
In most web applications, a poorly worded error message is an inconvenience. In cryptographic authentication APIs, it is a vulnerability. Every error response your system emits is a signal that an adversary can observe, measure, and exploit. The difference between "Invalid password" and "User not found" tells an attacker whether an account exists. The difference between a 2ms and a 14ms failure tells them whether a decryption key was partially correct. When your API handles fully homomorphic encryption, zero-knowledge proofs, and post-quantum signatures in a single call — computing on encrypted data without decryption — the surface area for information leakage multiplies.
H33's authentication pipeline runs FHE biometric matching, ZKP verification, and Dilithium attestation in under 42 microseconds per auth. At that speed, even microsecond-level timing variance becomes observable over thousands of requests. Defensive error handling is not optional -- it is a core security control.
The Cardinal Rule
Never return different error messages or response times for authentication failures that differ only in which step failed. An attacker who can distinguish "wrong biometric" from "expired token" from "invalid ZKP" can systematically narrow their attack vector.
Common Anti-Patterns That Leak Information
The most dangerous error handling patterns in cryptographic APIs are those that seem helpful during development but become exploitable in production.
Verbose Error Messages
Returning messages like "BFV ciphertext noise budget exhausted at modulus index 2" or "Dilithium signature verification failed: invalid coefficient at position 847" gives an adversary direct insight into your FHE parameter configuration and signature internals. In a harvest-now-decrypt-later scenario, this metadata is exactly what a future quantum attacker needs to reconstruct your parameter choices.
Timing-Based Oracle Attacks
If your API returns a 401 in 3ms when the user ID is wrong but takes 18ms when the biometric match fails (because FHE decryption ran before failing), an attacker can enumerate valid user IDs without ever submitting a valid biometric. This is a classic timing oracle. The fix is constant-time error paths: always run the full authentication pipeline before returning any error, or pad response times to a fixed duration.
Stack Traces in Production
Returning raw stack traces that mention internal module paths (src/fhe/bfv.rs:412 or src/zkp/plookup.rs:89) reveals your exact software architecture. Attackers use this to target known vulnerabilities in specific library versions. Production APIs must strip all internal details from error responses.
HTTP Status Code Mapping for Auth APIs
Choosing the correct HTTP status code is both a semantic and security decision. Using the wrong code leaks information about why a request failed.
| Status | Meaning | When to Use | Security Note |
|---|---|---|---|
401 |
Unauthorized | Invalid API key, expired token, failed biometric | Use for ALL authentication failures -- do not distinguish reason |
403 |
Forbidden | Valid auth but insufficient permissions or plan tier | Only after successful authentication; never for auth failures |
429 |
Too Many Requests | Rate limit exceeded | Include Retry-After header; do not reveal per-key vs global limit |
400 |
Bad Request | Malformed JSON, missing required fields, invalid FHE parameters | Safe to describe which field is missing; never describe why a value is wrong |
500 |
Internal Server Error | Unexpected failures, panic recovery | Generic message only; log full details server-side |
503 |
Service Unavailable | Circuit breaker open, maintenance window | Include Retry-After; do not reveal which subsystem is down |
Structured Error Responses
Every H33 API error response follows a consistent envelope. The error_code is a stable machine-readable identifier that your retry logic can switch on. The message is a safe, human-readable string that never contains internal details. The request_id is a correlation token that maps to full diagnostic data in your server-side logs.
{
"error": {
"code": "AUTH_FAILED",
"message": "Authentication could not be completed.",
"request_id": "req_7f3a9c2e1b4d",
"retry_after": null
}
}
This structure gives developers everything they need to debug (via request_id) without exposing anything an attacker can exploit. The same AUTH_FAILED code is returned whether the failure was a bad API key, an expired session, a biometric mismatch, or a ZKP verification failure.
H33-Specific Error Codes
| Error Code | HTTP Status | Cause | Client Action |
|---|---|---|---|
AUTH_FAILED |
401 | Any authentication failure | Prompt user to re-authenticate |
TOKEN_EXPIRED |
401 | Session or API token past TTL | Refresh token or re-authenticate |
RATE_LIMITED |
429 | Per-key or global rate limit hit | Backoff per Retry-After header |
FHE_PARAM_MISMATCH |
400 | Ciphertext parameters do not match enrolled template | Re-encrypt with correct scheme parameters |
PLAN_LIMIT |
403 | Monthly auth quota exceeded | Upgrade plan or wait for billing cycle reset |
SERVICE_UNAVAILABLE |
503 | Internal subsystem degraded | Retry with exponential backoff |
Retry Strategies: Backoff, Idempotency, and Circuit Breakers
Not all errors should be retried. A 400 (bad request) will never succeed on retry -- the client must fix the input. A 429 or 503 is inherently transient and should be retried with exponential backoff. A 401 from an expired token should trigger a token refresh, not a blind retry.
Exponential Backoff with Jitter
The standard pattern is min(base * 2^attempt + random_jitter, max_delay). Without jitter, thousands of clients that hit a rate limit simultaneously will retry at the same instant, creating a thundering herd. H33 SDKs use a base of 100ms, a max of 30 seconds, and up to 50% jitter.
Idempotency Keys
For enrollment and verification calls, pass an X-Idempotency-Key header. If a network timeout occurs and you retry, H33 will return the cached result of the original request rather than re-executing the operation. This prevents double-enrollment of biometric templates, which would corrupt the FHE ciphertext state.
Circuit Breakers
If your client observes 5 consecutive 5xx errors within 10 seconds, stop sending requests. Open the circuit breaker for 30 seconds, then send a single probe request. If it succeeds, close the breaker and resume. This protects both your application and H33's infrastructure from cascading failures.
Code Examples
Python: Robust H33 Client
import httpx
import time, random, logging
logger = logging.getLogger("h33")
class H33Client:
RETRYABLE = {429, 500, 502, 503}
def authenticate(self, user_id: str, template: bytes):
for attempt in range(4):
resp = httpx.post(
"https://api.h33.ai/v1/auth",
headers={"Authorization": f"Bearer {self.api_key}"},
json={"user_id": user_id, "template": template.hex()},
)
if resp.status_code == 200:
return resp.json()
error = resp.json().get("error", {})
code = error.get("code")
# Non-retryable errors: stop immediately
if resp.status_code not in self.RETRYABLE:
logger.warning("Auth failed",
extra={"request_id": error.get("request_id")})
raise AuthError(code)
# Exponential backoff with jitter
delay = min(0.1 * (2 ** attempt) + random.uniform(0, 0.05), 30)
time.sleep(delay)
raise AuthError("MAX_RETRIES_EXCEEDED")
Node.js: Retry with Circuit Breaker
const RETRYABLE = new Set([429, 500, 502, 503]);
let failures = 0, circuitOpen = false, circuitTimer = null;
async function h33Authenticate(userId, template) {
if (circuitOpen) throw new Error("CIRCUIT_OPEN");
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch("https://api.h33.ai/v1/auth", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"X-Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ user_id: userId, template }),
});
if (res.ok) { failures = 0; return res.json(); }
const { error } = await res.json();
// Non-retryable: surface error code, never internal details
if (!RETRYABLE.has(res.status)) throw new Error(error?.code);
// Trip circuit breaker after consecutive failures
if (++failures >= 5) {
circuitOpen = true;
circuitTimer = setTimeout(() => { circuitOpen = false; }, 30000);
}
const delay = Math.min(100 * 2 ** attempt + Math.random() * 50, 30000);
await new Promise(r => setTimeout(r, delay));
}
}
Logging Best Practices: What to Log vs. What to Redact
Server-side logs are your primary diagnostic tool when a request_id comes in from a customer support ticket. But cryptographic systems process data that must never appear in logs, regardless of log level or retention policy.
API keys and bearer tokens -- log only the last 4 characters for correlation. Biometric templates -- not even hashed; the hash itself is sensitive. FHE ciphertexts and private keys -- not even truncated. ZKP witness data -- the witness is the secret the proof protects. Dilithium/Kyber private key material -- catastrophic if leaked.
What you should log: request IDs, user IDs, timestamps, HTTP status codes, error codes (the safe ones), response latency, and the specific pipeline stage that failed (as an enum, not a message). This gives you full observability without any secret exposure.
Monitoring and Alerting on Error Rates
Set up threshold-based alerts on your error rates, segmented by error code. A sudden spike in AUTH_FAILED across many distinct user IDs could indicate a credential stuffing attack. A spike in FHE_PARAM_MISMATCH likely means a client SDK was updated with incompatible parameters. A sustained increase in 429 responses means you are either under attack or approaching organic capacity limits.
- Alert at 5% error rate (rolling 5-minute window) -- investigate.
- Alert at 15% error rate -- page on-call; possible incident.
- Alert on any 500 spike -- internal failure; immediate triage.
- Track p99 latency per error code -- timing divergence reveals oracle risk.
Key Takeaway
Error handling in cryptographic APIs is not about developer convenience -- it is about closing information channels. Every error message, every timing difference, every status code distinction is a potential signal to an adversary. Design your error paths with the same rigor you apply to your encryption parameters: assume the attacker sees everything, and give them nothing.
Ready to Go Quantum-Secure?
Start protecting your users with post-quantum authentication today. 1,000 free auths, no credit card required.
Get Free API Key →