Skip to content

surroundapps/human-credential

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Human Credential

An eligibility credential issued by an independent party and revocable at the moment of use — where the person also chooses which single attribute to reveal, and consents by signing. A reference implementation.

A claim you make about yourself is worth what you are worth; a credential is worth what its issuer is worth. The difference is not the signature — a self-signed claim verifies too. It is that someone other than the subject put their name behind the claim and can withdraw it. Human Credential builds on that distinction: the eligibility credential here is issued by a party other than the holder, checked against the issuer's signature, and — the property that makes it a credential rather than a signed assertion — checked against revocation at the moment of use, not frozen at issuance. Withdraw it upstream and the very next presentation is refused, while every earlier record stays intact.

On top of that foundation sits the move unique to a person as the subject: the holder chooses what to reveal. A door needs only "over 18"; a regional service needs only "region." Handing over the whole credential to prove one attribute leaks the rest, so the issuer signs salted commitments to each attribute, never the raw values — everything the holder does not reveal stays an opaque hash the verifier cannot read, or even name. The holder reveals exactly the attribute asked for and consents by signing that disclosure with their own key. Each verification is sealed into the verifier's tamper-evident log.

This is infrastructure, published as a reference. It is not a product.

The argument behind it is written up in the essay A Claim Is Not a Credential: independent issuance, revocability, and recourse are what separate a credential from a self-signed assertion. This repo makes the first two runnable. The third — recourse, who is answerable when an issuer vouches wrongly — is institutional, not something running code can demonstrate, and it is named here as an honest limit rather than claimed.

It is built on a shared trust primitive, provenance-core — revocable verifiable credentials and a tamper-evident hash-chained ledger — vendored into this repo at core/. The same primitive underlies two sibling implementations: one for the agent that acts (an agent whose every action is authorized by a revocable credential and sealed into a tamper-evident log) and one for the good that moves (a checkpoint that verifies a good's provenance offline). This repository is the primitive applied to the person who presents — the only one of the three subjects that actively chooses, at use time, what to reveal.

The 30-second story

Two things to watch: whether the credential still stands at the moment it is used (issuance and revocation — what makes it a credential), and how little the verifier learns (selective disclosure — what makes it a person's credential).

  1. Issue a credential to a person with several attributes. Only salted commitments are signed in — never the raw values.
  2. Present to a door that gates on age: the holder consents to reveal only over_18. The door learns the person is over 18, sees that three other commitments exist, and cannot read any of them.
  3. Present to a regional service: the holder reveals only region. Across both, name and member id are never disclosed to anyone.
  4. Binding fails (the live presenter is not the subject): rejected, even though the credential is valid and consented. (This biometric check is stubbed; the flag stands in for a real match.)
  5. Revoke the credential and present again: rejected at use time, while every earlier record stays intact. Withdrawal does not rewrite history.
  6. Tamper with a sealed record (a sandbox button) and the chain verification catches it, pointing at the exact record where trust ends.

Quickstart

Never used a terminal? Follow the step-by-step guide instead — it assumes no prior experience and takes about five minutes.

make dev          # install the vendored core, then this package + test deps
make test         # the trust + privacy checks, no services required
make run          # serve on http://localhost:8000
# in another terminal:
make demo         # drive the full flow above via curl

Then open http://localhost:8000 and walk the story.

# Or drive it directly:
curl -X POST localhost:8000/verifier -H 'content-type: application/json' \
     -d '{"name":"Door","requests":["over_18"]}'                    # -> verifier_id
curl -X POST localhost:8000/issue -H 'content-type: application/json' -d '{}'   # -> credential_id
curl -X POST localhost:8000/present -H 'content-type: application/json' \
     -d '{"credential_id":"<cid>","verifier_id":"<vid>","reveal":["over_18"]}'  # reveals only over_18
curl localhost:8000/log/<vid>                                       # sealed log + chain check
curl -X POST localhost:8000/scenario/selective-disclosure           # the headline story

How it works

Credentials come from provenance_core and follow the W3C Verifiable Credentials data model, signed with Ed25519. An eligibility credential is typed SubjectEligibilityCredential and carries one capability, present:eligibility. Its credentialSubject holds the holder's DID, the hash algorithm, and a sorted list of salted attribute commitments (_sd) — never the raw values. Issuance mirrors the core's exact signing convention, so the core's verify_credential validates the issuer signature and revocation unchanged.

Selective disclosure (human_credential/credential.py) is the heart of this repo. The holder keeps the raw (salt, name, value) disclosures and reveals only a chosen subset; the verifier recomputes each revealed hash and checks it is one the issuer committed to. Unrevealed attributes stay opaque. This works on plain Ed25519 because the signature is over the commitments, which never change.

Consent is a signed act. The holder has their own keypair (the subject DID is theirs) and signs each disclosure, bound to the verifier and a nonce. The verifier recovers the holder's public key from the subject DID embedded in the credential — did:key is self-describing, no network resolution — and checks it. No valid holder signature, no acceptance.

Binding has two halves. Key control is real (the holder signs; the verifier checks against the subject DID). The match proving the live presenter is the enrolled human is stubbed: a binding_ok gate the verifier requires, standing in for a biometric. The contribution is the layer around that check, not the matcher. See ADR-0001.

The presentation record is sealed into the verifier's hash chain using provenance_core.ledger, unchanged. The sequence of presentations is tamper-evident, and each record carries only what the verifier legitimately learned — the disclosed attributes and how many commitments stayed closed.

Two axes, as in the siblings: chain integrity (was the record edited?) and the verification outcome (accepted / rejected and why). Revoking and presenting again moves the second and leaves the first untouched.

The headline case: one credential, two verifiers

A holder reveals only over_18 to an age-gated door and only region to a regional service — and across both, name and member_id are never disclosed to anyone. Then a presentation is refused when the biometric binding fails, and another is refused at use time after the credential is revoked.

This is the person-side mirror of the sibling offline beats: there, an actor kept acting while blind to a revocation, and a checkpoint could not confirm one; here, a person's revoked credential is refused at the moment of use, having revealed only the minimum at every step.

Project layout

core/                          provenance-core, vendored via git subtree
  provenance_core/             canonical, keys, credentials, ledger, reconcile
  docs/adr/                    the core's own decisions
human_credential/
  credential.py   issuance with salted commitments, consent-signed presentations,
                  verification with the stubbed binding seam (the heart)
  verifier.py     verify a presentation and seal it into the verifier's chain
  scenario.py     the one-credential, two-verifiers headline story
  storage.py      SQLite (verifiers, presentation log, credentials, wallet, revocations)
  app.py          FastAPI — endpoints + the presentation-log replay, serves the UI
  fixtures/       the demo holder's attributes
web/              single-page presentation-log UI (vanilla JS, no build step)
tests/            the trust + privacy guarantees, including pinned invariants
docs/
  DESIGN.md       architecture narrative, how it relates to the shared core
  adr/            ADR-0001: selective disclosure, consent, and the binding stub
RUNNING.md        step-by-step run guide for a non-technical first-time user
scripts/demo.sh   terminal walkthrough of the full flow

The trust primitives live in core/ (provenance-core) because they are domain-neutral; this repository builds the person/eligibility implementation on top of them. core/ is vendored with git subtree, so git clone of this repo is self-contained — there is no submodule init step, and make dev installs the core from the local path before this package.

What this is not

  • Not zero-knowledge predicate proofs. The holder reveals a pre-derived boolean (over_18), not a ZK proof over a hidden birthdate. This is a real minimization but a different mechanism from BBS+/ZK; the two are not conflated, and ZK predicates are deferred, not faked.
  • Not a biometric matcher. Key control is proven; the human-to-key match is a named, stubbed gate.
  • Not a DID resolver. The holder's key is recovered from the self-describing did:key, not resolved over a network.
  • Not a distributed system, in the demo. One process plays issuer, holder wallet, and verifier so it runs with no services; a real deployment separates them. The trust logic is identical.
  • Not a gate. It records and proves a disclosure and a decision; it does not admit anyone or actuate anything. The receipt, not the gate — the boundary shared across the whole provenance-core family.
  • Not a recourse mechanism. The credential identifies an independent issuer who signs and can revoke — but who is answerable, and how, when an issuer vouches wrongly is a legal and institutional arrangement this code neither establishes nor can. Independent issuance and revocability are demonstrated here; recourse is named as the missing third leg, not simulated.

License

Apache-2.0. Copyright 2026 SurroundApps, Inc.

About

Selective-disclosure eligibility credentials: prove you're over 18 and reveal nothing else, with consent signed by the holder and revocation checked at use time. SD-JWT-style salted commitments on plain Ed25519; biometric binding left as an explicit stub. A reference implementation on provenance-core.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors