Back

Research

SansID

Every age gate on the internet asks you to upload a photo of your ID and a selfie. That is a privacy disaster: every website and every verification vendor gets a permanent copy of your most sensitive document, just to learn a yes-or-no fact. SansID proves you are over 18 without ever revealing who you are, using BBS+ anonymous credentials and blind issuance.

The problem is structural. Right now, if a website wants to know whether you are old enough, the only widely deployed answer is document upload. Your passport, your driver's license, your face. That data gets stored by the website, by their verification vendor, by whoever they resell it to, by whoever breaches them. Encryption does not fix this. The data is collected in plaintext because the verifier needs to read it.

SansID inverts the model. A trusted issuer checks your age once, under whatever evidence policy they require. In person at a bank, through a government digital ID flow, however they want. They sign a narrow credential: age_over_18. That is it. No name, no birthdate, no photo. From that point forward, you can prove that single fact to any service that trusts that issuer, without the issuer ever being contacted again. Every proof is fresh, bound to one website, and cannot be reused anywhere else.

How it works

Blind issuance

This is the cryptographic trick that makes the whole thing private. When you go to the issuer to get your age credential, you do not just hand them a public key and ask them to sign it. If you did that, the issuer could recognize that same public key later when you use the credential. You would be trackable across every service you prove your age to.

Instead, SansID uses blind issuance. Your device creates a random holder secret and sends the issuer a cryptographic commitment to it, plus a zero-knowledge proof that you know the secret behind the commitment. The issuer signs the commitment without ever learning the actual secret. The resulting BBS+ credential is bound to your holder secret, but the issuer cannot link it back to you later. When you present a proof to a service, the service sees a fresh randomized proof that you hold a valid credential, not a stable identifier.

BBS+ credentials

BBS+ is a pairing-based signature scheme on the BLS12-381 curve. It lets the holder of a credential prove individual attributes without revealing the full credential. SansID uses a fixed profile: one message in the credential, the predicate age_over_18. The proof reveals exactly that one predicate and nothing else. No credential signature, no holder secret, no birthdate.

Proof binding

Every proof is cryptographically bound to three things: the canonical HTTPS origin of the requesting website, a fresh 32-byte nonce from the verifier, and the issuer's public key ID. This means a proof made for site-a.example cannot be replayed on site-b.example. A proof from last week cannot be reused today. The service verifies the proof locally against its pinned issuer public key and atomically consumes the nonce. No network call to the issuer, no central relay, no correlation point.

The protocol

Three parties, two phases. Clean separation between the one-time issuance step and every subsequent verification.

Issuance happens once:

  1. Person proves age to a trusted issuer using whatever evidence the issuer requires
  2. Holder app creates a random private holder secret on the device, never sent to the issuer
  3. App sends a blind request: a commitment to the secret plus a zero-knowledge proof of knowledge
  4. Issuer verifies the age evidence, signs the commitment, returns a BBS+ credential
  5. App verifies the credential and stores it locally in an encrypted envelope

Verification happens on demand:

  1. Service backend creates a proof request with origin, predicate, and a fresh nonce
  2. Website forwards the request to the holder app through a native bridge
  3. Holder app shows the website and the requested fact, user approves
  4. Operating system authorizes access to the credential envelope
  5. App creates a fresh BBS+ proof bound to that origin and nonce
  6. Service backend verifies the proof against its pinned issuer key, consumes the nonce

What the code does

The Rust crate is the cryptographic core. About a thousand lines, zero unsafe. It implements:

  • BBS+ credential issuance and presentation on BLS12-381
  • Blind issuance with Schnorr proof-of-knowledge of the holder secret
  • Verifier-side proof request construction and presentation verification
  • Full origin binding, nonce binding, and nonce-reuse rejection
  • Key pinning so the verifier only trusts explicitly configured issuers
  • Serialization profiles for the wire protocol

The holder app itself is a native application, not a web app. Credential envelopes are stored locally and unlocked through the operating system's hardware protection: TPM 2.0 on Linux, CNG/TPM on Windows, Secure Enclave on macOS and iOS, Android Keystore. The core crate defines the contract between these platform adapters and the cryptographic logic.

The limits

This is a research project and a reference implementation. It is not a deployed identity system and does not include a production holder app, a trust registry, or real-world issuer infrastructure.

SansID makes the proof payload itself private. It does not make the surrounding traffic private. A service can still track users through accounts, cookies, IP addresses, and browser fingerprints. Privacy-respecting deployments need to handle those signals separately.

The holder secret must stay on the device. If someone extracts it from the TPM or Secure Enclave, they can impersonate the holder. That is the hardware trust boundary and there is no way around it.

Install and run

cargo install sansid for the command-line tool, or clone for the examples:

shell
git clone https://github.com/chrisch88dev/SansID
cd SansID
cargo test --all-targets
cargo run --example local_test_issuer
cargo run --example verifier_integration
cargo run --example age_gate

Apache 2.0. On GitHub and crates.io.

github.com/chrisch88dev/SansID

crates.io/crates/sansid