ZKAC/docs/SECURITY.md
everbarry d01a6ebf85 v0.3
2026-04-17 14:10:52 +02:00

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_auth for 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_id values (needed for routing) but not role names or registry contents beyond opaque state bytes.
  • Sees recipient_pk for mailbox addressing, plus eph_pk and ciphertext per grant, but cannot decrypt grant payloads.
  • Cannot forge BBS+ credentials without the issuer secret key.
  • Cannot learn member_secret from 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:

  1. 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 = ....
  2. Trust On First Use (TOFU): accept the server's key on first connection, pin it for subsequent sessions. Risk: first connection is vulnerable.
  3. Out-of-band verification: compare public key fingerprints over a trusted side channel (phone, in-person, encrypted messaging).
  4. Key registry / directory: a trusted service maps names to public keys. Shifts trust to the registry and its authentication channel.

Operational requirements

  1. Issuer secret key: Protect BbsIssuer secret material (HSM, KMS, or encrypted at rest). Compromise = ability to issue arbitrary credentials for that role.
  2. 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.
  3. Member storage: member_secret and finalized Credential material must be protected; loss = re-enrollment required.
  4. Epoch revocation: On compromise or policy change, call set_epoch and re-issue credentials only to legitimate members; old credentials become invalid at verification time.
  5. Registry integrity: Registry state is integrity-protected by BBS+ state certificates (admin must sign updates). The server verifies these certificates before accepting changes.
  6. Role ID privacy: role_id is a hash of the role name only if you use role_id("myrole"); treat role names as secrets if enumeration is a concern, or derive role IDs with an additional secret salt known to members.
  7. 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_presentation in 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 (subtle crate).
  • 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_pk as 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).