From e710dd9746ea1e1e374d5fac59097c998e5b9bba Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Tue, 20 Oct 2026 10:19:04 +0200 Subject: [PATCH 1/5] gdc: nym_auth verifier config (allowlist, circuit_zk formats) and an image overlay that bakes in the circuit artifact Preparation only: the pinned verifier image predates SUNET/vc#832 and ignores the new keys. Helm renders artifact_bytes (1062842) as 1.062842e+06, which became 1062842.0; normalised to an integer for the allowlist. Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 3 + README.md | 13 +++ dockerfiles/verifier-nymauth.Dockerfile | 29 +++++ environments/gdc.yaml | 34 ++++++ fixtures/vc-config-golden/accepted-diffs.yaml | 3 + sirosid_core/vc_render.py | 14 ++- tests/test_nymauth_verifier_render.py | 108 ++++++++++++++++++ 7 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 dockerfiles/verifier-nymauth.Dockerfile create mode 100644 tests/test_nymauth_verifier_render.py diff --git a/.gitignore b/.gitignore index 27739554..fdadf4eb 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ fixtures/wrpac-clients/*.issued/ # bootmgr editable install metadata (make install) *.egg-info/ + +# nym_auth circuit artifact (decompressed, ~1 MB): a build input for dockerfiles/verifier-nymauth.Dockerfile +fixtures/nym-auth-circuits/*.bin diff --git a/README.md b/README.md index 4ebbae00..dcb8e5b7 100644 --- a/README.md +++ b/README.md @@ -517,6 +517,19 @@ in:** holder-supplied commitments (the `known-offender-matching-psc` configurati 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.circuit_zk`. 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 bare host of the +verifier's `public_url` and scopes every pseudonym: changing it changes every pseudonym this verifier reports.** + ### Service Architecture ``` diff --git a/dockerfiles/verifier-nymauth.Dockerfile b/dockerfiles/verifier-nymauth.Dockerfile new file mode 100644 index 00000000..1e59380c --- /dev/null +++ b/dockerfiles/verifier-nymauth.Dockerfile @@ -0,0 +1,29 @@ +# The vc verifier image plus the nym_auth circuit artifact, for environments that verify nym_auth pseudonym proofs +# (environments/gdc.yaml, verifier.nym_auth). +# +# The artifact is the DECOMPRESSED Longfellow circuit (1,062,842 bytes). It cannot travel as a Fly file secret +# (base64 of it exceeds the ~1 MiB machine config body) and the vc repo does not carry it, so it is baked in here. +# The size and digest are checked at build time against the values the allowlist entry in environments/gdc.yaml +# states, which are in turn the ones zk-cred-longfellow's src/nymauth/profile.rs ships; vc re-checks both at startup +# and refuses anything else, so a wrong file fails the build rather than the first presentation. +# +# zstd -dc /longfellow/mdoc/circuits.zst > fixtures/nym-auth-circuits/nym_auth-ssc.bin # gitignored +# docker build -f dockerfiles/verifier-nymauth.Dockerfile \ +# --build-arg BASE=registry.fly.io/sirosid-gdc-vc-verifier: \ +# -t registry.fly.io/sirosid-gdc-vc-verifier:local- . +# +# The source archive is ours (no repository yet): ~/.cache/nymzip/nym-auth-circuits/longfellow/mdoc/circuits.zst +# (395,608 bytes, sha256 6c4b96ed431ee547a3a2e5b62621943a9a0a44646267653bf2db0fcc269b5c3b). Fetch it from its +# repository once there is one. +ARG BASE +FROM alpine:3.20 AS check +ARG ARTIFACT_BYTES=1062842 +ARG ARTIFACT_SHA256=92dd42fdf99673249eec33c7d07850bd9da66fbdab01ef26391e04c8341c1b7f +COPY fixtures/nym-auth-circuits/nym_auth-ssc.bin /nym_auth-ssc.bin +RUN set -e; \ + [ "$(wc -c < /nym_auth-ssc.bin)" = "${ARTIFACT_BYTES}" ] || { echo "artifact is not ${ARTIFACT_BYTES} bytes (decompressed?)"; exit 1; }; \ + echo "${ARTIFACT_SHA256} /nym_auth-ssc.bin" | sha256sum -c - + +FROM ${BASE} +# A distroless runtime has no shell: COPY only, readable by whatever user the image runs as. +COPY --from=check --chmod=0444 /nym_auth-ssc.bin /etc/vc/circuits/nym_auth-ssc.bin diff --git a/environments/gdc.yaml b/environments/gdc.yaml index 7299aba3..972611a2 100644 --- a/environments/gdc.yaml +++ b/environments/gdc.yaml @@ -283,6 +283,40 @@ values: 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 circuit_zk request. + # + # verifier_id is BARE and scopes EVERY pseudonym: SHA-256(verifier_id) is the pseudonym's verifier input. It must + # be the verifier's own client_id identifier without the `x509_san_dns:` prefix (public_url's host). 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: the gdc issuer's chain is 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: sirosid-gdc-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: + circuit_zk: + circuit_id_values: ["urn:circuits:longfellow:nym_auth:v1"] + circuit_proof_system_values: ["longfellow-libzk-v1"] # 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. diff --git a/fixtures/vc-config-golden/accepted-diffs.yaml b/fixtures/vc-config-golden/accepted-diffs.yaml index 3bc5e2a8..8689e5d3 100644 --- a/fixtures/vc-config-golden/accepted-diffs.yaml +++ b/fixtures/vc-config-golden/accepted-diffs.yaml @@ -287,3 +287,6 @@ fly-gdc: - path: verifier.zk_circuits reason: "now driven by environments/.yaml's zk_circuits_sources rather than a CLI-only flag" + - path: verifier.nym_auth + kind: added + reason: "gdc verifies nym_auth pseudonym proofs (SUNET/vc#832; environments/gdc.yaml verifier.extraConfig). Needs a zknative verifier image with the circuit artifact baked in (dockerfiles/verifier-nymauth.Dockerfile)." diff --git a/sirosid_core/vc_render.py b/sirosid_core/vc_render.py index ecd82c68..77976b87 100644 --- a/sirosid_core/vc_render.py +++ b/sirosid_core/vc_render.py @@ -519,6 +519,18 @@ def write_as_rules(docs: list, out_dir: Path) -> Path: return _write_documents(docs, out_dir, "wallet-backend-as-rules", "as-rules") +def integral_nym_auth_numbers(config: dict) -> dict: + """Helm renders an integer of a million or more as a float (artifact_bytes 1062842 becomes `1.062842e+06`, + which PyYAML then dumps as `1062842.0`), and vc's allowlist wants an integer there. Only the allowlist's + numeric fields are touched, and only when the float is integral.""" + allow = (((config.get("verifier") or {}).get("nym_auth") or {}).get("circuits") or {}).get("allow") or [] + for entry in allow: + for key in ("artifact_bytes", "block_enc_hash", "block_enc_sig"): + if isinstance(entry.get(key), float) and entry[key].is_integer(): + entry[key] = int(entry[key]) + return config + + def render_vc(docs: list, out_dir: Path, target: str, secrets_dir: Path, gen_secret, plain_http_hosts: set = None, secret_overrides: dict = None, env: str = None, mongo_password: str = None, @@ -539,7 +551,7 @@ def render_vc(docs: list, out_dir: Path, target: str, secrets_dir: Path, gen_sec seeds_enabled = bool((config.get("apigw") or {}).get("seeds")) if target == "fly": drop_twin_imports(config) - config = strip_unrenderable(config) + config = integral_nym_auth_numbers(strip_unrenderable(config)) if credential_registries: config = use_credential_registry(config, credential_types or {}, credential_registries) (out_dir / filename).write_text(yaml.dump(config, sort_keys=False)) diff --git a/tests/test_nymauth_verifier_render.py b/tests/test_nymauth_verifier_render.py new file mode 100644 index 00000000..4da2df2f --- /dev/null +++ b/tests/test_nymauth_verifier_render.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""The gdc verifier's nym_auth block (SUNET/vc#832): what the rendered verifier config must say, and the one value +that is easy to get wrong silently. + +`verifier_id` scopes every pseudonym, and a wrong one is not an error anyone sees: it yields a pseudonym no other +implementation reports. The artifact digest has a placeholder lookalike (docs/CONFIGURATION.md's example keeps the +real digest's first 8 and last 4 hex characters and zeroes the middle), so the check below is on the whole value.""" +import re +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") + +# zk-cred-longfellow src/nymauth/profile.rs - the authoritative source (the native loader hashes the decompressed +# artifact and compares against exactly these). +PROFILE = { + "hash_circuit_id": "5f8c75ae5c3893ef6ab6a079628cb385ba85e95a85437d58570c455289d75729", + "sig_circuit_id": "00513797c75b2eab1a9db75004977c15317d4e40c0db1d00674523431baa75cb", + "block_enc_hash": 6983, + "block_enc_sig": 4679, + "artifact_bytes": 1062842, + "artifact_sha256": "92dd42fdf99673249eec33c7d07850bd9da66fbdab01ef26391e04c8341c1b7f", +} + + +def render_gdc_verifier(): + tmp = Path(tempfile.mkdtemp()) + try: + values = env_config.load_environment_config("gdc").get("values") or {} + naming = Naming(env="gdc") + render("fly", ROOT / "chart", env="gdc", resources=Resources(ROOT), out_dir=tmp / "out", secrets_dir=tmp / "secrets", + env_values=values, naming=naming, say=lambda *a, **k: None, mongo_password="x") + text = (tmp / "out" / "fly-gdc" / "vc-verifier.yaml").read_text() + return text, yaml.safe_load(text) + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +@NEEDS +class GdcNymAuthVerifier(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.text, cfg = render_gdc_verifier() + cls.cfg = cfg["verifier"] + + def test_enabled_with_exactly_the_profile_the_circuit_ships(self): + entry = self.cfg["nym_auth"]["circuits"]["allow"] + self.assertEqual(len(entry), 1) + self.assertEqual({k: entry[0][k] for k in PROFILE}, PROFILE) + self.assertEqual(entry[0]["config"], "known-offender-matching-ssc") + self.assertTrue(self.cfg["nym_auth"]["enable"]) + + def test_numbers_are_integers_in_the_file_vc_reads(self): + """Helm renders 1062842 as 1.062842e+06 and PyYAML dumps that back as 1062842.0; a Go int field refuses it.""" + for k in ("artifact_bytes", "block_enc_hash", "block_enc_sig"): + self.assertIs(type(self.cfg["nym_auth"]["circuits"]["allow"][0][k]), int, k) + self.assertRegex(self.text, r"artifact_bytes: 1062842\n") + + def test_the_digest_is_not_the_documentation_placeholder(self): + digest = self.cfg["nym_auth"]["circuits"]["allow"][0]["artifact_sha256"] + self.assertRegex(digest, r"^[0-9a-f]{64}$") + self.assertNotRegex(digest, r"0{8}", "a run of zeros is the doc_example placeholder, not a SHA-256") + + def test_ids_are_strings_not_numbers_the_yaml_could_reinterpret(self): + entry = self.cfg["nym_auth"]["circuits"]["allow"][0] + for k in ("hash_circuit_id", "sig_circuit_id", "artifact_sha256"): + self.assertIsInstance(entry[k], str, k) + + def test_verifier_id_is_the_bare_client_id_identifier(self): + """client_id is x509_san_dns:; verifier_id is that host, never prefixed. Changing it + changes every pseudonym.""" + vid = self.cfg["nym_auth"]["verifier_id"] + self.assertEqual(vid, re.sub(r"^https://", "", self.cfg["public_url"])) + self.assertNotIn(":", vid) + + def test_the_artifact_path_is_where_the_overlay_image_puts_it(self): + self.assertEqual(self.cfg["nym_auth"]["circuits"]["allow"][0]["artifact_path"], "/etc/vc/circuits/nym_auth-ssc.bin") + dockerfile = (ROOT / "dockerfiles" / "verifier-nymauth.Dockerfile").read_text() + self.assertIn("/etc/vc/circuits/nym_auth-ssc.bin", dockerfile) + self.assertIn(PROFILE["artifact_sha256"], dockerfile) + self.assertIn(str(PROFILE["artifact_bytes"]), dockerfile) + + def test_a_pdp_is_configured_because_the_verifier_refuses_to_start_without_one(self): + self.assertTrue(self.cfg["trust"]["pdp_url"], "nym_auth takes the issuer key from the holder's chain; no PDP = refuse to start") + + def test_the_request_may_name_only_what_this_verifier_offers(self): + zk = self.cfg["preferred_vp_formats"]["circuit_zk"] + self.assertEqual(zk["circuit_id_values"], ["urn:circuits:longfellow:nym_auth:v1"]) + self.assertEqual(zk["circuit_proof_system_values"], ["longfellow-libzk-v1"]) + self.assertIn("dc+sd-jwt", self.cfg["preferred_vp_formats"], "adding circuit_zk must not drop the existing formats") + + +if __name__ == "__main__": + unittest.main() From 4f4c99e8251e7c9f28676c8e7797fc956c3b7fe0 Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Tue, 20 Oct 2026 10:42:36 +0200 Subject: [PATCH 2/5] verifier-nymauth: stage the artifact with readable directories (COPY --chmod made them 0444, so the non-root verifier could not open it) Found by starting the v5 verifier image locally: 'open /etc/vc/circuits/nym_auth-ssc.bin: permission denied'. Co-Authored-By: Claude Sonnet 5.5 --- dockerfiles/verifier-nymauth.Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dockerfiles/verifier-nymauth.Dockerfile b/dockerfiles/verifier-nymauth.Dockerfile index 1e59380c..f47f77d4 100644 --- a/dockerfiles/verifier-nymauth.Dockerfile +++ b/dockerfiles/verifier-nymauth.Dockerfile @@ -24,6 +24,11 @@ RUN set -e; \ [ "$(wc -c < /nym_auth-ssc.bin)" = "${ARTIFACT_BYTES}" ] || { echo "artifact is not ${ARTIFACT_BYTES} bytes (decompressed?)"; exit 1; }; \ echo "${ARTIFACT_SHA256} /nym_auth-ssc.bin" | sha256sum -c - +# Staged with its directories here: a distroless runtime has no shell to chmod with, and `COPY --chmod` gives the +# directories it creates the FILE's mode (0444: no x bit), which makes the artifact unreadable to the non-root user +# ("open /etc/vc/circuits/nym_auth-ssc.bin: permission denied" at startup). +RUN mkdir -p /out/etc/vc/circuits && cp /nym_auth-ssc.bin /out/etc/vc/circuits/nym_auth-ssc.bin \ + && chmod 0755 /out/etc /out/etc/vc /out/etc/vc/circuits && chmod 0444 /out/etc/vc/circuits/nym_auth-ssc.bin + FROM ${BASE} -# A distroless runtime has no shell: COPY only, readable by whatever user the image runs as. -COPY --from=check --chmod=0444 /nym_auth-ssc.bin /etc/vc/circuits/nym_auth-ssc.bin +COPY --from=check /out/ / From 6c2315d61ab052320ba5e988d76f745ec6150baf Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Tue, 20 Oct 2026 20:24:30 +0200 Subject: [PATCH 3/5] gdc verifier: rulebook v0.4 config (zkp_req, system_id_values, proof_system_values; verifier_id is the prefixed client_id) Co-Authored-By: Claude Sonnet 5.5 --- README.md | 9 +++++---- environments/gdc.yaml | 18 ++++++++++-------- tests/test_nymauth_verifier_render.py | 24 +++++++++++++++--------- values-base.yaml | 2 +- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index dcb8e5b7..5421ef0a 100644 --- a/README.md +++ b/README.md @@ -471,7 +471,7 @@ The verifier has matching presentation-request templates in `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 -`circuit_zk` presentations: pseudonyms and age predicates proven in zero +`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`). @@ -521,14 +521,15 @@ for a configured commitment claim. `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.circuit_zk`. The ~1 MB decompressed artifact cannot +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 bare host of the -verifier's `public_url` and scopes every pseudonym: changing it changes every pseudonym this verifier reports.** +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:` 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.** ### Service Architecture diff --git a/environments/gdc.yaml b/environments/gdc.yaml index 972611a2..280693f1 100644 --- a/environments/gdc.yaml +++ b/environments/gdc.yaml @@ -285,11 +285,12 @@ values: 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 circuit_zk request. + # keys (non-strict unmarshal) and simply cannot answer a zkp_req request. # - # verifier_id is BARE and scopes EVERY pseudonym: SHA-256(verifier_id) is the pseudonym's verifier input. It must - # be the verifier's own client_id identifier without the `x509_san_dns:` prefix (public_url's host). CHANGING IT - # CHANGES EVERY PSEUDONYM this verifier ever reports. The allowlist values are zk-cred-longfellow's + # 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. # @@ -302,7 +303,7 @@ values: verifier: nym_auth: enable: true - verifier_id: sirosid-gdc-vc-verifier.fly.dev + verifier_id: x509_san_dns:sirosid-gdc-vc-verifier.fly.dev circuits: allow: - config: known-offender-matching-ssc @@ -314,9 +315,10 @@ values: artifact_sha256: 92dd42fdf99673249eec33c7d07850bd9da66fbdab01ef26391e04c8341c1b7f artifact_bytes: 1062842 preferred_vp_formats: - circuit_zk: - circuit_id_values: ["urn:circuits:longfellow:nym_auth:v1"] - circuit_proof_system_values: ["longfellow-libzk-v1"] + # 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"] # 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. diff --git a/tests/test_nymauth_verifier_render.py b/tests/test_nymauth_verifier_render.py index 4da2df2f..2a869cdd 100644 --- a/tests/test_nymauth_verifier_render.py +++ b/tests/test_nymauth_verifier_render.py @@ -80,12 +80,12 @@ def test_ids_are_strings_not_numbers_the_yaml_could_reinterpret(self): for k in ("hash_circuit_id", "sig_circuit_id", "artifact_sha256"): self.assertIsInstance(entry[k], str, k) - def test_verifier_id_is_the_bare_client_id_identifier(self): - """client_id is x509_san_dns:; verifier_id is that host, never prefixed. Changing it - changes every pseudonym.""" + def test_verifier_id_is_the_published_client_id(self): + """Rulebook v0.4: verifier_id is the client_id exactly as published, x509_san_dns:, prefix + included (the bare host was the rule before v0.4 and is the NOT-a-verifier_id value now: it yields a + pseudonym no other implementation reports). Changing it changes every pseudonym.""" vid = self.cfg["nym_auth"]["verifier_id"] - self.assertEqual(vid, re.sub(r"^https://", "", self.cfg["public_url"])) - self.assertNotIn(":", vid) + self.assertEqual(vid, "x509_san_dns:" + re.sub(r"^https://", "", self.cfg["public_url"])) def test_the_artifact_path_is_where_the_overlay_image_puts_it(self): self.assertEqual(self.cfg["nym_auth"]["circuits"]["allow"][0]["artifact_path"], "/etc/vc/circuits/nym_auth-ssc.bin") @@ -98,10 +98,16 @@ def test_a_pdp_is_configured_because_the_verifier_refuses_to_start_without_one(s self.assertTrue(self.cfg["trust"]["pdp_url"], "nym_auth takes the issuer key from the holder's chain; no PDP = refuse to start") def test_the_request_may_name_only_what_this_verifier_offers(self): - zk = self.cfg["preferred_vp_formats"]["circuit_zk"] - self.assertEqual(zk["circuit_id_values"], ["urn:circuits:longfellow:nym_auth:v1"]) - self.assertEqual(zk["circuit_proof_system_values"], ["longfellow-libzk-v1"]) - self.assertIn("dc+sd-jwt", self.cfg["preferred_vp_formats"], "adding circuit_zk must not drop the existing formats") + zk = self.cfg["preferred_vp_formats"]["zkp_req"] + self.assertEqual(zk["system_id_values"], ["urn:circuits:longfellow:nym_auth:v1"]) + self.assertEqual(zk["proof_system_values"], ["longfellow-libzk-v1"]) + self.assertIn("dc+sd-jwt", self.cfg["preferred_vp_formats"], "adding zkp_req must not drop the existing formats") + + def test_no_pre_v0_4_names_remain(self): + """vc fails startup on circuit_zk / circuit_id_values / circuit_proof_system_values, naming the replacement.""" + text = self.text + for old in ("circuit_zk", "circuit_id_values", "circuit_proof_system_values"): + self.assertNotIn(old, text, old) if __name__ == "__main__": diff --git a/values-base.yaml b/values-base.yaml index d23fd178..c29eff01 100644 --- a/values-base.yaml +++ b/values-base.yaml @@ -353,7 +353,7 @@ features: claims: [given_name, family_name] # Nym-authentication attestation (sirosfoundation/nym_auth_rulebook v0.3): # birth_date + ssc (SHA-256 of a 16-byte secret seed shared by issuer and - # prover) + optional psc, for circuit_zk pseudonym and age-predicate + # prover) + optional psc, for zkp_req (formerly circuit_zk) pseudonym and age-predicate # presentations. doctype and namespace are both eu.europa.ec.eudi.nym_auth.1. # # fixtures/vc-metadata/nym_auth.mdoc.json is a byte-for-byte COPY of the From c5a7897070342babf60d464b9292806ad665b223 Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Wed, 21 Oct 2026 02:21:09 +0200 Subject: [PATCH 4/5] gdc: nym_auth_pseudonym and nym_auth_pseudonym_age zkp_req presets (shape per vc's brief), with a test Co-Authored-By: Claude Sonnet 5.5 --- environments/gdc.yaml | 40 +++++++++++++++++++++++++++ tests/test_nymauth_verifier_render.py | 26 +++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/environments/gdc.yaml b/environments/gdc.yaml index 280693f1..27a1a3c7 100644 --- a/environments/gdc.yaml +++ b/environments/gdc.yaml @@ -335,6 +335,46 @@ values: # (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: alice-001 was born 1990-01-15, so this window (born 2008-10-10..2013-10-09, from vc's + # example) is NOT satisfiable by her; use it to test refusal, or change it for a positive test. + params: + ge: "2008-10-10" + le: "2013-10-09" "PID (mdoc) - ZK Proof (Vega)": category: "ZK Proofs" credentials: diff --git a/tests/test_nymauth_verifier_render.py b/tests/test_nymauth_verifier_render.py index 2a869cdd..9b642144 100644 --- a/tests/test_nymauth_verifier_render.py +++ b/tests/test_nymauth_verifier_render.py @@ -55,6 +55,7 @@ class GdcNymAuthVerifier(unittest.TestCase): @classmethod def setUpClass(cls): cls.text, cfg = render_gdc_verifier() + cls.full = cfg cls.cfg = cfg["verifier"] def test_enabled_with_exactly_the_profile_the_circuit_ships(self): @@ -103,6 +104,31 @@ def test_the_request_may_name_only_what_this_verifier_offers(self): self.assertEqual(zk["proof_system_values"], ["longfellow-libzk-v1"]) self.assertIn("dc+sd-jwt", self.cfg["preferred_vp_formats"], "adding zkp_req must not drop the existing formats") + def test_the_nym_auth_presets_are_shaped_as_vc_reads_them(self): + """vc's brief (SUNET/vc#832): preset key `req` (the WIRE member `_req` was silently accepted and left empty), + item key fn_id, ver and prf required, no source_format / doctype_value (both derive from the scope's + credential_metadata, so a scope key that is not that entry reproduces invalid_meta in every wallet).""" + presets = self.cfg["presets"] + scopes = self.full["common"]["credential_metadata"] + for label in ("nym_auth_pseudonym", "nym_auth_pseudonym_age"): + self.assertEqual(list(presets[label]["credentials"]), ["nym_auth"], label) + self.assertIn("nym_auth", scopes, "the preset's scope key must be a common.credential_metadata scope") + self.assertEqual(scopes["nym_auth"]["format"], "mso_mdoc") + cred = presets[label]["credentials"]["nym_auth"] + self.assertEqual((cred["format"], cred["validate"]), ("zkp_req", "nym_auth")) + self.assertNotIn("_req", cred) + for banned in ("source_format", "doctype_value"): + self.assertNotIn(banned, cred, banned) + first = cred["req"][0] + self.assertEqual((first["fn_id"], first["params"]["ver"], first["params"]["prf"]), ("pseudonym", 1, "sha-256")) + self.assertEqual(first["params"]["config"], "known-offender-matching-ssc") + for item in cred["req"]: + self.assertIn("fn_id", item) + self.assertNotIn("id", item, "`id` was the pre-v0.4 item key") + ages = [i for i in presets["nym_auth_pseudonym_age"]["credentials"]["nym_auth"]["req"] if i["fn_id"] == "age_check"] + self.assertEqual(len(ages), 1) + self.assertEqual(len(presets["nym_auth_pseudonym"]["credentials"]["nym_auth"]["req"]), 1) + def test_no_pre_v0_4_names_remain(self): """vc fails startup on circuit_zk / circuit_id_values / circuit_proof_system_values, naming the replacement.""" text = self.text From c775877c54701465b89de8d77a5e01f21c47c316 Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Wed, 21 Oct 2026 02:48:40 +0200 Subject: [PATCH 5/5] gdc: age preset window that alice-001 satisfies (born 1980-01-01..2000-12-31) Co-Authored-By: Claude Sonnet 5.5 --- environments/gdc.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/environments/gdc.yaml b/environments/gdc.yaml index 27a1a3c7..85fc16d4 100644 --- a/environments/gdc.yaml +++ b/environments/gdc.yaml @@ -370,11 +370,11 @@ values: - 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: alice-001 was born 1990-01-15, so this window (born 2008-10-10..2013-10-09, from vc's - # example) is NOT satisfiable by her; use it to test refusal, or change it for a positive test. + # 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: "2008-10-10" - le: "2013-10-09" + ge: "1980-01-01" + le: "2000-12-31" "PID (mdoc) - ZK Proof (Vega)": category: "ZK Proofs" credentials: