Skip to content

About

AnonCreds method backed by Archon (did:cid) — an ACA-Py plugin. Issuer-rooted DID URL identifiers, content-addressed objects, revocation carried by a DID's operation log.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AnonCreds on Archon (did:cid)

An ACA-Py plugin implementing an AnonCreds method backed by Archon, so an existing Aries/AnonCreds deployment can use an Archon node as its Verifiable Data Registry.

Status: working prototype. Exercised end to end against a live Archon node and two ACA-Py agents; not yet submitted to the AnonCreds Methods Registry. Requires no changes to Archon.

It implements both halves of ACA-Py's pluggable AnonCreds interface in one class:

  • a resolver, which needs nothing but unauthenticated HTTP — no wallet, no account, no payment;
  • a registrar, which signs Archon DID operations with the issuer's wallet.

Why Archon

Archon is a content-addressed DID platform: a DID is the content identifier of its own genesis operation, and a DID document is reconstructed by replaying that DID's append-only operation log. Two properties carry the method.

Content addressing. An AnonCreds object can be named by the hash of its own bytes, so integrity verification is trust-free: fetch the object from any node, re-hash it, compare against the identifier you were given. No trust in the server that answered.

Replayable history. A DID is an ordered log of individually addressable operations — no document is ever stored — so mutable state like a revocation status list can be reconstructed as of any past moment, without snapshots or a separate index.


Identifier syntax

Object identifiers are issuer-rooted DID URLs: the issuer's DID is the identifier's own root, so an object's association with its issuer is structural rather than a lookup.

did:cid:<issuer>?resource=<resource>&resourceType=<type>
Object resourceType resource
Schema anonCredsSchema CID of the schema
Credential definition anonCredsCredDef CID of the credential definition
Revocation registry definition anonCredsRevocRegDef CID of the definition
Revocation status list anonCredsStatusList the revocation registry's DID

Two checks run on every resolve, and a failure of either is fatal:

  1. Content address — the retrieved object must hash to the CID the identifier names.
  2. Issuer binding — the object's issuerId must equal the identifier's issuer root. Without this an issuer could publish an identifier rooted at their own DID pointing at someone else's object.

The three immutable object types are stored as the verbatim serialized AnonCreds object in RFC 8785 (JCS) canonical form — no method-specific envelope, so the bytes a CID commits to are exactly the bytes an AnonCreds implementation consumes.


Revocation

Archon has no accumulator or bitstring status-list primitive, so the revocation registry is its own DID, and each status-list update appends one operation to its log. The DID carries a pointer, not the list:

{
  "anonCredsObjectType": "anonCredsRevocationRegistry",
  "issuerId":    "did:cid:<issuer>",
  "revRegDefId": "did:cid:<issuer>?resource=<cid>&resourceType=anonCredsRevocRegDef",
  "entry": {
    "revocationListCid":  "<CID of the RevList>",
    "currentAccumulator": "<accumulator>",
    "timestamp":          1755500600
  }
}

The list lives in IPFS because Archon bounds a single DID operation at 64 KB while a revocation list grows with maxCredNum. The pointer form is 1,222 bytes regardless of registry size. The accumulator is duplicated inline because it is small and is what a non-revocation proof actually consumes, so a verifier needing only the accumulator makes one fetch instead of two.

Reading state at a point in time

get_revocation_list(..., timestamp_to=T) returns the entry with the greatest AnonCreds timestamp at or before T, found by replaying the registry's operation log to successive depths with Archon's versionSequence selector.

It deliberately does not bound the replay by T as wall-clock time. An AnonCreds timestamp is chosen by the issuer and signed into the status list, whereas a VDR's own event time is set when the operation is written — after the list has been serialized and published. The two differ by however long publishing took, so bounding on wall-clock time selects the wrong entry whenever that difference crosses an entry boundary, and does so silently. A wrong answer about whether a credential is revoked is the worst failure this plugin could have.

update_revocation_list refuses to publish an entry timestamped earlier than its predecessor, which keeps entry timestamps non-decreasing and makes the search binary.


Install and configure

poetry install --extras aca-py     # add --extras wallet for the in-process wallet

Load it like any ACA-Py plugin:

plugin:
  - archon_anoncreds

plugin_config:
  archon-anoncreds:
    node_url: http://your-archon-node:4222
    registry: hyperswarm
    tails_dir: /var/lib/acapy/archon-tails

    # A wallet, by any of three routes. Omit all and the agent is resolve-only:
    # every registration fails with a clear error rather than degrading.
    wallet_path: /var/lib/acapy/archon-wallet.json
    wallet_passphrase: ...
