Skip to content
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.

ComingShips after sign-in for websites. This page describes it as designed; details can change before it ships.

What a membrane enforces, and what it does not

Read this before you design an admission rule

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 ServiceThe DNA directly
ForNetworks that already run a joining serverNetworks with no server
Policy lives inThe service’s config, through a happ_auth auth method we contribute upstreamDNA properties: the issuer roots and the accepted claims
Changing the policyDoes not fork the networkForks the network: suits policies that do not change
Proof the DNA seesThe service’s own membrane proof, as todayhApp Auth’s attestation, verified by the integrity zome

The flow you ship

  1. Install with allow_deferred_memproofs; the conductor creates the agent key.
  2. The UI, with @happauth/holochain, sends the person to hApp Auth with a memproof-request signed by that key and naming the DNA hash.
  3. 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.
  4. The UI calls provideMemproofs, then enableApp.

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).

happ-att/v1
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
# 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_typeValueEvidence
agent-boundA pairwise account id for this DNA (16 bytes)A bind signature by the subject key
github-ageDays since the GitHub account was created, banded: 90, 365 or 1095The linked GitHub account
email-domainThe verified domain, never the addressAn email code at that domain
maintainerA hAppRadar project slughAppRadar’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.