Skip to content

Authentication & Security ​

Zedgi uses a defence-in-depth approach: two-value API keys for identity, HMAC-SHA256 request signing for integrity, and ECIES credential encryption for confidentiality.

Two-value API key model ​

Every API key consists of two independent values, returned once at creation:

ValueLengthPurposeStored on server
key (starts zk_)64 hex charsPublic identifier β€” sent as x-zedgi-keySHA-256 hashed (irrecoverable)
signing_secret64 hex charsHMAC-SHA256 signing keyAES-256-GCM encrypted (Master KEK)
json
{
  "id": "a1b2c3d4e5f6...",
  "key": "zk_live_1a2b3c4d5e6f7890abcdef1234567890abcdef12",
  "signing_secret": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "label": "production"
}

Neither value is ever shown again. Store both securely β€” the key is irrecoverable from our servers.

Request signing ​

Every RPC request must carry four headers:

HeaderFormatDescription
x-zedgi-keyzk_.. (68 chars)Public key identifier
x-zedgi-tsepoch millisecondsWithin Β±5 min of server clock
x-zedgi-nonce32 hex chars (128-bit)Single-use, prevents replay
x-zedgi-sig64 hex charsHMAC-SHA256 of canonical message

Canonical message ​

message = `${ts}:${nonce}:${sha256hex(body)}`
body    = JSON.stringify(rpcPayload)   // raw JSON, not an object
sig     = HMAC-SHA256(message, signing_secret)

The nonce must be a fresh 128-bit random value for every request. The server rejects replayed nonces (stored in KV until the timestamp window expires).

Verification flow ​

x-zedgi-sig = HMAC-SHA256("1749600000000:a1b2...:3c4d...", signing_secret)
  1. Server checks x-zedgi-ts is within Β±5 minutes
  2. Server checks x-zedgi-nonce has not been seen before
  3. Server decrypts signing_secret_enc from the database
  4. Server recomputes the HMAC and compares constant-time
  5. Nonce is stored in KV (global replay protection across PoPs)

Credential encryption (ECIES) ​

Credentials (host, port, user, password, database) never travel in plaintext. They are supplied by your application and turned into the x-zedgi-cred "link" (encrypted client-side).

Account keypair ​

Every account has an X25519 keypair, generated on first sign-in:

  • Public key β€” safe to share; used for client-side encryption
  • Private key β€” wrapped with the Master KEK (AES-256-GCM); stored in D1

Two-hop encryption ​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Client     β”‚      β”‚   Gateway    β”‚      β”‚    Proxy     β”‚
β”‚              β”‚      β”‚  (Worker)    β”‚      β”‚  (Node.js)   β”‚
β”‚ credentials  β”‚      β”‚              β”‚      β”‚              β”‚
β”‚     β”‚        β”‚      β”‚              β”‚      β”‚              β”‚
β”‚     β–Ό        β”‚      β”‚              β”‚      β”‚              β”‚
β”‚ ECIES encrypt│─────▢│ ECIES decryptβ”‚      β”‚              β”‚
β”‚ (account pub)β”‚      β”‚              β”‚      β”‚              β”‚
β”‚              β”‚      β”‚ ECIES encrypt│─────▢│ ECIES decryptβ”‚
β”‚              β”‚      β”‚ (RPC node pk)β”‚      β”‚              β”‚
β”‚              β”‚      β”‚              β”‚      β”‚  connect +   β”‚
β”‚              β”‚      β”‚              β”‚      β”‚  execute     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Client β†’ Gateway hop:

Key derivation: X25519(ephemeral_priv, account_pub) β†’ HKDF(secret, "zedgi-cred-client-gateway-v1") β†’ AES-256-GCM key

Blob format (base64url-encoded binary):

0x01              (1 byte   β€” version)
accountId         (16 bytes β€” raw hex)
keyVersion        (2 bytes  β€” uint16 BE)
ephemeralPub      (32 bytes β€” X25519 raw)
iv                (12 bytes β€” AES-GCM nonce)
ciphertext        (variable)
tag               (16 bytes β€” appended by GCM)

Gateway β†’ RPC node hop:

Same ECIES scheme, different HKDF info: "zedgi-cred-gateway-rpc-v1"

The double-encrypted blob is cached in L1 (memory, rotated every 5 min) and L2 (KV, TTL 1 hour). No plaintext credential ever enters the cache.

Decrypt cache ​

Cache key: SHA-256(encryptedCredBlob) β€” ciphertext in, ciphertext out. On miss: full ECDH decrypt β†’ re-encrypt β†’ populate cache. On hit: zero crypto, zero network.

API key management endpoints ​

List API keys ​

http
GET /api/services/:serviceId/keys

Returns metadata only (id, label, created_at, last_used_at). The key and signing_secret are never returned again.

Create API key ​

http
POST /api/services/:serviceId/keys
Content-Type: application/json

{ "label": "production" }

Returns the key and signing_secret β€” save them immediately.

Delete API key ​

http
DELETE /api/keys/:keyId

Invalidates the key globally. The KV cache entry is deleted so the key stops working immediately.

Account keypair endpoints ​

SDKs can auto-pull this. Manually:

http
GET /api/account/keys/current
x-zedgi-key: zk_live_...

(Works with just the public identifier β€” no signing secret needed. Public data only.)

json
{
  "key_version": 1,
  "public_key": "lRk7...base64url...",
  "created_at": 1749600000000
}

Use public_key (and the key_version embedded in the blob) when you or the SDK encrypt credentials into the x-zedgi-cred "link".

List all keypairs ​

http
GET /api/account/keys

Returns all keypairs (active and retired), ordered by version descending.

Rotate keypair ​

http
POST /api/account/keys/rotate

Creates a new active keypair and retires the current one. A notification email is sent.

What you do:

  1. The SDK (when publicKey is omitted) or your code fetches the new public key via GET /api/account/keys/current (using your x-zedgi-key).
  2. Re-supply your credential to createZedgiClient (it will re-encrypt under the new key) or manually produce a fresh x-zedgi-cred blob.
  3. Deploy the update.

Old credential blobs encrypted under retired key versions will stop working after the grace period.

⚠︎
API keys are separate from the account keypair. Rotating your ECIES keypair does not invalidate your API keys β€” they continue to work. You only need to update the x-zedgi-cred link (re-encrypt credentials with the new public key).

Rate limiting ​

Rate limits are enforced per-user, based on balance:

  • Free tier: 100 req/day
  • Paid tier: proportional to balance

When exceeded, the server returns 429 RATE_LIMIT_EXCEEDED.

IP whitelisting ​

When creating a service, you can optionally specify a whitelisted IP. Only requests forwarded to the proxy from that IP will be accepted. The whitelist is checked by the proxy at connection time.