Skip to content

prof-faustus/identity-attribution

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SCARCITY identity attribution

A secure, privacy-preserving identity attribution system. An issuer attests a document (birth year, photo, attributes); the holder is enrolled in an indefinite-scale registry; and a verifier can confirm facts — "this is a genuine, enrolled identity, the holder is over 21, and this is the enrolled photo"in zero knowledge, learning nothing else. Registry roots are anchored through the BSV-canonical Merkle tree(s) already in this repo set.

Ask "are you over 21?" and the holder answers with a zero-knowledge proof derived from the issued document — the birth year is committed, never revealed. The same pattern proves the photo binding and any number of other attributes. It generalises to any identity in a 2^256 keyspace, not a hardcoded example.

Architecture (Rust workspace, 5 crates)

Crate Role Maps to
idattr-smt Sparse Merkle tree over a 256-bit keyspace — inclusion and non-inclusion (revocation) proofs, lazy empty subtrees, O(256) per op the indefinite-scale spine
idattr-zkp Pedersen commitments + Schnorr-OR range proof (age ≥ N) + sigma opening (image/photo binding), Fiat–Shamir, on Ristretto SCARCITY 09 attestation spirit
idattr-cred Issuer-signed document credentials (Ed25519), committed birth year + image hash, canonical CBOR, deterministic identity keys issuance / enrolment
idattr-anchor Federation: batch registry roots over epochs into vaa-merkle (WO 2022/100946 A1) and an independent BSV backend; two-hop proof to the on-chain anchor root "the merkle tree in git"
idattr-cli idattr binary: demo, corpus, scale; the full issue→enrol→prove→verify flow E2E + generalization harness

How a proof composes

identity  --SMT inclusion-->  registry root  --list-Merkle inclusion-->  anchor root  --on-chain
   |                              |                                          |
   +-- Ed25519 issuer signature   +-- ZK: age >= 21 (range proof)            +-- one root, constant
       over the credential            ZK: photo == enrolled (opening)            footprint at any scale

The verifier needs only the issuer key and the anchor/registry root. No secret ever reaches it.

Why it scales indefinitely (the "10 billion+" requirement)

  • The registry is a full binary tree of depth 256 → 2^256 ≈ 1.16e77 possible identities.
  • Empty subtrees are identical and precomputed, so storage tracks occupied leaves only.
  • Every insert touches exactly 256 nodes; every proof is ≤256 siblings (only the non-empty ones sent).
  • Identities are derived deterministically from a handle (SHA-256), so any leaf is addressable without enumeration. Only one root is anchored on-chain regardless of population.

Measured (idattr scale): per-identity cost is flat as population grows —

  population   insert (us/op)   prove+verify (ms/op)
        1000            387.2                 11.735
       10000            426.7                 12.366
       50000            394.7                 11.530

Run it

cargo test                 # 28 tests across the 5 crates
cargo run --release -p idattr-cli -- demo
cargo run --release -p idattr-cli -- corpus --count 10000
cargo run --release -p idattr-cli -- scale --steps 1000,10000,50000

corpus --count 10000 (verified): issues + enrols 10,000 identities, then for every one builds a holder bundle and verifies it — 8,177/8,177 adults proven over-21 and fully verified, 1,823/1,823 genuine minors unable to forge an over-21 proof (soundness), 0 anomalies, and a never-enrolled "ghost" rejected by the non-inclusion check.

Service deployment (Docker compose)

Three HTTP services + a one-shot acceptance client (idattr-svc):

Service Endpoints
issuer (:8081) GET /key, POST /issue
registry (:8082) POST /enrol, GET /root, POST /prove, GET /anchor
verifier (:8083) POST /verify
client one-shot: issue → enrol → prove → verify, asserts the result
docker compose up --build --abort-on-container-exit --exit-code-from client

The client drives the full flow across the services and exits non-zero unless attribution verifies; it also runs a negative case (a never-enrolled "ghost" must fail the inclusion check). For a local (non-container) run: idattr-svc issuer / registry / verifier in three shells, then idattr-svc client.

On-chain anchoring is wired as documented compose comments: attach registry to the running Teranode regtest network and provide its RPC. The broadcast itself needs a funded regtest key and is performed by the scarcity-system/chain tooling — see HONESTY below.

Federation & on-chain anchoring

idattr-anchor commits the epoch log of registry roots with every backend and checks they agree. Today: the real vaa-merkle (patent WO 2022/100946 A1) and an independent BSV re-implementation produce byte-identical anchor roots. The other trees in the repo set — the scarcity-system chain tree, the triple-entry tee_merkle, the AnchorChain TS tree — plug in behind the same MerkleBackend trait.

HONESTY: what is real here vs. what needs live infrastructure / your credentials

Real and tested in this workspace (28 tests + the 10k corpus): the SMT registry, the ZK age and image proofs, issuer signing, the full prove/verify flow over 10,000 identities, and the off-chain Merkle federation (registry root → anchor root) across two independent backends.