Setting Default Meaning
node_url http://localhost:4222 Drawbridge. All reads and IPFS uploads. Needs no credentials.
wallet_path / wallet_passphrase unset A file wallet. On its own this selects the in-process transport.
keymaster_cli unset Reach the wallet via the @didcid/keymaster CLI instead.
keymaster_url unset Reach the wallet via a Keymaster HTTP service instead.
gatekeeper_url node_url Where signed operations are submitted.
admin_api_key unset Sent as X-Archon-Admin-Key when Keymaster is protected.
registry hyperswarm Archon registry new revocation-registry DIDs are anchored to.
tails_dir /tmp/archon-anoncreds-tails Where resolved tails files are cached.
allow_unconfirmed_reads false Read through the legacy /api/v1/did/:did surface. See Assurance.
request_timeout 30 Seconds.

Three ways to reach a wallet

Registration signs Archon DID operations, so it needs a wallet; resolution never does. Nothing downstream of the registrar knows which transport is in use:

in-process keymaster_cli keymaster_url
Selected by wallet_path alone keymaster_cli keymaster_url
Mechanism archon-keymaster in the agent's event loop subprocess to the Node CLI HTTP to a Keymaster service
Listening port no no yes
Extra runtime none Node a running service
Errors surface as exceptions stderr, exit code 0 HTTP status

Prefer the in-process transport. It is native Python, runs in ACA-Py's own event loop, and reports failures as exceptions. The CLI is a fine fallback but exits 0 on error, so its wrapper validates output rather than return codes.

Note that archon-keymaster pins bolt11==2.2.0, which caps at Python <3.13 while acapy-agent requires >=3.13. pip cannot resolve both; uv can. The dependency is therefore optional, with the CLI as fallback.

All three keep the private key in the wallet — the plugin never signs.

Issuing requires a tails server

ACA-Py refuses to create a revocable credential definition unless tails_server_base_url is set, and uploads the tails file there before this plugin's registrar runs. The plugin stores the tails file in Archon regardless and rewrites tailsLocation to its CID, so verifiers never contact the tails server — it is an issuer-side requirement of ACA-Py only.


Assurance

On a non-anchored registry (local, hyperswarm), entry timestamps are issuer-claimed. Nothing external attests that a revocation was published when the issuer says it was.

Registry Timestamp assurance Publish latency
local Issuer-claimed. Single node, no gossip. Immediate
hyperswarm Issuer-claimed, replicated across peers. ~1 second
BTC:signet, BTC:mainnet, … Anchored — ordering and an upper time bound attested by the chain. As long as anchoring takes

Independently, the resolution surface returns confirmed state, so a just-published update is briefly invisible to verifiers. On local and hyperswarm that window is about a second; on an anchored registry it is as long as anchoring takes. Issuers needing read-your-writes can set allow_unconfirmed_reads: true, trading the forced verification and correct 404s for immediacy.

If you need revocation timestamps a relying party can trust without trusting the issuer, use an anchored registry.


Tests

poetry run pytest archon_anoncreds -q          # unit, no network
poetry run ruff check archon_anoncreds

# integration: needs an Archon node, but no Docker — the issuer's wallet is a
# throwaway file wallet created per session and driven through the Keymaster CLI
ARCHON_NODE_URL=http://your-node:4222 \
ARCHON_GATEKEEPER_URL=http://your-node:4224 \
  poetry run pytest integration/tests -q

python demo/acapy_flow.py                      # full ACA-Py credential lifecycle

No node to point at? integration/docker-compose.yml brings up a self-contained one — a convenience, not a dependency. See integration/README.md and demo/README.md.

What is verified

Against a live Archon node:

  • round-trip of all four object types through registrar and resolver
  • tails-file upload, fetch, CID and tailsHash verification
  • revocation publish, update, and time-pinned reads across entry boundaries
  • rotation safety — after an issuer rotates keys, previously published objects still resolve and pre-rotation revocation state resolves correctly
  • rejection of tampered content, foreign issuer roots, wrong resource types, backwards timestamps, and registration for an issuer the wallet does not hold

With two real ACA-Py agents (demo/):

  • issuer publishes schema, credential definition and both revocation registries
  • credential issued over DIDComm; the holder resolves every object from Archon with no wallet configured
  • proof with a non-revocation interval verifies true
  • after revocation, the same credential verifies false

What is not

Behaviour against a Drawbridge with L402 enabled, against an anchored registry such as BTC:signet, and at large maxCredNum. See FINDINGS.md §D.


Documents

SPEC-DRAFT.md Draft AnonCreds method specification — identifier syntax, storage, resolution rules, security considerations
FINDINGS.md Every assumption verified or violated, deviations from the original design, and proposals for upstream Archon and ACA-Py
WORKPLAN.md Interface-to-Archon-call mapping and the phase plan
demo/README.md Running the full ACA-Py lifecycle, and the two ACA-Py quirks you will hit
integration/README.md Integration test setup

Licence

MIT — see LICENSE.

About

AnonCreds method backed by Archon (did:cid) — an ACA-Py plugin. Issuer-rooted DID URL identifiers, content-addressed objects, revocation carried by a DID's operation log.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages