Skip to main content
This document covers the cryptographic primitives used by the auth service.

ES256 (ECDSA P-256)

All JWTs are signed using ES256 (ECDSA with the P-256 curve). The implementation uses PyJWT with the cryptography backend.

Key Generation

from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1, generate_private_key

private_key = generate_private_key(SECP256R1())
Keys are generated as PEM-encoded strings:
  • Public key: SubjectPublicKeyInfo format, cached in-memory by the API’s TokenValidator
  • Private key: PKCS8 format, encrypted at rest before storage

JWT Structure

{
  "header": { "alg": "ES256", "typ": "JWT" },
  "payload": {
    "jti": "unique-token-id",
    "sub": "customer-uuid",
    "typ": "agent",
    "iat": 1708905600,
    "exp": 1708992000,
    "parent_jti": "bearer-jti",
    "agent_id": "code-review-agent",
    "rbac": {}
  }
}
Why ES256 over RS256:
  • 256-bit keys vs 2048+ bit (smaller JWTs)
  • ~0.5ms verification vs ~1-2ms
  • Aligned with WebAuthn / FIDO2 ecosystem

AES-256-GCM (Private Key Encryption)

Private signing keys are encrypted at rest using AES-256-GCM before storage in auth.signing_keys.encrypted_private_key.

Encryption Flow

master_key (env var: AUTH_MASTER_KEY)

    ▼ SHA-256
256-bit AES key

    ▼ AES-256-GCM
encrypted = base64(nonce[12] + ciphertext + tag[16])

    ▼ stored in DB
auth.signing_keys.encrypted_private_key

Implementation

def encrypt_private_key(private_key_pem: str) -> str:
    key = hashlib.sha256(master_key.encode()).digest()  # 256-bit
    nonce = os.urandom(12)                               # 96-bit nonce
    aesgcm = AESGCM(key)
    ciphertext = aesgcm.encrypt(nonce, pem.encode(), None)
    return base64.b64encode(nonce + ciphertext).decode()
Nonce: 12 bytes (96 bits), generated with os.urandom(). Unique per encryption operation. Tag: 16 bytes (128 bits), appended to ciphertext by GCM mode. Provides authenticated encryption — tampering is detected on decryption.
The AUTH_MASTER_KEY is provided as an environment variable. In production, source it from a secrets manager (Railway secrets, AWS Secrets Manager, etc.). Never commit it to version control.

Bloom Filter (Revocation)

A Redis bitmap bloom filter provides O(1) token revocation checks.

Parameters

ParameterValueRationale
Size (m)1,000,000 bits (~122 KB)Fits in Redis memory
Hash count (k)7Optimal for expected load
Expected items (n)100,000Projected revoked tokens
False positive rate (p)~0.8%Acceptable for revocation

Hash Function: Double Hashing

digest = SHA-256(jti)
h1 = uint64(digest[0:8])    # First 8 bytes as unsigned 64-bit int
h2 = uint64(digest[8:16])   # Next 8 bytes
positions = [(h1 + i * h2) % m  for i in range(k)]
This produces k=7 bit positions from a single SHA-256 hash, avoiding the need for k independent hash functions.

Operations

Add (on revocation):
For each position: Redis SETBIT bloom_key position 1
Check (on validation):
For each position: Redis GETBIT bloom_key position
If ALL bits are 1 → possibly revoked (reject token)
If ANY bit is 0   → definitely not revoked (allow)
Rebuild (after Redis restart):
1. DELETE bloom_key
2. SELECT jti FROM auth.revocation_log
3. For each jti: SETBIT for all positions
Uses PIPELINE for batch operations to minimize Redis round-trips.

False Positive Impact

A false positive (~0.8% chance) causes a valid, non-revoked token to be rejected with TokenRevokedError. The impact is minimal:
  • Client receives 401 and re-authenticates
  • If persistent, the customer can trigger a bloom filter rebuild
  • The false positive rate is well below the 1% threshold
No false negatives: If a token is revoked, the bloom filter will always detect it.

HMAC-SHA256 (Action Signatures)

Action signatures provide non-repudiation — they cryptographically bind an agent to the action it performed.

Signature Creation

message = "{agent_jti}|{action}|{target_resource}|{timestamp}|{nonce}"
signature = HMAC-SHA256(secret, message)
FieldSource
agent_jtiFrom the authenticated agent token
actionThe event action being performed
target_resourceThe resource being accessed
timestampISO 8601 UTC timestamp
nonce16 bytes of secrets.token_hex()

Verification

  1. Check timestamp freshness (max age: 5 minutes)
  2. Reject future timestamps
  3. Recompute HMAC from claims
  4. Constant-time comparison with hmac.compare_digest()

Override Co-Signatures

Override decisions are attested with a separate HMAC co-signature:
message = "override|{event_id}|{decision}|{override_jti}"
cosignature = HMAC-SHA256(override_secret, message)
This creates a cryptographic proof that a specific override token was used to approve or reject a specific event.

Webhook Signing

Outbound webhook payloads are signed so customers can verify authenticity:
message = "{timestamp}.{json_payload}"
signature = HMAC-SHA256(webhook_secret, message)
Header format:
X-Quint-Signature: t={timestamp},v1={hex_signature}
Customers verify by:
  1. Extracting timestamp and signature from header
  2. Checking timestamp is within tolerance (e.g., 5 minutes)
  3. Computing HMAC-SHA256(their_secret, "{timestamp}.{payload}")
  4. Comparing with hmac.compare_digest()