Local development environment for SIROS ID wallet ecosystem.
This repository provides a complete local development stack for the SIROS ID wallet platform, including:
- wallet-frontend — Web wallet UI ("SIROS ID (dev)")
- go-wallet-backend — Wallet backend API (Go)
- go-trust — AuthZEN-compliant Trust PDP (allow / whitelist / deny modes)
- mock-verifier — OpenID4VP verifier mock
- mock-trust-pdp — Legacy AuthZEN PDP mock
- vctm-registry — Verifiable Credential Type Metadata registry (served by
go-wallet-backend itself in
--mode=all, not a separate container) - vc-issuer / vc-verifier / vc-apigw / vc-registry / mini-oidc — Production-like VC services (optional)
- facetec-api — FaceTec SDK ↔ vc-issuer bridge (optional,
FACETEC=yes)
Bootstrap a complete development environment with a single command:
curl -fsSL https://raw.githubusercontent.com/sirosfoundation/sirosid-dev/main/install.sh | bash
cd sirosid-dev
make upOr if you already have the repos cloned:
make up # Start default stack (go-trust allow-all)
make up VC=yes # … with production-like VC services
make up GOLDEN=yes # … using pre-built golden release images
make plan VC=yes # Show what `make up` would do, without starting anything
make status # Check all service health
make logs # View Docker logs
make down # Stop everything (Mongo data is kept - see "Storage")
make update # Force-update all repos to upstream
make help # Full option referenceThe option matrix behind make up/make fly-up has grown large. The boot
manager is a terminal UI over it, for finding your way rather than
remembering flags:
make setup # clones the sibling repos, installs the boot manager into .venv, launches it
make boot # afterwardsIt lists every environment in one place (the unnamed local stack, every
environments/<name>.yaml, every sirosid-<env>-* deployment found on
Fly), shows what booting one means before you do it (compose files,
storage, pre-flight checks), edits an environment's options as a form with
the help text next to each field, boots/stops/redeploys, tails logs, runs a
doctor over the known gotchas, and has a Storage panel with the same
"Clear all data" the dashboard has. Every action is a make command it
shows before running, so nothing it does is out of reach of the shell.
scripts/stack.py is what makes this possible: the option matrix as data
(make plan prints it), with tests/test_stack_parity.py asserting it
agrees with the Makefile's own COMPOSE_FILES logic.
Mongo data lives on named volumes that survive make down:
| volume | holds |
|---|---|
sirosid-mongodb-data |
wallet-backend (PDP=helm only - every other mode is an in-memory store) and the VC services |
sirosid-conformance-mongodb-data |
the OpenID conformance suite's own database |
sirosid-r2ps-softhsm-tokens, sirosid-attest-softhsm-tokens |
SoftHSM2 tokens (R2PS=yes) |
make storage-status # every store: mode, size, whether it persists
make storage-clear # wipe + re-register issuer/verifier (YES=yes skips the prompt)
make clean # the old way: containers, volumes and build cache, all goneClearing goes through env-admin (env-admin/, a small service every
make up starts; also sirosid-<env>-env-admin on Fly): it stops the
Mongo consumers, drops the application databases, starts them again so
they recreate their indexes and seed data, and re-registers the issuer and
verifier with wallet-backend (scripts/bootstrap.py, the same code make up and make fly-up run). The dashboard's Storage card is the same
action with a button, on both targets. When the stack is down there is no
env-admin, so make storage-clear removes the volumes instead.
The button needs the environment's admin token plus the environment name
typed as confirmation. A loopback-only local stack embeds the token in the
gitignored build-info.json so the prompt is skipped; under TUNNELS=yes
or DOMAIN= the dashboard is reachable from other machines, so the token is
not embedded and the card prompts for it, as it always does on Fly.
On Fly the same data sits on a Fly volume per Mongo app - see "Fly.io
Deployment" for KEEP_DATA=yes and make fly-storage-clear.
- Docker and Docker Compose
helm(CLI only, no cluster needed) — required forPDP=helmandmake fly-up; both render config viahelm templateagainst the in-repochart/- The following sibling repositories cloned alongside
sirosid-dev(not needed when usingGOLDEN=yeswith pre-built images):siros.org/ ├── sirosid-dev/ # this repo ├── wallet-frontend/ # web wallet UI ├── go-wallet-backend/ # wallet backend (Go) ├── go-trust/ # trust PDP ├── wallet-common/ # shared TypeScript types ├── vc/ # VC services (optional, for VC=yes) └── facetec-api/ # FaceTec SDK bridge (optional, for FACETEC=yes)
The install.sh script clones all of these automatically. Alternatively,
run make setup from an existing checkout to verify prerequisites.
A single make up command drives all configurations via parameters.
For the full list of options, targets, and usage patterns, run:
make helpThis shows all available parameters (PDP, AS_RULES, VC, TRANSPORT, CONFORMANCE, R2PS, DOMAIN, GOLDEN, TUNNELS, FACETEC, ANDROID_APPS, WALLET_NAME, APP_PACKAGE), interaction rules, and complete examples.
make up # Default stack
make up VC=yes # Add VC services
make up TRANSPORT=wmp # Use WMP transport
make up CONFORMANCE=yes # OpenID conformance suite
make up DOMAIN=<host>.local VC=yes # Custom domain for mobile
make up GOLDEN=yes # Pre-built imagesFor more examples and detailed explanations, see make help.
The DOMAIN= option replaces all localhost references in service URLs
with a custom hostname, enabling access from mobile devices or other
machines on the local network.
# Using a local hostname (requires DNS/mDNS or /etc/hosts on the device)
make up DOMAIN=myhost.local VC=yesThe domain must resolve to the host machine's IP from the testing device
(via /etc/hosts, mDNS, or local DNS).
For testing with real TLS certificates and publicly reachable URLs (e.g.
for mobile devices not on the same network, or when TLS is required),
use Cloudflare quick tunnels. No Cloudflare account is needed — temporary
*.trycloudflare.com domains are assigned automatically.
# 1. Start the stack with tunnel support
make up TUNNELS=yes VC=yes
# 2. Open the frontend tunnel URL on any device
# (shown in the output of 'make up')
# Check tunnel status
make tunnel-status
# Stop tunnels and remove the public URLs
make tunnel-stop
make up VC=yes # restart with localhostAndroid passkeys + tunnels: make up TUNNELS=yes automatically creates or
reuses .env.tunnel, injects the tunnel URLs into the stack, and re-runs
make android-setup after startup so Android testing remains aligned with the
current environment. See Android SDK Testing for the
full workflow.
Prerequisites: cloudflared must be installed:
# Linux
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
-o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared
# macOS
brew install cloudflaredHow it works: make up TUNNELS=yes ensures three cloudflared quick tunnel
processes exist (frontend:3000, backend:8080, engine:8082). Each gets a unique
https://<random>.trycloudflare.com URL with a valid TLS certificate. The
tunnel overlay reconfigures the frontend and backend containers to use these
URLs, injects the correct WebAuthn RP ID (hostname only — no https://
scheme), and updates WALLET_SERVER_RP_ORIGINS with the current APK key hash
from .env.android.
Lifecycle note: the tunnels are host processes, not Docker containers.
make down stops the stack but leaves the tunnels running so the URLs can be
reused. Use make tunnel-stop when you want to tear them down.
The Android SDK sample app (siros-sdk-kotlin) or native wrapper apps (TypeScript/JavaScript wrappers)
can be tested against the local dev environment using a physical Android device or emulator.
APP_PACKAGE specifies the app's package name for both SDK-based and wrapper implementations.
# Connect your Android device via USB (or start an emulator), then:
make android-setup
# For SDK sample app (siros-sdk-kotlin):
make android-setup APP_PACKAGE=org.siros.sdk.sample
# For native wrapper apps:
make android-setup APP_PACKAGE=com.example.mywalletappThis does four things automatically:
- Extracts the SHA256 fingerprint from
~/.android/debug.keystore - Computes the base64url APK key hash and writes it to
.env.android - Generates
.well-known/assetlinks.jsonserved by the frontend nginx - Enables
DEVELOPMENT_PASSKEY_REGISTRATIONon the connected device via ADB (required becausetrycloudflare.comsubdomains are not in Google's DAL cache)
After make android-setup, both make up and make up TUNNELS=yes
automatically source .env.android so WALLET_SERVER_RP_ORIGINS always
includes the correct APK key hash for your keystore.
No device connected? Steps 1–3 still run; only the ADB step is skipped.
You can re-run make android-setup later when a device is available.
If the device is on the same Wi-Fi as the dev machine, use DOMAIN=:
make android-setup
make up DOMAIN=<hostname>.local VC=yesThe device must be able to resolve <hostname>.local (mDNS or /etc/hosts).
Point the SDK sample app at http://<hostname>.local:8090 (via the wallet-proxy).
Passkeys on physical devices require HTTPS. Cloudflare quick tunnels provide real TLS without any account:
# 1. Generate assetlinks.json and configure ADB
make android-setup
# 2. Start the stack with public TLS URLs
make up TUNNELS=yes [VC=yes]
# 3. Point the sample app at the backend tunnel URL shown in the outputmake up TUNNELS=yes re-runs make android-setup automatically. This keeps
the generated Android config current and re-applies any device-side passkey
setup that may be needed for physical-device testing.
| Symptom | Most likely cause | Fix |
|---|---|---|
| "RP ID cannot be validated" | DEVELOPMENT_PASSKEY_REGISTRATION not set |
Connect device and run make android-setup |
| "Origin not allowed" | APK key hash mismatch | Delete .env.android and re-run make android-setup with the correct keystore |
| Passkey creation works locally but fails with tunnels | Wrong RP ID (scheme included) | Fixed in current code — ensure you are on latest main |
| Sample app can't reach backend | Tunnel URL not updated in app | Use the backend tunnel URL from the make up TUNNELS=yes or make tunnel-status output |
Manual ADB fallback (if make android-setup can't set the flag automatically):
adb shell am compat enable DEVELOPMENT_PASSKEY_REGISTRATION org.siros.sdk.sample| File | Description |
|---|---|
.env.android |
Generated by make android-setup; contains APK_KEY_HASH and ANDROID_PACKAGE; sourced by make up |
.well-known/assetlinks.json |
Served by wallet-frontend nginx; required for Android passkey RP ID validation |
scripts/setup-android.sh |
Script behind make android-setup |
docker-compose.tunnel.yml |
Injects TUNNEL_FRONTEND_URL, TUNNEL_RPID (hostname only), APK_KEY_HASH |
| Mode | Description |
|---|---|
PDP=allow (default) |
go-trust allow-all — every entity is trusted |
PDP=whitelist |
go-trust whitelist — only entities in fixtures/vc-go-trust-whitelist.yaml are trusted |
PDP=deny |
go-trust deny-all — rejects everything (negative testing) |
PDP=mock |
Legacy mock-trust-pdp (no go-trust) |
PDP=helm |
go-trust whitelist + wallet-backend, both configured from config files rendered off the in-repo chart/ (forked from siros-id-stack) (see scripts/render-helm-config.py) instead of hand-maintained env vars/flags. |
Under CIR (EU) 2025/848 issuers and verifiers alike are registered wallet-relying parties: each holds an access certificate (WRPAC) saying who it is and a registration certificate (WRPRC) saying what it registered for. The wallet checks the WRPRC before requesting issuance (ARF v3.0.0 §6.6.2.3) or releasing attributes.
create-pki.sh cannot produce these — a WRPRC is a signed JWT against a
register, not something openssl can mint — so the material comes from
siros-wrpac-tool:
# v0.2.0 or later: the tsl command arrived there
git clone git@github.com:sirosfoundation/siros-wrpac-tool.git ../siros-wrpac-tool
make -C ../siros-wrpac-tool build
make wrpac-pki
docker compose -f docker-compose.test.yml -f docker-compose.wrpac.yml up -dThis mints a WRPAC and WRPRC for the local vc-issuer and vc-verifier from
the specs in fixtures/wrpac-clients/, and publishes the deployment's trust
anchors in both list formats — an ETSI TS 119 602 LoTE and an ETSI TS 119
612 TSL — because go-trust reads either and a real deployment has to support
both. go-trust-wrpac on port 9099 is configured with both registries.
| File | Description |
|---|---|
fixtures/wrpac-clients/*.yaml |
Per-party registration specs; the filename stem is the client id |
fixtures/wrpac-pki/ |
The deployment: CA key, register, CRL number, status list indices. Gitignored and must persist — reusing a status index would transfer a previous holder's revocation to a new certificate |
fixtures/go-trust-wrpac.yaml |
go-trust with both the lote and etsi registries pointed at the published anchors |
Each client keeps its own key: only a CSR reaches the deployment, which certifies the public key and never holds the private half.
AS_RULES= selects the SPOCP policy wallet-backend's built-in Authorization
Server (passkey login + token endpoint) evaluates token requests against —
independent of the PDP= trust policy above, which governs issuer/verifier
trust rather than AS token issuance.
| Mode | Description |
|---|---|
AS_RULES=allow-all (default) |
Unconditional allow (fixtures/as-rules/allow-all.rules) — make up's job is to give every other feature a working AS out of the box, not to exercise the AS ruleset itself. |
AS_RULES=baseline |
go-wallet-backend's own real baseline policy (rules/default.rules + rules/delegation.rules, the same rules baked into every image and used by make fly-up, which never overrides WALLET_AS_RULES_DIR). Use this only when directly testing AS rule behavior — e.g. reproducing a Fly-only 403 locally, or checking a new client request shape against the real policy — see docker-compose.as-rules-baseline.yml. |
When started with VC=yes, the environment includes production-like VC
services built from the ../vc source repository. On startup, the issuer
and verifier are automatically registered with the wallet backend via the
admin API — no manual registration needed.
Available credentials: PID (ARF 1.5 + 1.8), EHIC, Diploma, SIROS ID, mDL and PID as mdoc, and the EU Business Wallet attestations below.
Most types are datastore-sourced: the credential is built from a document
in fixtures/vc-bootstrapping/<scope>.json, one file per scope, keyed by
mini-oidc user id. (pid_1_5 and ehic are the exceptions - they are
assertion-sourced, built from the mini-oidc claims at issuance time, and have
no file.) Three rules hold across that directory, each with a test in
tests/test_fixture_integrity.py behind it:
The person in the document is the person who logs in. identity_mappings. json is the bridge: PID-authenticated issuance resolves the holder by
matching the presented PID's given_name/family_name/birth_date against
it, so a document whose name or birth date drifts from mini-oidc's
users.yaml cannot be issued at all - and the error is "no documents", which
reads like a missing fixture rather than a mismatched one.
Every claim a document carries is declared by that type's VCTM
(fixtures/vc-metadata/vctm_<scope>.json) or MDDL schema
(<scope>.mdoc.json). Issuing an undeclared claim is silently dropped or
rejected depending on the issuer.
Each scope has a minimal document and exactly one maximal one. The maximal
document's meta.document_id ends in -full (e.g.
document_id_pid_1_8_carol-003-full) and covers the type's mandatory and
every optional claim; the others cover the mandatory claims only, and are the
typical case. One holder owns the -full document per scope - a holder with
two documents of the same scope would make issuance ambiguous, since nothing
picks between them.
| scope | minimal | -full |
|---|---|---|
pid_1_8 |
alice-001, bob-002, erik-010, maria-011, jan-012, sophie-013 | carol-003 |
mdl |
alice-001, bob-002 | carol-003 |
siros_id |
alice-001, bob-002 | carol-003 |
diploma |
alice-001, bob-002 | carol-003 |
pid_mdoc |
alice-001 | bob-002 |
mdl_zk4 |
alice-001 | bob-002 |
photoid |
alice-001, bob-002 | carol-003 |
<type>_offer for every row above |
as their source | as their source (pre-authorized twins for make datastore-offer; copies, kept equal by a test). Only in environments that do NOT set issuer.allowPreauthOffer (gdc and nymauth do not have them: SUNET/vc#840) |
ebw_oid |
erik-010, maria-011 | sophie-013 |
eucc |
erik-010, maria-011, jan-012 | sophie-013 |
eu_poa |
jan-012 | sophie-013 |
iban_ov |
erik-010, maria-011, jan-012 | sophie-013 |
nym_auth |
alice-001, bob-002 | carol-003 |
Derived claims (age_in_years, age_birth_year, age_over_18/21/65,
age_equal_or_over.*) are recomputed from the birth date at test time, so
a value that has fallen behind fails the suite instead of issuing a wrong
credential. When one does, update the fixture - that is the test working.
mdl_zk4's -full document is also the one to use for Vega ZK testing: it is
the only one that fills all four of the circuit's fixed attribute slots.
Four WE BUILD rulebook types for testing an EU Business Wallet, declared in
values-base.yaml and served from VCTMs copied out of
registry.siros.org (webuild-consortium/):
| scope | vct | what |
|---|---|---|
ebw_oid |
uri:eu.ebw.oid.1 |
Owner Identification Data (ds001) - which legal person owns the wallet |
eucc |
urn:eudi:eucc:1 |
EU Company Certificate (ds004) - the business-register extract |
eu_poa |
uri:eu.eudi.eu-poa.1 |
EU Power of Attorney (ds007) - principal grants an attorney powers |
iban_ov |
eu.we-build.iban-ov.1 |
IBAN Ownership Verification (rb-iban-ov) - the company's bank account |
All four require PID authentication, as they would in a live setting.
The wallet presents a PID (OpenID4VP during issuance) and the identity in it
selects the person's documents from the datastore
(fixtures/vc-bootstrapping/{ebw_oid,eucc,eu_poa,iban_ov}.json, mapped through
identity_mappings.json). Get a PID first as a company user (pid_1_5
works for every mini-oidc user), then request the attestation; the wallet asks
you to present the PID and the document for that person is issued. The
natural persons (alice, bob, carol) have no such documents, so their PID leads
to "no documents". The same synthetic data also ships in mini-oidc 0.0.5's
users.yaml (attestations:), for stacks that prefer OIDC-asserted issuance.
vc-apigw imports these documents only into an empty datastore, so an
environment that already holds data does not pick up a new type on redeploy.
Add the documents to it without a wipe with
make datastore-upload FILE=fixtures/vc-bootstrapping/<scope>.json [ENV=<name>]
(make datastore-search SCOPE=<scope> to check). The admin API behind those
targets takes a Bearer token signed with a per-environment key the render step
generates; see scripts/api_auth.py.
| mini-oidc user | company | EBW-OID | EUCC | EU PoA | IBAN-OV |
|---|---|---|---|---|---|
| erik-010 | Nordic Tech Solutions AB (SE), sole representative | yes | yes | SEB account | |
| maria-011 | Grünberg Consulting GmbH (DE), joint representative | yes | yes | Deutsche Bank account | |
| jan-012 | Grünberg Consulting GmbH employee | yes | attorney, principal Maria | Deutsche Bank account | |
| sophie-013 | Transport Dubois SARL (FR), sole manager | yes | yes | attorney, principal co-founder | BNP Paribas account |
Every company user also has a PID (pid_1_8), which they need before any of
these can be issued: the attestations above are PID-authenticated, so a user
without one cannot start the flow at all.
The verifier has matching presentation-request templates in
fixtures/vc-presentation-requests/eu_business_wallet.yaml.
Testing nym_auth end to end (issuance with seed delivery, then a zero-knowledge pseudonym / age presentation) is a step-by-step walkthrough for testers plus an operator and developer appendix in
docs/NYMAUTH-TESTING.md; thenymauthFly environment (environments/nymauth.yaml, derived from gdc) is the known starting point for it.
nym_auth is the natural-person attestation of the
Nym-authentication rulebook
(v0.3): an mso_mdoc whose doctype and namespace are both
eu.europa.ec.eudi.nym_auth.1, carrying birth_date (full-date, tag 1004),
ssc - SHA-256 of a 16-byte secret_seed the issuer and the prover share -
and optionally psc (SHA-256 of a prover-only seed). It exists for
zkp_req presentations (called circuit_zk before rulebook v0.4): pseudonyms and age predicates proven in zero
knowledge, never disclosed. fixtures/vc-metadata/nym_auth.mdoc.json is a
byte-for-byte copy of the rulebook's schema until registry.siros.org
publishes it (mdoc types stay vendored even with REGISTRY=external).
Who can get it: alice-001, bob-002 and carol-003
(fixtures/vc-bootstrapping/nym_auth.json). How: request nym_auth in the wallet; the issuer
asks, during OpenID4VCI, for any one of a PID (pid_1_8 or pid_1_5, SD-JWT),
a pid_mdoc, an mdl or a photoid, and the person in it selects the
document through identity_mappings.json. Get one of those first.
Birth date. The issued birth_date is meant to be copied from the
presented credential (normalised to YYYY-MM-DD: an SD-JWT PID's
birthdate, an mdoc's tag-1004 birth_date). vc main cannot copy a presented
claim into a datastore-sourced credential, so values-base.yaml also renders
auth_scopes.<x>.document_claims (and identity_attributes, which lets the
pid_mdoc/photoid options resolve a person) for a proposed vc change; vc
main ignores both keys and issues the document's own birth_date, which
tests/test_fixture_integrity.py holds equal to the birth date in every
credential the person can present. With vc main only the pid_1_8, pid_1_5
and mdl options resolve a person; pid_mdoc stays unusable even with the vc
change until our pid_mdoc schema carries a birth_date.
ssc is generated by the issuer (SUNET/vc#793 + #821): issuer.apigw.seeds.primary is a seed
source whose secret is generated once per environment (fixtures/rendered-secrets/vcApigwSeedPrimary, or
the vc-secrets state of a service instance) and is permanent - replacing it repseudonymises everyone,
losing it makes every issued pseudonym unrecoverable. For each person the issuer derives a seed from that
secret and the person's issuing_country and personal_administrative_number (the same two values their
PID carries, stored in nym_auth.json as inputs only: the schema does not declare them, so they are never
issued), commits to it (ssc = SHA-256 of the seed) and, because the type sets deliver: true, hands the
seed to the wallet in the credential response (credential_metadata, namespace
org.siros.nym_auth; the engine requires an encrypted response for that). The stored documents therefore
carry no ssc, and vc refuses a document that does. An offer (the pre-authorized grant: make datastore-offer SCOPE=nym_auth, or the
nym_auth_offer twin where an environment still has twins) derives the same seed for the same person: one person,
one pseudonym, whichever way the credential was obtained.
psc is a PLACEHOLDER for now. It is the commitment to a private_seed the wallet generates (from
its container key); the issuer cannot know it. The -full document (carol-003) carries a fixed one only so
the optional claim is exercised and the plumbing stays in place: psc = SHA-256 of the public test seed
101112131415161718191a1b1c1d1e1f, standard base64 /C4scwcr+ivaA/+TB0ct69PMgQUCioqeI141uo0uN/Q=. Nothing
may depend on it - it commits to a seed no wallet holds - and no smoke test asserts on it. Where it plugs
in: holder-supplied commitments (the known-offender-matching-psc configuration, a commitments entry with
source: holder, SUNET/vc's PR 2 - the issuer refuses it at startup until it has a store for it). The day that
lands, REMOVE the fixed psc from nym_auth*.json: the issuer refuses any document that already carries a value
for a configured commitment claim.
Verifying a pseudonym (gdc config; image comes with vc's verifier release). environments/gdc.yaml
values.verifier.extraConfig carries verifier.nym_auth (SUNET/vc#832): the allowlist entry
known-offender-matching-ssc (circuit ids, Ligero block parameters, artifact size and SHA-256 from
zk-cred-longfellow src/nymauth/profile.rs; do not copy the digest from vc's docs/CONFIGURATION.md, whose
example has 52 zeros in the middle) and preferred_vp_formats.zkp_req (system_id_values, proof_system_values; rulebook v0.4 names). The ~1 MB decompressed artifact cannot
travel as a Fly secret and is not in the vc repo, so dockerfiles/verifier-nymauth.Dockerfile bakes it into the
verifier image at /etc/vc/circuits/nym_auth-ssc.bin, checking size and digest at build time (build input:
fixtures/nym-auth-circuits/nym_auth-ssc.bin, gitignored; the recipe is in the file). The verifier needs the
zknative build and refuses to start without trust.pdp_url: it takes the issuer key from the HOLDER's own
chain, so the PDP is the only check that key belongs to an issuer (here: action mdoc-issuer, the mDOC IACA
registry; a self-signed chain with the same SAN gets decision false). verifier_id is the verifier's client_id exactly as published,
x509_san_dns:<host of public_url> with the prefix (rulebook v0.4; the bare host was the rule before), and scopes every
pseudonym: changing it changes every pseudonym this verifier reports.
Browser → wallet-frontend (3000)
↓
wallet-backend (8080/8081/8082)
↓ (credential discovery)
vc-apigw (9003) ← OAuth2 AS + OpenID4VCI metadata
↓
vc-issuer (9000) ← credential signing
vc-verifier (9001) ← OpenID4VP + OIDC provider
vc-registry (9004) ← status lists, type metadata
mongodb ← persistence
The apigw is the public-facing entry point for OpenID4VCI — it serves
.well-known/openid-credential-issuer metadata and handles token/credential
endpoints. The issuer and registry are internal backend services.
| Service | Port | Description |
|---|---|---|
| wallet-frontend | 3000 | Web wallet UI |
| wallet-backend | 8080 | Backend API |
| wallet-backend admin | 8081 | Admin API |
| wallet-engine | 8082 | Credential engine |
| mock-verifier | 9011 | OpenID4VP verifier mock |
| mock-trust-pdp | 9081 | Trust PDP mock |
| Service | HTTP | gRPC | Description |
|---|---|---|---|
| vc-issuer | 9000 | 9190 | OpenID4VCI credential issuer |
| vc-verifier | 9001 | 9091 | OpenID4VP verifier + OIDC provider |
| vc-apigw | 9003 | — | OAuth2 AS + credential metadata |
| vc-registry | 9004 | 9094 | Status lists and type metadata |
| Instance | Port | Description |
|---|---|---|
| go-trust-allow | 9095 | Allow-all (default PDP) |
| go-trust-whitelist | 9096 | Whitelist mode |
| go-trust-deny | 9097 | Deny-all |
| Service | Port | Description |
|---|---|---|
| r2ps-server | 8443 | R2PS protocol (WSCD/WSCA) |
| r2ps-server admin | 8444 | Admin API (key listing, status lists) |
| r2ps-server (conformance) | 9443 | R2PS when running with conformance suite |
| r2ps-server admin (conformance) | 9444 | Admin when running with conformance suite |
Services are built from sibling directories by default. Override with environment variables or on the command line:
| Variable | Default | Description |
|---|---|---|
FRONTEND_PATH |
../wallet-frontend |
Wallet frontend source |
BACKEND_PATH |
../go-wallet-backend |
Wallet backend source |
VC_PATH |
../vc |
VC services source |
GO_TRUST_PATH |
../go-trust |
go-trust source |
FACETEC_PATH |
../facetec-api |
facetec-api source (FACETEC=yes only) |
CHART_PATH |
chart |
config chart rendered by helm template (in this repo) |
WALLET_NAME |
SIROS ID (dev) |
Wallet display name |
# Example: use a different frontend checkout
make up FRONTEND_PATH=~/other/wallet-frontendThe GOLDEN= option lets you run pre-built container images from a tested
release instead of building from local source. This is useful for quick
demos, reproducing reported issues, or running the stack without cloning
all the source repos.
# Use the default golden release
make up GOLDEN=yes
# Use a specific named release
make up GOLDEN=beta_r2 VC=yesGolden release definitions are maintained in the
siros-conformance
repo (golden-releases.yaml). The Makefile fetches this file on demand
and generates a .env.golden file with the resolved image tags.
Note: When using GOLDEN=yes VC=yes, the wallet and go-trust services
use golden images but VC services are still built from local source. This is
because the VC config rendered from the chart (values-base.yaml into
fixtures/rendered/vc-*.yaml) is written for current source and evolves
between releases, making golden VC images incompatible with it. The golden VC
overlay (docker-compose.golden-vc.yml) is available for future use once
configs are version-aligned.
Images are pulled from ghcr.io/sirosfoundation/* — you may need
docker login ghcr.io if the images are not public.
Force-update all sibling repositories to their default upstream branches:
make updateThis fetches and hard-resets each repo (main for most, release/sirosid
for wallet-frontend and wallet-common).
The OpenID Foundation Conformance Suite validates the wallet's OID4VCI and OID4VP implementations.
# 1. Start conformance environment (auto-configures /etc/hosts)
make up CONFORMANCE=yes
# 2. Install test dependencies
cd ../sirosid-tests && make install
# 3. Run conformance tests
cd ../sirosid-tests && make test-conformance
# Conformance UI: https://localhost.emobix.co.uk:8443/The wallet targets the Decentralized Identity Interop Profile v5. See DIIP-V5.md for the requirement-by-requirement matrix and how to exercise each part locally.
The wallet defaults to the HAIP profile, which binds the Holder's raw key — the proof
shape every SIROS issuer accepts today. DIIP is opt-in, so this stack sets
INTEROP_PROFILE=diip in docker-compose.test.yml and holder keys are did:jwk; drop that
line to get the default back. DID_KEY_VERSION then selects which did:key flavour a HAIP
wallet mints, and is ignored under DIIP.
An Issuer that publishes cryptographic_binding_methods_supported on a credential
configuration overrides both — vc advertises ["jwk"] unconfigured and
["did:jwk", "jwk"] once configured — so negotiation, not the local setting, is normally what
decides. That path needs the engine to forward the field first; see
DIIP-V5.md.
DIIP mandates the IETF Token Status List for revocation, and vc-registry publishes real ones
under make up VC=yes — no fixture needed. It implements the draft directly, signing tokens
with the registry.token_status_lists.key_config keys rendered into
fixtures/rendered/vc-registry.yaml (from values-base.yaml), and vc-apigw allocates each
issued credential a section/index so its status.status_list.uri already points at a live
list:
# Aggregation of all Status List Token URIs (draft §9.3)
curl http://localhost:9004/statuslists
# An individual Status List Token (draft §8.1), as application/statuslist+jwt
curl http://localhost:9004/statuslists/0Both application/statuslist+jwt and application/statuslist+cwt are supported. To flip a
credential to revoked or suspended, use the registry's admin GUI at
http://localhost:9004/admin/login (admin, with the password from
fixtures/rendered-secrets/vcAdminGuiPassword — generated per checkout, no longer a fixed
literal).
The tokens' iss/sub — and the status.status_list.uri written into issued credentials —
come from registry.public_url, which is the registry's own host-reachable address
(http://vc-registry.localhost:9004), so a plain make up VC=yes is enough to fetch a
credential's status list from the host browser. This used to be https://vc-proxy:8445, a host
only defined in docker-compose.conformance.yml, which made those URIs unresolvable outside
CONFORMANCE=yes; see fixtures/vc-config-golden/accepted-diffs.yaml for the correction. The
wallet still treats an unfetchable status list as a warning rather than a failure (see
DIIP-V5.md).
An advanced, currently deprioritized WSCD option: a remote HSM-backed signing service (SoftHSM2 + PAKE-authenticated protocol), as an alternative to the default on-device keystore.
make up R2PS=yes VC=yes
make r2ps-setup # verify health + list provisioned keysSee R2PS.md for the key-provisioning protocol, admin API cookbook, HSM parameters, and Android SDK plugin configuration.
Spin up a full, independently addressable wallet stack (frontend, wallet-proxy,
backend, PDP, issuer, verifier, apigw, registry, mongo, mini-oidc) on Fly.io
under the shared sirosfoundation org - each named environment gets its own set of
sirosid-<env>-* apps and *.fly.dev URLs, fully isolated from every other
environment. Config is rendered from the in-repo chart/ (same
mechanism as PDP=helm, see below) and images are pulled straight from that
chart's values.yaml - no local Docker build.
make fly-up ENV=alice # deploy a new environment
make fly-status ENV=alice # check all 11 apps
make fly-down ENV=alice # tear it down (deletes the Mongo data too)
make fly-down ENV=alice KEEP_DATA=yes # tear down but keep the Mongo apps + volumes, machines stopped
make fly-storage-clear ENV=alice # wipe the data through env-admin (or the volumes, if down)Mongo data persists on a Fly volume (one per Mongo app, created by fly-up
if missing), so a redeploy, an image bump or a Fly host restart no longer
empties the environment. Two consequences: the Mongo root password is now
cached per environment in fixtures/rendered/fly-<env>/ and recovered from
the running machine over fly ssh console when that cache is missing
(another developer redeploying), and a volume pins its app to a region -
fly-up refuses to move an environment with data rather than silently
orphaning it. The dashboard's Storage card and make fly-storage-clear go
through the environment's env-admin app, which restarts the Mongo
consumers with one app-scoped Fly deploy token per consumer app.
Requires flyctl installed and authenticated, and the in-repo chart/
checkout (make setup clones it).
Environments are fully isolated by name - two developers can run
make fly-up ENV=alice and make fly-up ENV=bob at the same time with zero
collision (verified with two concurrent full deploys).
Two different override mechanisms exist, for two different purposes - they're not redundant, each solves a problem the other can't:
-
IMAGES=(ad-hoc, per-environment, opt-in) - override any one of the 10 components' image for your environment only, e.g. to test your own branch build:make fly-up ENV=alice IMAGES="wallet-backend=ghcr.io/sirosfoundation/go-wallet-backend:pr-123" # comma-separate multiple: IMAGES="wallet-backend=...,pdp=..."
This never touches any checked-in file - it only affects the one environment you passed it to.
Testing a local build, with no registry to push to by hand: if the value is a bare, unqualified tag (no
/) that's already present in your local Docker daemon - exactly whatmake up REBUILD=yes/ plaindocker-compose buildalready produce (wallet-backend-e2e-test:local,vc-issuer-e2e-test:local, etc.) -fly-up.pypushes it into that environment's ownregistry.fly.io/sirosid-<env>-<component>namespace for you and deploys from there:make fly-up ENV=alice IMAGES="wallet-backend=wallet-backend-e2e-test:local"No manual
docker tag/docker push/flyctl auth dockerneeded - it reuses your existingflyctl auth loginsession. A fully-qualified ref (anything with a/, e.g.ghcr.io/sirosfoundation/...) is always passed through untouched, even if that exact tag also happens to be cached locally, so this never reinterprets an intentionally-remote value.Images pushed this way live in that Fly app's own registry namespace, so they're torn down for free with the environment itself (
make fly-down ENV=alice) - no separate cleanup/expiry job needed. -
values-fly.yaml'simages:block (shared default, checked in) - pins several components (currentlyimages.pdp,images.walletBackend, and the fourvc.*image keys) ahead ofsiros-id-stack's own, slower-moving pins, each because this repo needs a fix or feature the chart hasn't caught up to yet - e.g. theimages.pdpoverride exists because the chart's own pin predates a fix (sirosfoundation/go-trust#112) the PDP's whitelist needs to function at all once deployed on Fly (without it, JWKS fetch fails for every whitelisted issuer/verifier and the whitelist never becomes healthy). This is deliberately not handled by asking every developer to rememberIMAGES=pdp=...on everymake fly-up- a broken default should be fixed at the default, not worked around per-invocation.Every entry in that file's
images:block carries its own comment naming the exact upstream condition under which it should be deleted (e.g. "once siros-id-stack bumps images.pdp past vX.Y.Z") - readvalues-fly.yamldirectly for the current pinned versions and per-override rationale rather than trusting a version number here; this prose already drifted out of sync with the file once and will again.
wallet-backend's server-side WebAuthn accept list (server.rp_origins) must
carry an android:apk-key-hash:... entry for every Android app/signing-key
pair you want to test passkeys with - registering a package in
assetlinks.json alone passes Android's OS-level Digital Asset Links check
but still fails the actual WebAuthn ceremony if rp_origins doesn't also
have it (see scripts/android_apps.py's docstring).
Copy .android-apps.example to .android-apps (gitignored, per-developer/
per-checkout) and list every package=fingerprint pair you want trusted -
several debug builds and/or Play Store upload keys at once:
com.example.debug=AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99
com.example=11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00
This one file is read by both targets, regardless of which one you use:
make up # default local docker-compose
make up PDP=helm # helm-rendered local config
make fly-up ENV=alice # Fly.ioANDROID_APPS=pkg=fingerprint,... (comma-separated, repeatable-by-comma) adds
one-off entries on the command line without touching .android-apps, and
.env.android (from make android-setup) is still honored on top of both -
none of these replace each other, they all merge. See
scripts/android_apps.py for the exact precedence and the hex/base64url
conversion every consumer needs.
sirosid-dev/
├── install.sh # Bootstrap script (curl | bash)
├── Makefile # Single entry point for all operations
├── docker-compose.test.yml # Primary dev environment (always included)
├── docker-compose.*.yml # Optional overlays - one per make up flag
│ # (go-trust modes, VC, R2PS, conformance,
│ # Android/USB, tunnel, domain, golden
│ # images, transports, FaceTec, helm-config)
│ # -- run `make help` for which flag adds
│ # which overlay; there are ~30 of these
├── nginx-e2e.conf # Frontend nginx config (dashboard + health + env-admin proxies)
├── startup.html, dashboard/ # The local dashboard, and the Storage card both dashboards share
├── values-dev.yaml / values-fly.yaml # Helm values overlays for render-helm-config.py
├── environments/ # Persisted per-environment config (Fly + local: block)
├── env-admin/ # The in-environment storage-reset service (local + Fly)
├── bootmgr/ # The boot manager TUI (installed by make setup, run by make boot)
├── tests/ # unittest suites: stack parity, env-admin, Fly storage
├── fixtures/ # VC/PDP config templates, PKI, presentation requests
├── mocks/ # mock-verifier (OpenID4VP) + trust-pdp (legacy AuthZEN) mocks
└── scripts/ # All Makefile-invoked automation (Fly deploy, Android/USB
# setup, helm config rendering, tunnels, PKI) - see each
# script's own header/--help; setup-android.sh is the
# actively-used Android setup path, generate-assetlinks.sh
# is its (unused) legacy predecessor
# Check service health
make status # Core services
make status-vc # VC services (when VC=yes)
# View logs for a specific service
docker logs wallet-backend-e2e-test
docker logs vc-apigw-e2e
# Rebuild from clean state
make clean # Remove all containers and volumes
make up VC=yes # Rebuild and start
# Regenerate PKI (signing keys and certificates)
make pki# Start dev environment
cd sirosid-dev && make up
# Run tests (in another terminal)
cd sirosid-tests && make test
# With conformance suite
cd sirosid-dev && make up CONFORMANCE=yes
cd sirosid-tests && make test-conformance- ANDROID-TESTING.md — Android SDK / Waydroid / USB device testing deep dive
- R2PS.md — Remote PAKE-Protected Signing deep dive
- Local Development Environment — full setup guide on the developer docs site
- sirosid-tests — E2E test suites
- go-wallet-backend — Wallet backend
- wallet-frontend — Web wallet UI
- go-trust — Trust PDP
- SUNET/vc — VC services (issuer, verifier, registry)
- siros-id-stack — Origin of
chart/(production Helm chart; ported from by hand) (PDP=helm/make fly-up)