12 KiB
Security model and audit notes (ZKAC 0.3)
This document summarizes the design, residual risks, and recommendations for operators integrating ZKAC. It is not a substitute for independent review before high-assurance deployment.
Goals
- Authentication: Only holders of a valid BBS+ credential for a registered role can complete
verify_authfor that role. - Server identity: The server proves its long-term identity to the client via a Schnorr signature over the session transcript; clients verify against a pinned public key. This prevents MITM attacks without requiring TLS.
- Confidentiality & integrity: All traffic (management and authenticated sessions) is authenticated-encrypted (ChaCha20-Poly1305) with keys derived from an ephemeral X25519 handshake.
- Replay resistance: Duplicate ciphertexts in a direction are rejected (sliding window + monotonic counter).
- Unlinkability (credential layer): BBS+ presentations are unlinkable across sessions when the presentation header (the session transcript hash) differs; the verifier learns only the disclosed attributes (opaque
role_id, epoch) and validity. Client anonymity is preserved: the client never reveals its long-term key during the handshake. - Server cannot forge credentials: The server stores only the issuer public key per role; forging requires the issuer secret key.
- Opaque server: The server stores only cryptographically verified state blobs and opaque grant ciphertexts. No user identities, role names, or credential material are stored or visible to the server.
Cryptographic components
| Layer | Primitive | Purpose |
|---|---|---|
| Transport | X25519 ephemeral DH, HKDF-SHA256, ChaCha20-Poly1305 | Session keys, AEAD |
| Identity | Schnorr on Ristretto255, BLAKE2b-512 challenge | Server identity binding |
| Credentials | BBS+ on BLS12-381 (zkryptium), SHAKE256 ciphersuite | Blind issuance, ZK presentations |
| Role IDs | BLAKE2b-512 (truncated to 32 bytes) | Opaque role identifiers |
| Grant delivery | X25519 static/ephemeral DH, HKDF-SHA256, ChaCha20-Poly1305 | E2E-encrypted credential grants |
Protocol flow
Unified channel (all connections)
Client Server
|--- init_msg (eph_pk) ------------>|
| | accept()
| | prove_identity() → sign(transcript)
|<-- response_msg + identity_pkt ---|
| complete DH |
| decrypt + verify server sig |
|===== encrypted session ==========>|
|--- {op: "mgmt"} or {op: "auth"}->|
Management commands (create_registry, post_grant, etc.) and BBS+ role authentication both run inside the same encrypted, server-authenticated channel. There is no unencrypted management path.
Grant delivery (admin → recipient, through server)
Admin Server (opaque relay) Recipient
|-- post_grant ------->| |
| (admin_proof, | stores only: |
| recipient_pk, | {eph_pk, ciphertext} |
| eph_pk, | keyed by recipient_pk |
| ciphertext) | |
| |<-- list_grants ------------|
| |--- [{eph_pk, ct}, ...] --->|
| | | trial-decrypt
| |<-- claim_grant ------------|
| | (removes entry) |
Threats considered
Network attacker (passive)
- Observes ciphertexts; cannot break ChaCha20-Poly1305 or derive session keys without breaking X25519 / HKDF under standard assumptions.
- Management traffic is indistinguishable from auth traffic at the wire level (same handshake, same framing).
Network attacker (active / MITM)
- Server impersonation: The server signs the session transcript hash with its long-term Ristretto255 key (
prove_identity). The client verifies this signature against the pinned server public key. A MITM running a separate DH exchange produces a different transcript; it cannot forge the server's signature. The client aborts on mismatch. - Client impersonation: The BBS+ presentation is bound to the session transcript hash. A MITM cannot relay a presentation from one session to another (different transcripts) or forge one (requires a valid credential from the issuer).
- Relay attack: A MITM that relays the real server's identity proof to a client fails because the proof is encrypted under the MITM-to-server session keys (not the client-to-MITM keys), and the signature is over the wrong transcript.
- Management channel: All management commands (registry creation, grants) are protected by the same encrypted channel, eliminating the previous plaintext management path.
Malicious server
- Can learn opaque
role_id, current epoch, and that some valid member authenticated. - Sees
registry_idvalues (needed for routing) but not role names or registry contents beyond opaque state bytes. - Sees
recipient_pkfor mailbox addressing, pluseph_pkand ciphertext per grant, but cannot decrypt grant payloads. - Cannot forge BBS+ credentials without the issuer secret key.
- Cannot learn
member_secretfrom presentations under the BBS+ security assumptions. - Cannot distinguish which specific member authenticated among valid credential holders (unlinkability holds against the verifier for distinct presentation headers).
- Cannot learn the client's long-term public key — it is never transmitted during handshake or auth.
- Cannot perform admin operations (registry updates, grant posting) without a valid admin BBS+ credential.
- Cannot correlate a recipient's mailbox identity with their authenticated sessions (different keys, unlinkable proofs).
Malicious client
- Cannot decrypt others' traffic without session keys.
- Cannot produce valid auth for a role without a valid credential + correct epoch + registry entry.
Denial of service
- Auth packet size: Proof length is capped (
MAX_BBS_AUTH_PROOF_BYTES, 256 KiB) to bound allocations. - Handshake: Fixed 32-byte messages; no variable-length handshake parsing.
- General packet limits should still be enforced at the application layer (total message size, rate limits).
Key distribution
The server's long-term PublicKey (32-byte Ristretto255 point) functions as a self-authenticating identity — no certificate authority is required. The client must obtain and pin this key before connecting.
Recommended strategies:
- Static configuration (default): embed the server public key in client config or CLI pin command (
zkac-node server pin). Equivalent to WireGuard's[Peer] PublicKey = .... - Trust On First Use (TOFU): accept the server's key on first connection, pin it for subsequent sessions. Risk: first connection is vulnerable.
- Out-of-band verification: compare public key fingerprints over a trusted side channel (phone, in-person, encrypted messaging).
- Key registry / directory: a trusted service maps names to public keys. Shifts trust to the registry and its authentication channel.
Operational requirements
- Issuer secret key: Protect
BbsIssuersecret material (HSM, KMS, or encrypted at rest). Compromise = ability to issue arbitrary credentials for that role. - Server long-term key: Protect the server's
server_key.json. Compromise = ability to impersonate the server. Rotate the key and distribute the new public key to clients if compromised. - Member storage:
member_secretand finalizedCredentialmaterial must be protected; loss = re-enrollment required. - Epoch revocation: On compromise or policy change, call
set_epochand re-issue credentials only to legitimate members; old credentials become invalid at verification time. - Registry integrity: Registry state is integrity-protected by BBS+ state certificates (admin must sign updates). The server verifies these certificates before accepting changes.
- Role ID privacy:
role_idis a hash of the role name only if you userole_id("myrole"); treat role names as secrets if enumeration is a concern, or derive role IDs with an additional secret salt known to members. - Client identity: The only persistent client identifier is the issuance public key used for grant mailbox addressing. This key is shared out-of-band with admins; it is not linked to any transport key or BBS+ presentation.
Implementation notes (audit checklist)
- BBS+ proof verification uses the same header and presentation binding as proof generation (
verify_presentationin Rust). - Session transcript is included in the presentation via
present(transcript_hash). - Server identity proof: Schnorr signature over
transcript_hash, verified against pinned public key before any traffic. - Schnorr nonce is deterministic (
H(sk || msg)) — no dependence on RNG quality at signing time. - Replay protection is symmetric per direction in
Session. - Constant-time comparisons are used where critical in transport/replay paths (
subtlecrate). - Client long-term key is never transmitted, preserving BBS+ unlinkability.
- Management and auth channels use the same encrypted handshake (no plaintext management path).
- Admin proofs in management commands are bound to the session transcript hash (no separate nonce).
- Server stores only opaque state bytes, state certs, and encrypted grant blobs (no role names, no user IDs).
- External: Python bindings surface raw bytes; callers must not log secrets (
secret_key_bytes,member_secret,prover_blind). - External: Use secure randomness from the OS (library uses OS RNG for key generation paths exposed in Rust).
Design decisions
- Unified encrypted channel: All traffic (management and auth) uses the same anonymous handshake. This eliminates the attack surface of an unencrypted management path and simplifies the protocol to a single mode.
- Anonymous handshake (
complete_connect_anon): The client verifies the server's identity but does not authenticate itself during the handshake. BBS+ auth is sent as an application-layer message inside the encrypted session, not as part of the handshake. This allows the same channel for both anonymous management and authenticated role access. - Server-only identity proof: Only the server signs the transcript. Adding client long-term signing would break BBS+ unlinkability (the server could correlate sessions by client public key). Client authentication is handled entirely by the anonymous BBS+ credential.
- Deterministic Schnorr nonces: The signing nonce is derived as
H("zkac-schnorr-nonce" || sk || msg), eliminating a class of RNG-failure attacks (cf. PS3 ECDSA, Sony 2010). Same key + same message = same signature. - Opaque mailbox: Grant entries on the server contain only
(eph_pk, ciphertext)— no registry ID or role name. Recipients find their grants by trial-decrypting. This prevents the server from learning which registry or role a grant is for. - No user IDs on server: The server has no concept of user accounts. It is a stateless relay authenticated only by cryptographic proofs.
Known limitations
- No post-quantum primitives: classical security assumptions only.
- Epoch granularity: Revocation is coarse (epoch bump); plan issuance and rotation policy accordingly.
- zkryptium dependency: Security follows the underlying crate and BLS12-381/BBS+ standards; keep dependencies updated.
- Key distribution: The library provides the cryptographic mechanism; initial key distribution is an application-layer responsibility.
- Mailbox metadata: The server sees
recipient_pkas a mailbox address and the number/size/timing of grants. This is inherent to the delivery mechanism.
Reporting issues
Report security-sensitive findings through your project's private disclosure channel (configure SECURITY.md contact or GitHub security advisories when the repository is public).