Needs live infrastructure or your credentials — intentionally NOT faked:

  1. On-chain anchoring of the anchor rootDONE (companion idattr-onchain). The anchor root is broadcast via the note-anchoring template (SCARCITY REQ-CHAIN-0003, never OP_RETURN): the 32-byte root rides as pushdata + OP_DROP ahead of a native P2PKH spend tail, so the commitment output is itself the spendable possession outpoint (REQ-CHAIN-0001/0002, REQ-CHAIN-0051, REQ-BUILD-0010). Verified live against the Teranode regtest node (RPC 127.0.0.1:9292) with a funded regtest key — accepted by sendrawtransaction and mined; see idattr-onchain/ANCHOR_RECEIPT.md.
  2. Device binding — interface + verifier DONE (idattr-device); genuine hardware still needs real silicon. idattr-device defines the full attestation/binding interface: an Attestation over the app measurement, device key, freshness nonce and non-exportable flag (root-signed); a fail-closed verify_attestation (allowlisted measurement, fresh nonce, valid root sig); a device_cert_commitment(device_pub, measurement) the issuer embeds in the credential (Issuer::issue_with_device); and bind / verify_binding tying a presentation to the attested, enrolled device under a fresh challenge. The flow + idattr device demo show an honest presentation verifying while a replay (stale nonce) and a leaked credential presented from another device are rejected. Boundary kept honest: idattr-device's SoftwareDeviceSee is a software emulation — its "non-exportable" key and attestation root are ordinary in-process keys, not a secure element. Genuine non-exportability + a hardware-rooted attestation chain require the scarcity-device-client (Android StrongBox/TEE) on physical secure-element hardware, with the vendor attestation root verified off-device. The real quotes plug into this same verifier interface. A runnable software stand-in for that device lives in the companion tee-sim repo (a simulated TEE, CLI + HTTP), wire-compatible with idattr-device and pinned by a cross-repo known-answer vector. See docs/ARCHITECTURE.md for how all three boundaries fit together.
  3. Compact bulletproofs backendDONE (the bridge is wired). The in-repo range proof is self-contained and linear-size (32 BitProofs); the AnchorChain TS bulletproofs give a logarithmic proof of the same age statement. anchorchain/packages/privacy/src/agebridge.ts (proveAgeAtLeastBP / verifyAgeAtLeastBP) expresses this crate's exact predicate on the secp256k1 Bulletproof: the verifier recomputes C_delta = threshold·G − C_birth from the issuer commitment, so soundness is identical to prove_age_at_least / verify_age_at_least here. The two backends are over different groups (Ristretto vs secp256k1), so the bridge federates the statement, not a shared commitment — to use it the issuer also commits the birth year in secp256k1. Verified: 6 tests on the same vector (birth 1990 / threshold 2005), proof is 5 inner-product rounds for 32 bits vs. 32 linear BitProofs here.
  4. Document/biometric ingestion — pipeline + real MRZ validation DONE (idattr-ingest); capture hardware, biometrics, and the issuer HSM remain real-world boundaries. idattr-ingest implements a genuine ICAO 9303 TD3 / MRZ parser with the standard check-digit algorithm (tested against the Doc 9303 examples), DocumentCapture / PortraitCheck boundary traits, an IssuerRoots trust set, and an IngestPipeline that validates the MRZ, runs the biometric check, derives (subject, birth_year, image_hash, attributes) and issues a committed credential. The idattr ingest demo runs simulated-scan → validate → issue → enrol → prove → verify; a MRZ with a bad check digit is rejected before issuance. Boundary kept honest: the SimulatedPassportScanner only constructs a valid MRZ and AcceptingPortraitCheck does no matching — genuine capture (camera OCR / NFC chip read + passive authentication against the national CSCA), liveness + 1:1 face matching, and an HSM-held issuer key need real hardware/SDKs and the issuer's credentials.

Status

  • idattr-smt — sparse Merkle registry (indefinite scale)
  • idattr-zkp — age range proof + image binding (ZK)
  • idattr-cred — signed document credentials + deterministic identity
  • idattr-anchor — federated anchoring (vaa-merkle + independent backend)
  • idattr-cli — E2E + 10k-corpus generalization + scale check
  • idattr-svc + Docker compose — issuer/registry/verifier services + acceptance client
  • idattr-device — device-binding interface: attestation (measurement allowlist, fresh nonce, non-exportable, root-signed) + presentation binding to the issuer-certified device. Software SEE here; genuine hardware via scarcity-device-client. idattr device demo shows replay and leaked-credential rejection.
  • idattr-ingest — issuer-side ingestion: real ICAO 9303 MRZ parse + check-digit validation, capture/biometric boundary traits, issuer-root trust, and an issue pipeline. idattr ingest demo: simulated scan → validate → issue → enrol → prove → verify. Genuine capture/biometrics/HSM remain real-world boundaries.
  • Live on-chain broadcast of anchor roots — done (Teranode regtest), via the note-anchoring template (no OP_RETURN). See the companion idattr-onchain crate and its ANCHOR_RECEIPT.md: a real registry root 700e28d3…41d74d4 committed in tx 068093ae…97840580 as a spendable <root> OP_DROP <P2PKH> possession outpoint, mined into block 309 (num_tx = 2) with a node-accepted BIP143/FORKID signature.

About

SCARCITY identity attribution: indefinite-scale sparse-Merkle registry + zero-knowledge over-21/image proofs + federated BSV anchoring + Docker service deployment

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors