diff --git a/README.md b/README.md index 5421ef0..d0d03a8 100644 --- a/README.md +++ b/README.md @@ -464,6 +464,11 @@ The verifier has matching presentation-request templates in `fixtures/vc-presentation-requests/eu_business_wallet.yaml`. ### nym_auth (Nym-authentication attestation) +> **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`](docs/NYMAUTH-TESTING.md); the `nymauth` Fly 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](https://github.com/sirosfoundation/nym_auth_rulebook) diff --git a/docs/NYMAUTH-TESTING.md b/docs/NYMAUTH-TESTING.md new file mode 100644 index 0000000..bfdd23d --- /dev/null +++ b/docs/NYMAUTH-TESTING.md @@ -0,0 +1,186 @@ +# Testing nym_auth: the `nymauth` environment + +`environments/nymauth.yaml` is a **known starting point** for testing the nym_auth pseudonym flow end to end: +the issuer gives a wallet a `nym_auth` credential (and delivers its secret seed), and a verifier asks the wallet for a +**zero-knowledge pseudonym** (and optionally an age range) without seeing any attribute. It is the `gdc` stack reduced to +what that needs; `diff environments/gdc.yaml environments/nymauth.yaml` shows exactly what is nymauth-specific. + +**Status.** The flow below was proven live on `gdc` (a Pixel, the Kotlin sample app): issuance with seed delivery and +notification; same-device presentation of both presets; cross-device presentation by scanning the verifier page's QR in a +browser. **Nobody has deployed `ENV=nymauth` yet**; it reproduces the same pins on its own hostnames. + +> **Every pinned image except the PDP and the frontend is a local build of UNMERGED pull requests.** Re-pin them to +> released images when the PRs land. What went into each build (tags, digests, base commits, PR heads) is the +> `PIN PROVENANCE` comment block at the top of `images:` in `environments/gdc.yaml`. + +Who does what: **Operator** steps need Fly access and a checkout of this repo. **Tester** steps need only the phone, the +sample app and a browser. + +--- + +## Part 1 - Operator: deploy the environment and make a credential offer + +### Prerequisites +- `flyctl` logged in to the Fly org (`flyctl auth whoami`), `helm`, `docker`, `python3` with PyYAML, and a checkout of + this repo **on the machine that deploys**: per-environment state (PKI, secrets, the seed secret) lives in the + gitignored `fixtures/rendered/fly-nymauth/` and must be kept; a deploy from another checkout generates new ones. +- The images are pinned to gdc's registry namespace (`registry.fly.io/sirosid-gdc-*`); the deploy pulls them, so the Fly + account must be able to read gdc's images. **Not yet exercised.** If it cannot, push the same images to the new apps' + registries after the first `fly-up` created the apps, and repin in `environments/nymauth.yaml`. + +### Deploy +```sh +flyctl auth docker # registry login (expires in minutes: run it right before) +make fly-up ENV=nymauth +make datastore-sync ENV=nymauth # puts the offer twins' documents into the datastore (once, and after a storage clear) +``` +`make fly-up ENV=nymauth` would create, in the org's `arn` region: **11 apps** - `sirosid-nymauth-` + `mongodb`, `mini-oidc`, +`pdp`, `vc-registry`, `vc-issuer`, `vc-apigw`, `vc-verifier`, `wallet-backend`, `wallet-proxy`, `wallet-frontend`, +`env-admin` - with a volume for MongoDB, the secrets (signing key, JWT/admin secrets, the permanent seed secret and the +credential-encryption key), deploy tokens for env-admin, and public `*.fly.dev` hostnames. It generates the environment's +own PKI; the signing certificate carries the verifier's host (`signingCertDnsSan`). `make fly-down ENV=nymauth` removes it. +Do not run these unless you mean to create those resources. + +### What lives where +| | URL | +|---|---| +| verifier page (operator UI) | `https://sirosid-nymauth-vc-verifier.fly.dev/` | +| issuer | `https://sirosid-nymauth-vc-apigw.fly.dev` | +| wallet backend / engine (what the sample app uses as backend URL) | `https://sirosid-nymauth-wallet-proxy.fly.dev` | +| PDP (internal only) | `http://sirosid-nymauth-pdp.internal:8080` | + +### Mint an offer for alice-001 +```sh +make datastore-offer SCOPE=nym_auth DOCUMENT_ID=document_id_nym_auth_offer_alice-001 ENV=nymauth +``` +It prints an `openid-credential-offer://...` URL (add `QR=yes` for a terminal QR). **The code is valid for 5 minutes and +single use**: mint it when the phone is in your hand. `nym_auth_offer` is the pre-authorized twin of `nym_auth`; once the +twins migration lands (PRs for SUNET/vc#840, see sirosid-dev PR list) the same command mints against `nym_auth` itself. + +--- + +## Part 2 - Tester: what you see and do + +Use the Kotlin sample app (`org.siros.sdk.sample`). Its signing keys are in `android_apps` of the environment file, so +sign-up works against this environment. + +### 1. Sign in +Open the app, point it at the environment's wallet backend (`https://sirosid-nymauth-wallet-proxy.fly.dev`), create or +use a passkey, and sign in. The **Credentials** tab shows **Welcome, **. + +### 2. Allow zkp_req requests (once) +**Settings** tab -> **Request policy**: +- switch **zkp_req requests** ON (it is OFF by default); +- leave **FAKE prover (no real proof)** OFF - the verifier cannot accept a fake proof; +- the section says *"Takes effect the next time the wallet is created: after Disconnect, or an app restart."* Do that now + (Disconnect and reconnect, or restart the app). **A wallet created before you switched this on does not answer + zkp_req requests**, and has to be recreated. + +### 3. Get the credential +The operator gives you an offer link or QR code (Part 1). Either open the link on the phone, or on **Home** tap +**Add a credential**, scan with **Scan QR Code**, or paste the link under **Or paste a credential offer URI** and tap +**Redeem Offer**. The wallet shows progress ("Reading credential offer...", "Issuer trust verified", "Requesting +credential..."), then the credential appears under **Credentials** as **Nym authentication**. The secret seed is stored +silently and never shown. + +### 4. Present it to the verifier +On a computer (or the phone's browser), open `https://sirosid-nymauth-vc-verifier.fly.dev/`. Under **Preconfigured +presentation requests** open **Show more presets** if needed; the presets are under **ZK Proofs**: +- **nym_auth_pseudonym** - prove a pseudonym only; +- **nym_auth_pseudonym_age** - also prove the holder's birth date is in a range (1980-01-01 to 2000-12-31, which + alice-001, born 1990-01-15, satisfies). + +Click one: a **QR code** appears with the wallet link under it. On the phone, open **Home** and use the wallet's QR scanner +(**Scan QR Code**) on the screen, or, on the same device, open the link with the wallet. + +### 5. Consent in the wallet +The wallet shows **Credential Request**: the verifier's name, the credential **Nym authentication** and **0 data fields +requested** (nothing is disclosed: zero knowledge). **Next** -> **Select which data to share** (nothing to select) -> +**Next** -> **Ready to share** -> **Share**. Then **Processing...** / **Sending response to verifier...** and back to Home. + +### 6. Check the result on the verifier page +The page shows **Verification succeeded** and a table **Credential claims - nym_auth** with columns **Name** and **Value**: +the **pseudonym** (`nym`), and for the age preset the **age** result, with the proof system and time. **Present again to the +same verifier: the pseudonym is the same** (it is scoped to this verifier). A different verifier gives a different one. + +### 7. What failure looks like +A **Flow Failed** dialog with a reason. The usual ones are in the table below. **Stuck at "Contacting verifier..."** means the +wallet's engine connection is thrashing (two client connections replacing each other): update the app. + +### Known gotchas +| Symptom | Cause | Fix | +|---|---|---| +| Wallet never answers a pseudonym request | **zkp_req requests** is OFF (default), or the wallet was created before it was switched on | switch it ON, then Disconnect / restart so the wallet is recreated | +| "no verifier id" / the request is refused | the PDP did not return `context.verifier_ids` for the verifier | the PDP must be go-trust >= 0.26.0 with the `policies:` in `environments/nymauth.yaml` (`credential-verifier` and `verifier` -> `x509_san_dns`) | +| verifier answers HTTP 500 / "issuer key set has 2 elements" | the wallet sent the issuer's certificate chain as `issuer_key_set` | the set must hold ONLY the Document Signer (DS) leaf; use an APK with that fix (siros-sdk-kotlin PR 280) | +| the pseudonym differs from other implementations, or the verifier refuses to start | `verifier.nym_auth.verifier_id` must be the PREFIXED client id `x509_san_dns:` and equal the verifier's client_id | fix `verifier_id` (changing it changes every pseudonym) | +| the preset list is empty, or a request has no `_req` items | the preset key is `req` in config, the WIRE member is `_req`; an old UI also dropped them | use a verifier image with the UI fix (vc `a56c4be9e` or later) | +| wallet refuses: invalid_meta | `meta.source_format` / `doctype_value` missing | they are DERIVED from the scope's `credential_metadata`; the preset's credential key must be that scope (`nym_auth`); do not configure them | +| "Contacting verifier..." forever | the engine keeps ONE session per user; two client connections supersede each other | one wallet connection per user; update the app (single-flight reconnect) | +| a verifier request is refused as untrusted | the verifier's request-signing certificate lacks the verifier host as a DNS SAN | `values.verifier.signingCertDnsSan: true` (set) re-issues it on deploy | +| the debug APK shows a 16 KB page-size alignment dialog on first start | a debug-build notice | dismiss it | +| offer redeem fails with invalid/expired code | the code is single use and valid 5 minutes | mint a new one | + +--- + +## For developers (appendix) + +### Starting a presentation without the page +`GET /ui/metadata` lists the presets. `POST /ui/interaction` takes the DCQL the picker builds from a preset (the preset's +`req` goes on the wire as `_req`): +```sh +curl -s -H 'Content-Type: application/json' https://sirosid-nymauth-vc-verifier.fly.dev/ui/interaction -d '{ + "dcql_query": {"credentials": [{ + "id": "nym_auth", "format": "zkp_req", + "meta": {"doctype_value": "eu.europa.ec.eudi.nym_auth.1", "source_format": "mso_mdoc"}, + "_req": [{"fn_id": "pseudonym", "return": "nym", + "params": {"config": "known-offender-matching-ssc", "ver": 1, "prf": "sha-256"}}]}]}}' +``` +The reply has `session_id`, `authorization_request` (`openid4vp://cb?client_id=x509_san_dns%3A&request_uri=...`, the +QR and same-device link) and `dc_api_authorization_request`. For the age preset add a second item +`{"fn_id": "age_check", "return": "age", "params": {"ge": "1980-01-01", "le": "2000-12-31"}}` (the range must span at least +365 days). The request object (`GET `) is an ES256 `oauth-authz-req+jwt` with `client_id` +`x509_san_dns:`, `response_mode` `direct_post.jwt` and an `x5c` whose leaf has the verifier host as a DNS SAN. + +Deliver the link to a connected phone: `adb shell am start -a android.intent.action.VIEW -d 'openid4vp://cb?...'`. + +### Reading the result by API +`GET /ui/completion?session_id=` (poll until done), then `GET /ui/result?response_code=`. Expect the +results keyed by the `return` handles (`nym`, `age`), plus `system_id` and `proof_time`. + +### Checking issuance +- issuer log: `Delivered a secret seed {namespaces: [org.siros.nym_auth], notification_id: ...}`, then `POST /credential` 200 and + `POST /notification` 204; +- engine log: `credential notification forwarded to issuer`; `/v1/evaluate` 200 (decision true); +- the issuer's `/.well-known/openid-credential-issuer` must NOT contain the seed (no 32-hex string) and advertises + `notification_endpoint`. + +### Logs +`flyctl logs --no-tail` returns only the **last 100 lines** per app; at debug level that is about a minute. **Run a continuous +capture before you reproduce:** +```sh +for a in wallet-backend wallet-proxy vc-verifier vc-apigw; do flyctl logs -a sirosid-nymauth-$a > /tmp/$a.log & done +``` +On the phone: `adb logcat` filtered by the app's PID, started before the repro. + +### Asking the PDP what the verifier path returns +```sh +flyctl proxy 18080:8080 -a sirosid-nymauth-pdp & +curl -s -H 'Content-Type: application/json' localhost:18080/evaluation -d '{ + "subject": {"type": "key", "id": "x509_san_dns:sirosid-nymauth-vc-verifier.fly.dev"}, + "resource": {"type": "x5c", "id": "x509_san_dns:sirosid-nymauth-vc-verifier.fly.dev", "key": ["", ""]}, + "action": {"name": "credential-verifier"}}' +``` +Expect `decision: true` and `context.verifier_ids` = `[{"verifier_id_type": "x509_san_dns", "verifier_id": +"sirosid-nymauth-vc-verifier.fly.dev", "basis": "client-id-binding"}]`. The issuer path (`action` `mdoc-issuer`, subject = the issuer +URL, x5c = the DS leaf) is true from the mDOC IACA registry with the leaf alone. + +### The pins +| component | pin | built from | +|---|---|---| +| vc-verifier | `registry.fly.io/sirosid-gdc-vc-verifier:local-1792603698` | vc `integration/gdc-nymauth-v5` @ `a56c4be9e` + circuit-artifact overlay | +| vc-apigw / issuer / registry | `...:local-1792473614` | vc `integration/gdc-nymauth-v4` @ `5e546b2d` | +| wallet-backend (engine) | `registry.fly.io/sirosid-gdc-wallet-backend:local-1792564143` | go-wallet-backend local branch @ `9cb6e57` (#469, #471, #163, #472; #473 inside #471) | +| pdp | `ghcr.io/sirosfoundation/go-trust:0.26.0` | released (go-trust#218) | + +Next vc build (not in the proven set): `c8c5b1ea` with classified zkp_req refusals and SUNET/vc#840 (`allow_preauth_offer`). diff --git a/environments/gdc.yaml b/environments/gdc.yaml index 6fe71ad..0879f2d 100644 --- a/environments/gdc.yaml +++ b/environments/gdc.yaml @@ -33,6 +33,23 @@ images: # exists only in registry.fly.io/sirosid-gdc-* and must be re-pinned to a RELEASED image when its PRs are merged and # released. "Heads" are the PR head commits that were built. # + # KNOWN-GOOD SET (2026-10-21). The nym_auth flow was PROVEN end to end on this gdc, on a Pixel with the Kotlin sample app: + # issuance with seed delivery and notification, and same-device (nym_auth_pseudonym and nym_auth_pseudonym_age) and + # cross-device browser-QR (pseudonym) presentations with a verified, stable pseudonym, with EXACTLY: the verifier, + # engine, PDP and issuer images below (verifier vc a56c4be9e = local-1792603698, engine 9cb6e57 = local-1792564143, + # PDP go-trust 0.26.0 with the verifier_ids policies, apigw/issuer/registry local-1792473614), zk-cred-longfellow v0.1.2 + # and zk-cred-vega v0.0.7, matcher https://github.com/sirosfoundation/siros-dc-matcher v0.11.0 on the wallet side, and the + # circuit catalog https://github.com/sirosfoundation/go-zk-circuits release v0.1.0-release.16 (zk-circuits.fly.dev). The next + # vc build (c8c5b1ea: classified zkp_req refusals + https://github.com/SUNET/vc/pull/840 allow_preauth_offer) is NOT in this + # set yet. environments/nymauth.yaml reproduces this set on its own hosts (docs/NYMAUTH-TESTING.md). + # + # NYMAUTH-CRITICAL SETTINGS in this file (each one broke the flow when wrong; see docs/NYMAUTH-TESTING.md, gotchas): + # verifier.nym_auth.verifier_id = the PREFIXED client_id x509_san_dns:; verifier.preferred_vp_formats.zkp_req + # (system_id_values / proof_system_values); the presets (key `req`, fn_id, ver, prf); the artifact baked into the verifier + # image; verifier.trust.pdp_url; pdp.extraConfig.policies (verifier_ids); issuer.apigw.seeds + credentialEncryption (seed + # delivery); signingCertDnsSan (the verifier's host in the request-signing cert); the engine image (request encryption, + # client_metadata, WMP) with the config unchanged (dcql_consent_check default). + # # vc-verifier registry.fly.io/sirosid-gdc-vc-verifier:local-1792603698 # digest sha256:96c41f16dd9324da6890c76b9faa7810d4c8259cf142ffd16360e413c8ca8945 # source sirosfoundation/vc `integration/gdc-nymauth-v5` @ a56c4be9ec42ef394d536f5ea08d80b50f2a95a0 (merge base diff --git a/environments/nymauth.yaml b/environments/nymauth.yaml new file mode 100644 index 0000000..bc54ec0 --- /dev/null +++ b/environments/nymauth.yaml @@ -0,0 +1,248 @@ +# Persisted config for `make fly-up ENV=nymauth`: a clean, nym_auth-only test environment, derived from gdc. +# +# WHAT THIS IS. gdc is a shared integration playground (Vega ZK, Geneva interop, ...). nymauth is the same stack +# reduced to what is needed to test the nym_auth pseudonym flow end to end: issuance of nym_auth with seed delivery, +# and verification of a zkp_req presentation by the verifier with the PDP returning verifier_ids. The team uses it as a +# KNOWN STARTING POINT. How to test it: docs/NYMAUTH-TESTING.md. +# +# HOW IT DIFFERS FROM gdc (the whole diff is small: `diff environments/gdc.yaml environments/nymauth.yaml`): +# - every hostname is sirosid-nymauth-*; the verifier's `verifier_id` is the PREFIXED client_id of ITS OWN host +# (x509_san_dns:sirosid-nymauth-vc-verifier.fly.dev) - it scopes every pseudonym and must equal the client_id; +# - the images pin the SAME proven builds as gdc (see gdc.yaml's PIN PROVENANCE block for what is in each), pulled from +# gdc's registry namespace (registry.fly.io/sirosid-gdc-*): a new environment has no image of its own yet. This works +# if the deploying account can pull gdc's images (same Fly org); if it cannot, push the same images to the new +# apps' registries after `fly-up` created them and repin here. NOT yet exercised: nobody has run fly-up ENV=nymauth; +# - dropped as not needed for nym_auth: the Geneva/yitbarek/multipaz/Accredify interop trust (trusted_issuers, +# trusted_verifiers, trusted_verifier_roots) and the Geneva RICAL (rical_*). The environment's own issuer and +# verifier are whitelisted automatically. Kept: the Vega presets and zk_circuits_sources (identical to gdc: the +# pinned verifier image was proven with them), wallet attestation, the sample app's signing keys. +# +# EVERY pinned image except the PDP and the frontend is a LOCAL BUILD OF UNMERGED PULL REQUESTS; re-pin to released images +# when they land (list in gdc.yaml's PIN PROVENANCE block). Twins: allowPreauthOffer is NOT set here (the pinned apigw +# lacks SUNET/vc#840), so `make datastore-offer SCOPE=nym_auth` mints against the nym_auth_offer twin. + +region: arn + +# Resolve every SD-JWT credential type from the SIROS registry (see gdc.yaml for the why). +# Resolve every SD-JWT credential type from the SIROS registry instead of the +# VCTMs vendored in fixtures/vc-metadata (REGISTRY=external, PR #33). This is +# what makes vc ADVERTISE the type's real vct (urn:eudi:pid:arf-1.8:1) in +# credential_configurations_supported - for a vendored VCTM it advertises its +# own /type-metadata/ URL while the issued credential carries the urn, +# and the wallets (siros-sdk-kotlin 0.13, wallet-frontend) refuse that +# mismatch: "Issuer delivered a 'urn:eudi:pid:arf-1.8:1' credential, but this +# offer was for https://sirosid-gdc-vc-apigw.fly.dev/type-metadata/pid_1_8". +# mdoc types stay vendored (mdl_zk4 has no registry entry). +credential_registries: + - https://registry.siros.org + +images: + # Same pins as gdc (PIN PROVENANCE in environments/gdc.yaml): tag, digest, base commit and PR heads of every build. + vc-verifier: registry.fly.io/sirosid-gdc-vc-verifier:local-1792603698 # vc a56c4be9e + artifact overlay (zkp_req) + vc-apigw: registry.fly.io/sirosid-gdc-vc-apigw:local-1792473614 # vc v4 5e546b2d (seed delivery) + vc-issuer: registry.fly.io/sirosid-gdc-vc-issuer:local-1792473614 + vc-registry: registry.fly.io/sirosid-gdc-vc-registry:local-1792473614 + wallet-backend: registry.fly.io/sirosid-gdc-wallet-backend:local-1792564143 # engine 9cb6e57 (#469/#471/#163/#472) + pdp: ghcr.io/sirosfoundation/go-trust:0.26.0 # released; verifier_ids (go-trust#218) + wallet-frontend: ghcr.io/sirosfoundation/wallet-frontend:0.5.0-1-sirosid.11 + +# Vega's r12 circuit isn't published to the default zk-circuits.fly.dev +# catalog yet (pending expert review) - this is where it actually lives. +zk_circuits_sources: + - https://zk-circuits-test.fly.dev + +# The sample app's signing keys, so wallet-backend's rp_origins (the +# server-side WebAuthn accept-list) contains the apk-key-hash the app +# actually presents. Without a matching entry, /auth/passkey/register/finish +# fails with "Error validating origin" and signup returns 400 - while login +# with an already-registered passkey keeps working, which makes it look like +# a server regression rather than a missing accept-list entry. +# +# Persisted here on purpose. These previously lived only in `.android-apps`, +# which is gitignored and per-checkout, so a `make fly-up ENV=gdc` from any +# working copy that happened not to have that file silently deployed an +# rp_origins with the app's keys missing - which is exactly what happened on +# 2026-09-07. Environment state that the environment needs belongs in the +# environment file, where anyone's next deploy reproduces it. +# +# Three keys, all for org.siros.sdk.sample: the repo's committed debug +# keystore (sample-app/debug.keystore - what CI and a clean checkout build +# with), a local per-developer debug keystore, and an older one kept so +# devices still carrying that build can log in. `.android-apps` is still read +# and still adds to this; nothing here replaces it. +android_apps: + - org.siros.sdk.sample=A4:DF:EC:00:09:BF:CA:CD:A4:67:F7:D4:6D:A0:AD:96:1C:4E:38:D2:4F:42:5C:6A:21:7F:98:69:9C:C5:82:14 + - org.siros.sdk.sample=C4:07:F2:6D:C3:B4:C8:F9:54:03:F1:A4:A3:6B:47:EA:82:11:6F:D6:3E:A9:AD:EE:56:D9:B4:9B:FA:96:D3:B4 + - org.siros.sdk.sample=46:56:F0:24:08:55:32:F0:94:0F:53:60:A2:0E:6A:31:9A:0B:57:5B:38:C6:BD:F8:48:B6:95:09:39:CB:B0:2F + +wallet_attestation: true + +# Free-form chart values, deep-merged last - see scripts/env_config.py and +# environments/README.md. Anything the chart can express is reachable here +# without a code change anywhere else. +values: + issuer: + apigw: + # nym_auth's seed-backed ssc and its delivery (SUNET/vc#793/#821; the integration build pinned above): + # the issuer derives each person's seed from a per-environment secret (generated once, PERMANENT) and the + # wallet receives it encrypted, which needs the published credential-encryption key. Verified end to end on a + # Pixel 2026-10-20 (sirosid-dev PR 78). The other environments run images without it, so it is off by default. + seeds: + primary: + model: derived + credentialEncryption: + enabled: true + pdp: + # NYMAUTH-CRITICAL: the wallet's pseudonym function needs the verifier's identifier from the PDP + # (context.verifier_ids; go-trust >= 0.26.0 = sirosfoundation/go-trust PR 218, which makes the whitelist, system_cert_pool + # and mdoc_rical registries emit it). Without these policies the wallet refuses with "no verifier id". Policies exist + # ONLY for the two verifier actions; default_policy and fail_closed_on_unknown_action are NOT set and no registries / + # allowed_key_types / require_key_binding are set, so every other action (credential-issuer, mdoc-issuer, + # mdoc-issuer-auth) is evaluated exactly as before (the snippet is docs/examples/gdc-verifier-ids-policies.yaml in that + # PR). A verifier-path replay (subject x509_san_dns:sirosid-nymauth-vc-verifier.fly.dev, x5c = the environment's chain, + # action credential-verifier) returns context.verifier_ids [{x509_san_dns, sirosid-nymauth-vc-verifier.fly.dev}]: the + # bare DNS name, lower case. + extraConfig: + policies: + policies: + credential-verifier: + description: "Verifier access: return the verified dNSName of the verifier" + constraints: + return_verifier_ids: [x509_san_dns] + verifier: + description: "Verifier access (legacy action name): return the verified dNSName" + constraints: + return_verifier_ids: [x509_san_dns] + # Resolve entities identified by did:web, so a presentation from a wallet + # to - or an issuer identified by - a did:web subject can be evaluated at + # all. Additive: it registers one more resolver alongside the whitelist and + # the mdoc registries, and changes nothing about how this environment's own + # issuer and verifier identify themselves (still x509_san_dns, derived from + # their public URLs). + # + # Turning gdc's OWN verifier into a did:web identity is a different switch + # (verifier.clientIdScheme: "did" plus verifier.did), and it would need + # every whitelist entry naming the current x509_san_dns identity revisited. + extraRegistries: + didweb: + enabled: true + name: "did:web" + description: "did:web DID document resolution" + timeout: 10s + verifier: + # nym_auth pseudonym verification (SUNET/vc#827/#829/#830/#832). Needs a vc verifier BUILT WITH the zknative tag + # and the circuit artifact baked in (dockerfiles/verifier-nymauth.Dockerfile); an older verifier ignores these + # keys (non-strict unmarshal) and simply cannot answer a zkp_req request. + # + # verifier_id scopes EVERY pseudonym: SHA-256(verifier_id) is the pseudonym's verifier input. Since rulebook v0.4 + # (vc 2026-10-20, integration/gdc-nymauth-v5 @ fd562f10) it is the verifier's client_id EXACTLY AS PUBLISHED, + # prefix and all: x509_san_dns: (it was the bare host before v0.4, and vc refused the prefix). + # CHANGING IT CHANGES EVERY PSEUDONYM this verifier ever reports. The allowlist values are zk-cred-longfellow's + # src/nymauth/profile.rs (authoritative); do NOT take the digest from docs/CONFIGURATION.md, whose example is a + # placeholder with 52 zeros in the middle. + # + # trust.pdp_url is already set by the chart and MUST stay: nym_auth takes the issuer key from the HOLDER's own + # certificate chain, so the PDP is the only check that key belongs to a real issuer (the verifier refuses to + # start without it). Checked against this environment's PDP 2026-10-20: gdc's issuer chain was trusted for + # eu.europa.ec.eudi.nym_auth.1 (action mdoc-issuer, the mDOC IACA registry) and a self-signed chain with the same + # SAN is not. + extraConfig: + verifier: + nym_auth: + enable: true + verifier_id: x509_san_dns:sirosid-nymauth-vc-verifier.fly.dev + circuits: + allow: + - config: known-offender-matching-ssc + hash_circuit_id: 5f8c75ae5c3893ef6ab6a079628cb385ba85e95a85437d58570c455289d75729 + sig_circuit_id: 00513797c75b2eab1a9db75004977c15317d4e40c0db1d00674523431baa75cb + block_enc_hash: 6983 + block_enc_sig: 4679 + artifact_path: /etc/vc/circuits/nym_auth-ssc.bin + artifact_sha256: 92dd42fdf99673249eec33c7d07850bd9da66fbdab01ef26391e04c8341c1b7f + artifact_bytes: 1062842 + preferred_vp_formats: + # v0.4 names (circuit_zk / circuit_id_values / circuit_proof_system_values fail startup, naming these) + zkp_req: + system_id_values: ["urn:circuits:longfellow:nym_auth:v1"] + proof_system_values: ["longfellow-libzk-v1"] + # The verifier's client_id is x509_san_dns:sirosid-nymauth-vc-verifier.fly.dev; put that host in the SAN of the + # certificate it signs request objects with (re-issued once from the same key and root; see create-pki.sh). + signingCertDnsSan: true + # Selectable options in the verifier's own presentation-definition UI. + # NOT the same thing as presentation-request templates, which are only + # reachable through a real OIDC /authorize?scope=... request. + # + # This lives here rather than in values-base.yaml because the + # category/credentials/zk_system_type shape only exists on the Vega ZK + # branch the images pinned above are built from - a verifier built from + # vc main panics on it at startup. + # Selectable options in the verifier's own presentation-definition UI. NOT + # the same thing as presentationRequestTemplates below, which are only + # reachable through a real OIDC /authorize?scope=... request. + # + # The chart passes presets through verbatim, which matters here: this shape + # (category/credentials/zk_system_type) is only understood by the vc build + # pinned in environments/gdc.yaml, not by vc main. + presets: + # nym_auth pseudonym request (rulebook v0.4 zkp_req; needs preferred_vp_formats.zkp_req and verifier.nym_auth + # above, else the preset is not published). `req` items are {fn_id, return, params}; ver and prf are required. + # The scope key is the verifier's own credential_metadata scope for nym_auth, which supplies meta.source_format + # (mso_mdoc). `validate: nym_auth` makes vc refuse this preset AT STARTUP if it breaks the rulebook. The + # pseudonym is scoped by verifier.nym_auth.verifier_id: changing that changes every pseudonym. + # nym_auth_pseudonym_age adds an age_check; its bounds are vc's example (see there). + "nym_auth_pseudonym": + category: "ZK Proofs" + credentials: + nym_auth: + format: "zkp_req" + validate: "nym_auth" + req: + - fn_id: pseudonym + return: nym + params: + config: known-offender-matching-ssc + ver: 1 + prf: sha-256 + "nym_auth_pseudonym_age": + category: "ZK Proofs" + credentials: + nym_auth: + format: "zkp_req" + validate: "nym_auth" + req: + - fn_id: pseudonym + return: nym + params: + config: known-offender-matching-ssc + ver: 1 + prf: sha-256 + - fn_id: age_check + return: age + # The range must span at least 365 days (a narrower one is an exact birth date) and must suit the + # holder: this window (born 1980-01-01..2000-12-31) is satisfied by alice-001 (born 1990-01-15), so + # it is a positive test; the 2008..2013 example in vc's brief is not. + params: + ge: "1980-01-01" + le: "2000-12-31" + "PID (mdoc) - ZK Proof (Vega)": + category: "ZK Proofs" + credentials: + mdl_zk4: + format: "mso_mdoc_zk" + zk_system_type: + - id: "vega-mc-p256-v1-prover-key-r12" + system: "vega-mc-p256-v1" + claims: + - path: ["org.iso.18013.5.1", "family_name"] + - path: ["org.iso.18013.5.1", "given_name"] + "Age Over 18 - ZK Proof (Vega)": + category: "ZK Proofs" + credentials: + mdl_zk4: + format: "mso_mdoc_zk" + zk_system_type: + - id: "vega-mc-p256-v1-prover-key-r12" + system: "vega-mc-p256-v1" + claims: + - path: ["org.iso.18013.5.1", "age_over_18"] diff --git a/tests/test_create_pki_verifier_san.py b/tests/test_create_pki_verifier_san.py index abc1227..99f004c 100644 --- a/tests/test_create_pki_verifier_san.py +++ b/tests/test_create_pki_verifier_san.py @@ -131,12 +131,12 @@ def test_every_other_environment_is_unchanged(self): for values in ({}, {"verifier": {}}, {"verifier": {"signingCertDnsSan": False}}, None): self.assertNotIn("SIGNING_CERT_VERIFIER_HOST", self.generate(values), values) - def test_gdc_opts_in_and_nothing_else_does(self): + def test_gdc_and_its_nym_auth_derivative_opt_in_and_nothing_else_does(self): import yaml for env_file in sorted((ROOT / "environments").glob("*.yaml")): cfg = yaml.safe_load(env_file.read_text()) or {} on = (((cfg.get("values") or {}).get("verifier") or {}).get("signingCertDnsSan")) - self.assertEqual(bool(on), env_file.stem == "gdc", env_file.name) + self.assertEqual(bool(on), env_file.stem in ("gdc", "nymauth"), env_file.name) if __name__ == "__main__": diff --git a/tests/test_nymauth_environment.py b/tests/test_nymauth_environment.py new file mode 100644 index 0000000..d8ddcfa --- /dev/null +++ b/tests/test_nymauth_environment.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""environments/nymauth.yaml: the nym_auth-only test environment derived from gdc (docs/NYMAUTH-TESTING.md). + +It must stay a SMALL diff against gdc - same chart values and the same proven image pins, only its own hostnames - so the +team can see exactly what is nymauth-specific, and everything nymauth-critical (verifier_id equal to the prefixed +client_id of ITS OWN verifier, the zkp_req formats and presets, the PDP verifier_ids policies, the seed/credential +encryption switches) must be set.""" +import json +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts")) + +import env_config # noqa: E402 +from sirosid_core.naming import Naming # noqa: E402 +from sirosid_core.render import render # noqa: E402 +from sirosid_core.resources import Resources # noqa: E402 + +NEEDS = unittest.skipUnless(shutil.which("helm"), "needs helm") +HOST = "sirosid-nymauth-vc-verifier.fly.dev" + + +def swap(value): + return json.loads(json.dumps(value).replace("sirosid-gdc-vc-verifier", "sirosid-nymauth-vc-verifier")) + + +class SmallDiffAgainstGdc(unittest.TestCase): + def setUp(self): + self.gdc = env_config.load_environment_config("gdc") + self.ny = env_config.load_environment_config("nymauth") + + def test_values_are_gdcs_with_only_the_verifier_host_changed(self): + self.assertEqual(self.ny["values"], swap(self.gdc["values"])) + + def test_the_images_are_the_same_proven_pins(self): + self.assertEqual(self.ny["images"], self.gdc["images"]) + + def test_what_a_nym_auth_test_does_not_need_is_dropped_and_nothing_else(self): + for key in ("trusted_issuers", "trusted_verifiers", "trusted_verifier_roots"): + self.assertEqual(self.ny[key], [], key) + self.assertEqual(self.ny["rical_provider_url"], "") + for key in ("region", "credential_registries", "zk_circuits_sources", "android_apps", "wallet_attestation"): + self.assertEqual(self.ny[key], self.gdc[key], key) + + +@NEEDS +class RenderedNymAuthEnvironment(unittest.TestCase): + @classmethod + def setUpClass(cls): + cfg = env_config.load_environment_config("nymauth") + tmp = Path(tempfile.mkdtemp()) + try: + render("fly", ROOT / "chart", env="nymauth", resources=Resources(ROOT), out_dir=tmp / "o", secrets_dir=tmp / "s", + env_values=cfg["values"], naming=Naming(env="nymauth"), say=lambda *a, **k: None, mongo_password="x") + out = tmp / "o" / "fly-nymauth" + cls.cfg = {n: yaml.safe_load((out / f"{n}.yaml").read_text()) for n in ("vc-verifier", "vc-apigw", "vc-issuer", "pdp")} + cls.vctms = sorted(p.name for p in (out / "vctms").glob("nym_auth*")) + finally: + shutil.rmtree(tmp, ignore_errors=True) + + def test_verifier_id_is_the_prefixed_client_id_of_its_own_host(self): + verifier = self.cfg["vc-verifier"]["verifier"] + self.assertEqual(verifier["public_url"], f"https://{HOST}") + self.assertEqual(verifier["nym_auth"]["verifier_id"], f"x509_san_dns:{HOST}") + self.assertTrue(verifier["nym_auth"]["enable"]) + + def test_the_zkp_req_formats_presets_and_pdp_are_set(self): + verifier = self.cfg["vc-verifier"]["verifier"] + self.assertEqual(verifier["preferred_vp_formats"]["zkp_req"], + {"system_id_values": ["urn:circuits:longfellow:nym_auth:v1"], + "proof_system_values": ["longfellow-libzk-v1"]}) + for preset in ("nym_auth_pseudonym", "nym_auth_pseudonym_age"): + self.assertEqual(verifier["presets"][preset]["credentials"]["nym_auth"]["format"], "zkp_req") + self.assertEqual(verifier["trust"]["pdp_url"], "http://sirosid-nymauth-pdp.internal:8080") + + def test_the_pdp_returns_verifier_ids_for_the_verifier_actions_only(self): + policies = self.cfg["pdp"]["policies"]["policies"] + self.assertEqual(set(policies), {"credential-verifier", "verifier"}) + whitelist = self.cfg["pdp"]["registries"]["whitelist"]["lists"] + self.assertEqual(whitelist["verifiers"], ["https://sirosid-nymauth-vc-apigw.fly.dev", f"https://{HOST}"]) + + def test_the_issuer_delivers_the_seed_encrypted_with_a_notification_endpoint(self): + apigw = self.cfg["vc-apigw"]["apigw"] + self.assertEqual(apigw["seeds"]["primary"]["model"], "derived") + self.assertIn("credential_encryption", apigw["issuer_metadata"]) + self.assertEqual(apigw["issuer_metadata"]["notification_endpoint"], "https://sirosid-nymauth-vc-apigw.fly.dev/notification") + self.assertIn("nym_auth_offer", self.cfg["vc-apigw"]["common"]["credential_metadata"], "twins stay: no #840 in the pinned apigw") + + def test_no_other_environments_hostname_leaks_into_the_configs(self): + for name, cfg in self.cfg.items(): + text = json.dumps(cfg) + for stale in ("sirosid-gdc-vc", "sirosid-gdc-pdp", "sirosid-gdc-wallet", "sirosid-gdc-mongodb", "sirosid-gdc-mini"): + self.assertNotIn(stale, text, f"{name}: {stale}") + + +if __name__ == "__main__": + unittest.main()