Schnorr-based zero-knowledge proof authentication library.
Snarkey implements the Schnorr identification protocol with Fiat-Shamir non-interactive proofs, interactive fallback, blind-hashed identities, and WebAuthn PRF passkey binding support.
Core guarantees
- No password on the wire — secret
xnever leaves the client - No password in the database — server stores only
public_y = g^x mod p - No linkable identity — database rows use HMAC-blind identifiers
- Replay-proof — every proof is bound to a timestamp window
- Hardware-backed secrets — WebAuthn PRF derives
xfrom passkey seeds
Integration
Snarkey is a pure functional library with zero runtime dependencies. The caller provides storage, transport, and session handling.
See the individual module docs for detailed API reference:
Snarkey.Crypto— modular exponentiation, hash-to-scalar, default paramsSnarkey.Proof— key generation, commitment, response, verificationSnarkey.Identity— blind identity hashingSnarkey.Fallback— interactive challenge-response
Summary
Functions
Verifies a Schnorr proof against a public key.
Produces a blind identifier by HMAC-SHA256(pepper, raw_id).
Returns the default 2048-bit Schnorr group parameters.
Verifies a registration proof-of-ownership for a new public key.
Types
Functions
@spec authenticate(binary(), map(), keyword()) :: {:ok, :authenticated} | {:fallback, binary()} | {:error, atom()}
Verifies a Schnorr proof against a public key.
Non-interactive (Fiat-Shamir) path
The proof map must contain :r, :s, and :timestamp keys.
Options
:params— crypto params (defaults todefault_params/0):user_id— user identifier used in the Fiat-Shamir hash:max_drift— max timestamp skew in seconds (default 5)
Returns
{:ok, :authenticated}— proof valid{:fallback, challenge}— proof expired, challenge binary for interactive re-auth{:error, :unauthorized}— proof invalid{:error, :expired}— timestamp outside drift window
Interactive path
The proof map must contain :r, :c, and :s keys. Used after the
caller has obtained a challenge from Snarkey.Fallback.generate_challenge/1.
Options
:params— crypto params (defaults todefault_params/0)
Returns
{:ok, :authenticated}— proof valid{:error, :unauthorized}— proof invalid
Produces a blind identifier by HMAC-SHA256(pepper, raw_id).
Reads the pepper from Application.get_env(:snarkey, :pepper).
Examples
iex> blind = Snarkey.blind_identity("user@example.com")
iex> is_binary(blind)
true
Returns the default 2048-bit Schnorr group parameters.
Delegates to Snarkey.Crypto.default_params/0.
Examples
iex> params = Snarkey.default_params()
iex> params.p |> is_integer()
true
iex> params.g
2
Verifies a registration proof-of-ownership for a new public key.
The client sends {public_y, r, s} and the server uses a pre-generated
nonce to compute c = hash_to_scalar(g || y || r || nonce) and verify
the proof. This ensures the client knows the secret key corresponding to
public_y without revealing it.
Options
:params— crypto params (defaults todefault_params/0):nonce— required. Server-generated registration nonce (binary):user_id— optional user identifier for logging
Returns
{:ok, :registered}— proof is valid, caller should storepublic_y{:error, :invalid_proof}— proof failed verification