Docs · Membrane proofs
Holochain membrane proofs
hApp Auth signs short facts about a person, each bound to one network and one agent key. Your integrity zome verifies them offline, with one signature check, when the agent joins.
What a membrane enforces, and what it does not
A membrane decides who joins. It does not gate who connects or who reads. On Holochain 0.7, apps cannot block agents and warrants are system-only.
Expiry is advisory inside validation. Validation has no clock, and action timestamps are the author’s choice. hApp Auth enforces expires_at when it issues, and the helper refuses stale proofs; validation does not and cannot.
Revoking an attestation does not evict an admitted agent. hApp Auth publishes a signed revocation list that coordinator and UI code can honour softly (refuse to show, refuse to serve). Hard eviction is a new network epoch: you change a property, and hApp Auth re-issues attestations for the new DNA hash to every member still in good standing, in one action.
So the claims are chosen to need little revocation: bound to one key, slow to change, or cheap to re-epoch.
Two ways to use it
hApp Auth proves facts; the network’s operator holds the policy. You never register a network with us. One attestation serves either consumer:
| Holo’s Joining Service | The DNA directly | |
|---|---|---|
| For | Networks that already run a joining server | Networks with no server |
| Policy lives in | The service’s config, through a happ_auth auth method we contribute upstream | DNA properties: the issuer roots and the accepted claims |
| Changing the policy | Does not fork the network | Forks the network: suits policies that do not change |
| Proof the DNA sees | The service’s own membrane proof, as today | hApp Auth’s attestation, verified by the integrity zome |
The flow you ship
- Install with
allow_deferred_memproofs; the conductor creates the agent key. - The UI, with
@happauth/holochain, sends the person to hApp Auth with amemproof-requestsigned by that key and naming the DNA hash. - The person signs in with a passkey and sees: “{network name} asks to confirm: GitHub account older than 1 year.” They press Continue; hApp Auth checks the evidence and returns the membrane proof.
- The UI calls
provideMemproofs, thenenableApp.
If the claim is not true, the person is told exactly why (“Your GitHub account is 4 months old; this network admits accounts older than 1 year”), no attestation is issued and nothing is recorded against them. With a joining server, the UI talks to the Joining Service as it already does and the service returns its own proof.
The format: hApp Attestation v1
Fixed canonical bytes, signed raw with Ed25519 and verified with verify_signature_raw (never verify_signature, which msgpacks first and ties outside verifiers to the HDK’s serializer).
delegation = root_sig( "happ-att/v1/delegation\0" || online_key(32)
|| not_before(u64) || not_after(u64) || key_id(8) )
attestation = online_sig( "happ-att/v1/claim\0" || dna_hash(32)
|| subject_agent_key(32) || claim_type(u16)
|| claim_len(u16) || claim_value
|| issued_at(u64) || expires_at(u64)
|| attestation_id(16) )
membrane_proof = version(u8) || delegation || attestation
The format is an open specification and hApp Auth is its first issuer, not its only one: a DNA pins a set of issuer roots, and any organisation can run an issuer. The verifier crate, happ_attestation_verify, depends only on hdi. Called from genesis_self_check (fail early) and from validate on the AgentValidationPkg (the rule peers enforce), it checks that the root is in the properties, the delegation and attestation signatures hold, the dna_hash is this DNA, the subject is the joining agent, and the claim is accepted.
DNA properties
# dna.yaml — the network's admission policy lives in its properties
properties:
happ_attestation_roots:
- "<hApp Auth's 32-byte root key>"
- "<another issuer's root key>" # several issuers are normal
happ_attestation_accept:
- claim_type: github-age
min_value: 365 # days
Roots are long-lived and held offline, because changing properties forks the network. The online issuing key rotates under a root-signed delegation without a new DNA hash. Several roots from several issuers are normal.
The claims, v1
| claim_type | Value | Evidence |
|---|---|---|
agent-bound | A pairwise account id for this DNA (16 bytes) | A bind signature by the subject key |
github-age | Days since the GitHub account was created, banded: 90, 365 or 1095 | The linked GitHub account |
email-domain | The verified domain, never the address | An email code at that domain |
maintainer | A hAppRadar project slug | hAppRadar’s claim proof |
There is no “verified human” claim. Each new type needs a named source of evidence and a reason it needs little revocation.
What a claim costs to fake
github-age is a cost, not a person: aged GitHub accounts are sold, seasoned ones for thousands of dollars, and a sold account keeps its attestations because revocation inside a network is soft. maintainer is only as strong as hAppRadar’s claim proof and its bar for listing a project. Each attestation names one DNA, so one bought account yields one proof per network it asks for, never a proof that replays across networks, and issuance is rate-limited per account.
Combine claims where it matters, and where admission should be a person’s judgement, choose the Joining Service’s operator approval instead.