From c4b0855c0eac032c691956821fca4a51a5035f57 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 28 Jun 2026 01:50:01 -0400 Subject: [PATCH 01/25] docs: reshape onboarding tutorials Signed-off-by: Jeremi Joslin --- crates/registryctl/CHANGELOG.md | 13 + crates/registryctl/src/lib.rs | 280 ++++++++- crates/registryctl/src/sample.rs | 546 ++++++++++++++++-- .../src/templates/compose-with-notary.yaml | 1 - crates/registryctl/src/templates/compose.yaml | 1 - .../src/templates/notary_project_readme.md | 15 +- .../src/templates/project_readme.md | 32 +- .../src/templates/relay_config.yaml.tmpl | 289 ++++++++- .../src/templates/relay_metadata.yaml | 64 -- docs/site/astro.config.mjs | 27 +- docs/site/scripts/check-tutorial.sh | 4 +- docs/site/src/components/HomeLanding.astro | 20 +- .../docs/explanation/integration-patterns.mdx | 8 +- .../docs/reference/apis/registry-notary.mdx | 12 +- .../docs/reference/environment-variables.mdx | 8 +- .../content/docs/reference/registryctl.mdx | 7 +- .../src/content/docs/start/see-it-live.mdx | 161 +----- .../configure-dhis2-claim-checks.mdx | 334 +++++++++++ .../deploy-standalone-with-own-data.mdx | 4 +- .../tutorials/first-run-with-registry-lab.mdx | 4 +- .../getting-started-fhir-evidence.mdx | 8 +- ...blish-spreadsheet-secured-registry-api.mdx | 204 ++----- .../run-notary-standalone-for-api.mdx | 223 +++++++ .../tutorials/verify-claim-registry-api.mdx | 351 ++--------- products/notary/docs/README.md | 6 +- .../docs/source-claim-modeling-guide.md | 12 +- 26 files changed, 1821 insertions(+), 813 deletions(-) delete mode 100644 crates/registryctl/src/templates/relay_metadata.yaml create mode 100644 docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx create mode 100644 docs/site/src/content/docs/tutorials/run-notary-standalone-for-api.mdx diff --git a/crates/registryctl/CHANGELOG.md b/crates/registryctl/CHANGELOG.md index 67cbba2a..1b7f0d1d 100644 --- a/crates/registryctl/CHANGELOG.md +++ b/crates/registryctl/CHANGELOG.md @@ -22,6 +22,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). `fingerprint.commitment` in YAML. Generated configs reference fingerprint env vars only; local raw keys and matching fingerprint values remain in `secrets/local.env`. +- The generated benefits sample now uses a richer three-sheet workbook + (`Households`, `Persons`, `Applications`) and a broader Bruno collection covering + discovery, row reads, relationship expansion, purpose-header failures, and aggregates. +- The generated Relay sample config now includes focused YAML comments that explain auth + fingerprints, source tables, public entities, relationships, filters, and aggregates. +- `registryctl init relay ` no longer generates a duplicate split `relay/metadata.yaml` + manifest for the local sample; Relay derives standards metadata from `relay/config.yaml` + unless a project explicitly opts into split metadata. + +### Fixed + +- The generated Relay sample no longer binds `person.id` to the API-key principal id, + which made the Bruno "Read sample people" request return an empty result set. ## [0.1.0] - 2026-06-12 diff --git a/crates/registryctl/src/lib.rs b/crates/registryctl/src/lib.rs index fbc72fa9..1e2ef019 100644 --- a/crates/registryctl/src/lib.rs +++ b/crates/registryctl/src/lib.rs @@ -1353,7 +1353,6 @@ fn init_benefits_project(dir: &Path) -> Result<()> { write_text(dir.join("README.md"), project_readme())?; write_text(dir.join(".gitignore"), include_str!("templates/gitignore"))?; write_text(dir.join("relay/config.yaml"), &relay_config(&credentials))?; - write_text(dir.join("relay/metadata.yaml"), relay_metadata())?; write_text(dir.join("secrets/local.env"), &credentials.env_file())?; write_text(dir.join("output/.gitkeep"), "")?; sample::write_benefits_workbook(&dir.join("data/benefits_casework.xlsx"))?; @@ -2554,8 +2553,22 @@ fn generated_file(path: &str, contents: &str) -> GeneratedFile { } fn bruno_relay_files(relay_base_url: &str, _secrets: &LocalEnv) -> Vec { + let application_query_body = r#"{ + "measures": ["application_count"], + "group_by": ["program", "application_status"], + "filters": { + "program": "cash_transfer" + } +}"#; + vec![ - bruno_get("Relay/Health.bru", "Relay health", 1, "{{relay_base_url}}/healthz", &[]), + bruno_get( + "Relay/Health.bru", + "Relay health", + 1, + "{{relay_base_url}}/healthz", + &[], + ), bruno_get("Relay/Ready.bru", "Relay ready", 2, "{{relay_base_url}}/ready", &[]), bruno_get( "Relay/OpenAPI.bru", @@ -2578,16 +2591,163 @@ fn bruno_relay_files(relay_base_url: &str, _secrets: &LocalEnv) -> Vec Result { #[derive(Serialize)] struct RelaySection<'a> { config: &'a str, - metadata: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option<&'a str>, data: Vec<&'a str>, } @@ -3511,7 +3676,7 @@ fn registryctl_manifest(dir: &Path, kind: ProjectManifestKind<'_>) -> Result &'static str { - include_str!("templates/relay_metadata.yaml") -} - #[derive(Debug, Deserialize, Serialize)] struct SmokeReport { base_url: String, @@ -3681,12 +3842,11 @@ fn run_smoke_checks(base_url: &str, secrets: &LocalEnv) -> SmokeReport { 400, &[bearer_header(secrets.value("ROW_READER_RAW"))], ); - record_smoke_check( + record_row_data_smoke_check( &mut checks, base_url, "row reader can read filtered records", "/v1/datasets/benefits_casework/entities/person/records?household_id=hh-1001", - 200, &[ bearer_header(secrets.value("ROW_READER_RAW")), ( @@ -3835,6 +3995,44 @@ fn record_smoke_check( } } +fn record_row_data_smoke_check( + checks: &mut Vec, + base_url: &str, + name: &'static str, + path: &'static str, + headers: &[(String, String)], +) { + let url = format!("{base_url}{path}"); + match http_get(&url, headers) { + Ok(response) => { + let has_rows = response.status == 200 + && serde_json::from_str::(&response.body) + .ok() + .and_then(|value| value["data"].as_array().map(|data| !data.is_empty())) + .unwrap_or(false); + checks.push(SmokeCheck { + name: name.to_string(), + method: "GET".to_string(), + path: path.to_string(), + expected_status: 200, + actual_status: Some(response.status), + passed: has_rows, + error: (!has_rows) + .then(|| "row response did not include any sample records".to_string()), + }); + } + Err(err) => checks.push(SmokeCheck { + name: name.to_string(), + method: "GET".to_string(), + path: path.to_string(), + expected_status: 200, + actual_status: None, + passed: false, + error: Some(redact_error(&err.to_string())), + }), + } +} + fn record_notary_evaluation_check( checks: &mut Vec, base_url: &str, @@ -4523,7 +4721,6 @@ workflows: "README.md", ".gitignore", "relay/config.yaml", - "relay/metadata.yaml", "data/benefits_casework.xlsx", "secrets/local.env", "output/.gitkeep", @@ -4535,6 +4732,38 @@ workflows: ] { assert!(project.join(path).exists(), "{path} should exist"); } + assert!(!project.join("relay/metadata.yaml").exists()); + + let config_text = fs::read_to_string(project.join("relay/config.yaml")).unwrap(); + assert!(config_text.contains("# This file is the Relay contract")); + assert!(config_text.contains("# The raw bearer keys live in secrets/local.env.")); + assert!(config_text.contains("# Tables describe the source workbook.")); + assert!(config_text.contains("# Aggregates expose predeclared grouped statistics.")); + assert!(config_text.contains("# Entities are the public API surface.")); + let config: Value = serde_yaml::from_str(&config_text).unwrap(); + let manifest: Value = + serde_yaml::from_str(&fs::read_to_string(project.join("registryctl.yaml")).unwrap()) + .unwrap(); + let compose = fs::read_to_string(project.join("compose.yaml")).unwrap(); + assert!(config.get("metadata").is_none()); + assert!(manifest["relay"].get("metadata").is_none()); + assert!(!compose.contains("metadata.yaml")); + assert_eq!( + config["datasets"][0]["aggregates"][0]["access"]["aggregate_only_execution"], + true + ); + assert_eq!( + config["datasets"][0]["aggregates"][0]["disclosure_control"]["min_group_size"], + 2 + ); + assert_eq!( + config["datasets"][0]["aggregates"][1]["access"]["aggregate_only_execution"], + true + ); + assert_eq!( + config["datasets"][0]["aggregates"][1]["disclosure_control"]["min_group_size"], + 2 + ); let readme = fs::read_to_string(project.join("README.md")).unwrap(); assert!(readme.contains("registryctl doctor --profile local --format json")); @@ -4556,15 +4785,28 @@ workflows: let request = fs::read_to_string(project.join("bruno/registry-api/Relay/Read sample people.bru")) .unwrap(); + let aggregate_request = fs::read_to_string( + project.join("bruno/registry-api/Relay/Run households by district aggregate.bru"), + ) + .unwrap(); + let application_aggregate_request = fs::read_to_string( + project.join("bruno/registry-api/Relay/Query applications aggregate.bru"), + ) + .unwrap(); let openapi_request = fs::read_to_string(project.join("bruno/registry-api/Relay/OpenAPI.bru")).unwrap(); assert!(local_bru.contains(&env_value(&env, "METADATA_READER_RAW"))); assert!(local_bru.contains(&env_value(&env, "ROW_READER_RAW"))); + assert!(local_bru.contains(&env_value(&env, "AGGREGATE_READER_RAW"))); assert!(example_bru.contains("replace-with-metadata_reader_raw")); + assert!(example_bru.contains("replace-with-aggregate_reader_raw")); assert!(!request.contains(&env_value(&env, "METADATA_READER_RAW"))); assert!(!request.contains(&env_value(&env, "ROW_READER_RAW"))); + assert!(!aggregate_request.contains(&env_value(&env, "AGGREGATE_READER_RAW"))); assert!(request.contains("{{relay_row_key}}")); + assert!(aggregate_request.contains("{{relay_aggregate_key}}")); + assert!(application_aggregate_request.contains("Data-Purpose")); assert!(!openapi_request.contains("Authorization")); assert!(!openapi_request.contains("{{relay_metadata_key}}")); } @@ -4927,7 +5169,9 @@ workflows: "ghcr.io/registrystack/registry-relay", ); assert_eq!(manifest["runtime"]["relay_base_url"], RELAY_BASE_URL); + assert!(manifest["relay"].get("metadata").is_none()); assert!(compose.contains(&format!("image: {RELAY_IMAGE}"))); + assert!(!compose.contains("metadata.yaml")); assert!(!compose.contains("registry-relay:snapshot")); assert!(!compose.contains("registry-relay:latest")); } @@ -5050,6 +5294,8 @@ workflows: "bruno/registry-api/environments/local.example.bru", "bruno/registry-api/Relay/List datasets.bru", "bruno/registry-api/Relay/Read sample people.bru", + "bruno/registry-api/Relay/Read approved applications.bru", + "bruno/registry-api/Relay/List aggregates.bru", "bruno/registry-api/Notary/List claims.bru", "bruno/registry-api/Notary/Evaluate person exists.bru", ] { @@ -5362,7 +5608,6 @@ workflows: "compose.yaml", "README.md", "relay/config.yaml", - "relay/metadata.yaml", ] { let contents = fs::read_to_string(project.join(path)).unwrap(); for secret in &secrets { @@ -5385,7 +5630,9 @@ workflows: let lossy = String::from_utf8_lossy(&workbook); assert!(lossy.contains("Households")); assert!(lossy.contains("Persons")); + assert!(lossy.contains("Applications")); assert!(lossy.contains("hh-1001")); + assert!(lossy.contains("app-3001")); } #[test] @@ -5506,12 +5753,7 @@ workflows: ) ); let rendered = fs::read_to_string(&doctor_config).unwrap(); - assert!(rendered.contains( - &project_dir - .join("relay/metadata.yaml") - .display() - .to_string() - )); + assert!(!rendered.contains("metadata.yaml")); assert!(rendered.contains( &project_dir .join("data/benefits_casework.xlsx") diff --git a/crates/registryctl/src/sample.rs b/crates/registryctl/src/sample.rs index a3dd75ae..5bf4a16e 100644 --- a/crates/registryctl/src/sample.rs +++ b/crates/registryctl/src/sample.rs @@ -10,18 +10,495 @@ pub enum Sample { Benefits, } +#[derive(Clone, Copy)] +enum Cell { + Text(&'static str), + Integer(i64), + Bool(bool), +} + pub fn write_benefits_workbook(path: &Path) -> Result<()> { + let households_sheet = sheet_xml(&[ + &[ + Cell::Text("household_id"), + Cell::Text("district"), + Cell::Text("ward"), + Cell::Text("poverty_band"), + Cell::Text("household_status"), + Cell::Text("registered_on"), + Cell::Text("declared_member_count"), + Cell::Text("address_line"), + ], + &[ + Cell::Text("hh-1001"), + Cell::Text("south"), + Cell::Text("ward_7"), + Cell::Text("band_1"), + Cell::Text("active"), + Cell::Text("2024-01-12"), + Cell::Integer(4), + Cell::Text("595 River Rd, Southvale"), + ], + &[ + Cell::Text("hh-1002"), + Cell::Text("north"), + Cell::Text("ward_2"), + Cell::Text("band_3"), + Cell::Text("active"), + Cell::Text("2023-11-03"), + Cell::Integer(2), + Cell::Text("524 Hill St, Northvale"), + ], + &[ + Cell::Text("hh-1003"), + Cell::Text("east"), + Cell::Text("ward_5"), + Cell::Text("band_2"), + Cell::Text("review_hold"), + Cell::Text("2024-02-18"), + Cell::Integer(3), + Cell::Text("81 Market Ln, Eastport"), + ], + &[ + Cell::Text("hh-1004"), + Cell::Text("west"), + Cell::Text("ward_1"), + Cell::Text("band_1"), + Cell::Text("active"), + Cell::Text("2022-08-30"), + Cell::Integer(1), + Cell::Text("140 Lakeside Ave, Westhaven"), + ], + &[ + Cell::Text("hh-1005"), + Cell::Text("south"), + Cell::Text("ward_8"), + Cell::Text("band_2"), + Cell::Text("closed"), + Cell::Text("2021-04-22"), + Cell::Integer(1), + Cell::Text("22 Orchard Ct, Southvale"), + ], + &[ + Cell::Text("hh-1006"), + Cell::Text("north"), + Cell::Text("ward_3"), + Cell::Text("band_1"), + Cell::Text("active"), + Cell::Text("2024-05-09"), + Cell::Integer(1), + Cell::Text("9 Cedar Loop, Northvale"), + ], + ]); + let persons_sheet = sheet_xml(&[ + &[ + Cell::Text("person_id"), + Cell::Text("household_id"), + Cell::Text("given_name"), + Cell::Text("family_name"), + Cell::Text("date_of_birth"), + Cell::Text("age_band"), + Cell::Text("relationship_to_head"), + Cell::Text("registration_status"), + Cell::Text("eligibility_status"), + Cell::Text("is_primary_applicant"), + Cell::Text("national_id"), + ], + &[ + Cell::Text("per-2001"), + Cell::Text("hh-1001"), + Cell::Text("Fae"), + Cell::Text("Elm"), + Cell::Text("1989-05-14"), + Cell::Text("35-49"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-856648"), + ], + &[ + Cell::Text("per-2002"), + Cell::Text("hh-1001"), + Cell::Text("Jo"), + Cell::Text("Elm"), + Cell::Text("2019-02-03"), + Cell::Text("5-17"), + Cell::Text("child"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(false), + Cell::Text("FAKE-806707"), + ], + &[ + Cell::Text("per-2003"), + Cell::Text("hh-1001"), + Cell::Text("Kai"), + Cell::Text("Elm"), + Cell::Text("1954-09-21"), + Cell::Text("65+"), + Cell::Text("parent"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(false), + Cell::Text("FAKE-219346"), + ], + &[ + Cell::Text("per-2004"), + Cell::Text("hh-1001"), + Cell::Text("Mina"), + Cell::Text("Elm"), + Cell::Text("1991-11-10"), + Cell::Text("35-49"), + Cell::Text("spouse"), + Cell::Text("active"), + Cell::Text("pending_review"), + Cell::Bool(false), + Cell::Text("FAKE-331902"), + ], + &[ + Cell::Text("per-2005"), + Cell::Text("hh-1002"), + Cell::Text("Dee"), + Cell::Text("Iron"), + Cell::Text("1984-01-28"), + Cell::Text("35-49"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-748201"), + ], + &[ + Cell::Text("per-2006"), + Cell::Text("hh-1002"), + Cell::Text("Ari"), + Cell::Text("Iron"), + Cell::Text("2016-07-18"), + Cell::Text("5-17"), + Cell::Text("child"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(false), + Cell::Text("FAKE-671240"), + ], + &[ + Cell::Text("per-2007"), + Cell::Text("hh-1003"), + Cell::Text("Nia"), + Cell::Text("Stone"), + Cell::Text("1998-03-05"), + Cell::Text("18-34"), + Cell::Text("head"), + Cell::Text("pending"), + Cell::Text("pending_review"), + Cell::Bool(true), + Cell::Text("FAKE-503118"), + ], + &[ + Cell::Text("per-2008"), + Cell::Text("hh-1003"), + Cell::Text("Sol"), + Cell::Text("Stone"), + Cell::Text("2022-12-12"), + Cell::Text("0-4"), + Cell::Text("child"), + Cell::Text("pending"), + Cell::Text("pending_review"), + Cell::Bool(false), + Cell::Text("FAKE-663910"), + ], + &[ + Cell::Text("per-2009"), + Cell::Text("hh-1003"), + Cell::Text("Ren"), + Cell::Text("Stone"), + Cell::Text("1970-06-30"), + Cell::Text("50-64"), + Cell::Text("parent"), + Cell::Text("active"), + Cell::Text("ineligible"), + Cell::Bool(false), + Cell::Text("FAKE-447120"), + ], + &[ + Cell::Text("per-2010"), + Cell::Text("hh-1004"), + Cell::Text("Ivo"), + Cell::Text("Reed"), + Cell::Text("1957-04-02"), + Cell::Text("65+"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-990231"), + ], + &[ + Cell::Text("per-2011"), + Cell::Text("hh-1005"), + Cell::Text("Uma"), + Cell::Text("Vale"), + Cell::Text("1993-08-16"), + Cell::Text("18-34"), + Cell::Text("head"), + Cell::Text("closed"), + Cell::Text("ineligible"), + Cell::Bool(true), + Cell::Text("FAKE-125904"), + ], + &[ + Cell::Text("per-2012"), + Cell::Text("hh-1006"), + Cell::Text("Lina"), + Cell::Text("Moss"), + Cell::Text("1982-10-25"), + Cell::Text("35-49"), + Cell::Text("head"), + Cell::Text("active"), + Cell::Text("eligible"), + Cell::Bool(true), + Cell::Text("FAKE-775120"), + ], + ]); + let applications_sheet = sheet_xml(&[ + &[ + Cell::Text("application_id"), + Cell::Text("household_id"), + Cell::Text("applicant_person_id"), + Cell::Text("program"), + Cell::Text("application_date"), + Cell::Text("intake_channel"), + Cell::Text("office_code"), + Cell::Text("application_status"), + Cell::Text("decision"), + Cell::Text("benefit_level"), + Cell::Text("review_due_on"), + Cell::Text("identity_verified"), + Cell::Text("residence_verified"), + Cell::Text("consent_reference"), + ], + &[ + Cell::Text("app-3001"), + Cell::Text("hh-1001"), + Cell::Text("per-2001"), + Cell::Text("cash_transfer"), + Cell::Text("2024-01-20"), + Cell::Text("office"), + Cell::Text("SOUTH-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("enhanced"), + Cell::Text("2026-01-20"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9001"), + ], + &[ + Cell::Text("app-3002"), + Cell::Text("hh-1002"), + Cell::Text("per-2005"), + Cell::Text("food_support"), + Cell::Text("2024-02-10"), + Cell::Text("mobile_team"), + Cell::Text("NORTH-02"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2025-08-10"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9002"), + ], + &[ + Cell::Text("app-3003"), + Cell::Text("hh-1003"), + Cell::Text("per-2007"), + Cell::Text("cash_transfer"), + Cell::Text("2024-03-05"), + Cell::Text("partner_referral"), + Cell::Text("EAST-01"), + Cell::Text("under_review"), + Cell::Text("pending_review"), + Cell::Text("none"), + Cell::Text("2024-06-30"), + Cell::Bool(true), + Cell::Bool(false), + Cell::Text("consent-9003"), + ], + &[ + Cell::Text("app-3004"), + Cell::Text("hh-1004"), + Cell::Text("per-2010"), + Cell::Text("disability_support"), + Cell::Text("2023-09-15"), + Cell::Text("office"), + Cell::Text("WEST-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("enhanced"), + Cell::Text("2025-09-15"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9004"), + ], + &[ + Cell::Text("app-3005"), + Cell::Text("hh-1005"), + Cell::Text("per-2011"), + Cell::Text("emergency_grant"), + Cell::Text("2023-04-25"), + Cell::Text("online"), + Cell::Text("SOUTH-01"), + Cell::Text("closed"), + Cell::Text("ineligible"), + Cell::Text("none"), + Cell::Text("2023-07-25"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9005"), + ], + &[ + Cell::Text("app-3006"), + Cell::Text("hh-1006"), + Cell::Text("per-2012"), + Cell::Text("cash_transfer"), + Cell::Text("2024-05-12"), + Cell::Text("office"), + Cell::Text("NORTH-02"), + Cell::Text("submitted"), + Cell::Text("pending_review"), + Cell::Text("none"), + Cell::Text("2024-08-12"), + Cell::Bool(false), + Cell::Bool(true), + Cell::Text("consent-9006"), + ], + &[ + Cell::Text("app-3007"), + Cell::Text("hh-1001"), + Cell::Text("per-2001"), + Cell::Text("school_meals"), + Cell::Text("2024-06-01"), + Cell::Text("office"), + Cell::Text("SOUTH-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2026-06-01"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9007"), + ], + &[ + Cell::Text("app-3008"), + Cell::Text("hh-1002"), + Cell::Text("per-2005"), + Cell::Text("cash_transfer"), + Cell::Text("2024-06-15"), + Cell::Text("mobile_team"), + Cell::Text("NORTH-02"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2026-06-15"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9008"), + ], + &[ + Cell::Text("app-3009"), + Cell::Text("hh-1003"), + Cell::Text("per-2007"), + Cell::Text("food_support"), + Cell::Text("2024-06-18"), + Cell::Text("partner_referral"), + Cell::Text("EAST-01"), + Cell::Text("approved"), + Cell::Text("eligible"), + Cell::Text("standard"), + Cell::Text("2026-06-18"), + Cell::Bool(true), + Cell::Bool(true), + Cell::Text("consent-9009"), + ], + ]); + let entries = [ ZipEntry::new("[Content_Types].xml", CONTENT_TYPES.as_bytes()), ZipEntry::new("_rels/.rels", ROOT_RELS.as_bytes()), ZipEntry::new("xl/workbook.xml", WORKBOOK.as_bytes()), ZipEntry::new("xl/_rels/workbook.xml.rels", WORKBOOK_RELS.as_bytes()), - ZipEntry::new("xl/worksheets/sheet1.xml", HOUSEHOLDS_SHEET.as_bytes()), - ZipEntry::new("xl/worksheets/sheet2.xml", PERSONS_SHEET.as_bytes()), + ZipEntry::new("xl/worksheets/sheet1.xml", households_sheet.as_bytes()), + ZipEntry::new("xl/worksheets/sheet2.xml", persons_sheet.as_bytes()), + ZipEntry::new("xl/worksheets/sheet3.xml", applications_sheet.as_bytes()), ]; ZipStoreWriter::write(path, &entries) } +fn sheet_xml(rows: &[&[Cell]]) -> String { + let mut xml = String::from( + r#" + + +"#, + ); + for (row_idx, row) in rows.iter().enumerate() { + let row_number = row_idx + 1; + xml.push_str(&format!(" \n")); + for (col_idx, cell) in row.iter().enumerate() { + let reference = cell_reference(col_idx, row_number); + write_cell(&mut xml, &reference, *cell); + } + xml.push_str(" \n"); + } + xml.push_str(" \n"); + xml +} + +fn write_cell(xml: &mut String, reference: &str, cell: Cell) { + match cell { + Cell::Text(value) => { + xml.push_str(&format!( + " {}\n", + escape_xml(value) + )); + } + Cell::Integer(value) => { + xml.push_str(&format!(" {value}\n")); + } + Cell::Bool(value) => { + let value = if value { 1 } else { 0 }; + xml.push_str(&format!( + " {value}\n" + )); + } + } +} + +fn cell_reference(mut col_idx: usize, row_number: usize) -> String { + let mut letters = Vec::new(); + loop { + let remainder = col_idx % 26; + letters.push((b'A' + remainder as u8) as char); + col_idx /= 26; + if col_idx == 0 { + break; + } + col_idx -= 1; + } + letters.iter().rev().collect::() + &row_number.to_string() +} + +fn escape_xml(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + const CONTENT_TYPES: &str = r#" @@ -29,6 +506,7 @@ const CONTENT_TYPES: &str = r#" + "#; const ROOT_RELS: &str = r#" @@ -41,6 +519,7 @@ const WORKBOOK: &str = r#" + "#; @@ -48,66 +527,5 @@ const WORKBOOK_RELS: &str = r#" + "#; - -const HOUSEHOLDS_SHEET: &str = r#" - - - - household_id - district - poverty_band - address_line - - - hh-1001 - south - band_1 - 595 Fake St, southvale - - - hh-1002 - north - band_3 - 524 Fake St, northvale - - -"#; - -const PERSONS_SHEET: &str = r#" - - - - person_id - household_id - age_band - eligibility_status - full_name - national_id - - - per-2001 - hh-1001 - 0-4 - eligible - Fae Elm - FAKE-856648 - - - per-2002 - hh-1001 - 65+ - eligible - Jo Apple - FAKE-806707 - - - per-2003 - hh-1002 - 18-64 - eligible - Dee Iron - FAKE-219346 - - -"#; diff --git a/crates/registryctl/src/templates/compose-with-notary.yaml b/crates/registryctl/src/templates/compose-with-notary.yaml index 834a0a8a..968ffea6 100644 --- a/crates/registryctl/src/templates/compose-with-notary.yaml +++ b/crates/registryctl/src/templates/compose-with-notary.yaml @@ -8,7 +8,6 @@ services: - "4242:8080" volumes: - ./relay/config.yaml:/etc/registry-relay/config.yaml:ro - - ./relay/metadata.yaml:/etc/registry-relay/metadata.yaml:ro - ./data:/var/lib/registry-relay/data:ro registry-notary: diff --git a/crates/registryctl/src/templates/compose.yaml b/crates/registryctl/src/templates/compose.yaml index 63fd3cf4..1f79ac6b 100644 --- a/crates/registryctl/src/templates/compose.yaml +++ b/crates/registryctl/src/templates/compose.yaml @@ -8,5 +8,4 @@ services: - "4242:8080" volumes: - ./relay/config.yaml:/etc/registry-relay/config.yaml:ro - - ./relay/metadata.yaml:/etc/registry-relay/metadata.yaml:ro - ./data:/var/lib/registry-relay/data:ro diff --git a/crates/registryctl/src/templates/notary_project_readme.md b/crates/registryctl/src/templates/notary_project_readme.md index a9734a92..95ff0cff 100644 --- a/crates/registryctl/src/templates/notary_project_readme.md +++ b/crates/registryctl/src/templates/notary_project_readme.md @@ -5,10 +5,23 @@ This project was generated by `registryctl`. ## Start ```sh -registryctl doctor --profile local --format json registryctl start registryctl notary smoke +``` + +Expected local URL: + +```text +Notary API: http://127.0.0.1:4255 +API docs: http://127.0.0.1:4255/docs +``` + +## Inspect + +```sh registryctl notary open +sed -n '1,220p' notary/config.yaml +registryctl doctor --profile local --format json ``` The generated local demo credentials live in `secrets/local.env`. They are for diff --git a/crates/registryctl/src/templates/project_readme.md b/crates/registryctl/src/templates/project_readme.md index 10049304..9020aea3 100644 --- a/crates/registryctl/src/templates/project_readme.md +++ b/crates/registryctl/src/templates/project_readme.md @@ -5,10 +5,40 @@ This project was generated by `registryctl`. ## Start ```sh -registryctl doctor --profile local --format json registryctl start registryctl smoke +``` + +Expected local URL: + +```text +Relay API: http://127.0.0.1:4242 +API docs: http://127.0.0.1:4242/docs +``` + +The sample workbook at `data/benefits_casework.xlsx` has three sheets: +`Households`, `Persons`, and `Applications`. + +## Try one protected read + +```sh +set -a +. secrets/local.env +set +a + +curl -sS -G \ + -H "Authorization: Bearer $ROW_READER_RAW" \ + -H "Data-Purpose: https://example.local/purpose/tutorial" \ + --data-urlencode "household_id=hh-1001" \ + http://127.0.0.1:4242/v1/datasets/benefits_casework/entities/person/records +``` + +## Inspect + +```sh registryctl open +sed -n '1,180p' relay/config.yaml +registryctl doctor --profile local --format json ``` The generated local demo credentials live in `secrets/local.env`. They are for diff --git a/crates/registryctl/src/templates/relay_config.yaml.tmpl b/crates/registryctl/src/templates/relay_config.yaml.tmpl index df8902a4..fc7fefc2 100644 --- a/crates/registryctl/src/templates/relay_config.yaml.tmpl +++ b/crates/registryctl/src/templates/relay_config.yaml.tmpl @@ -1,17 +1,23 @@ # Generated by registryctl. +# This file is the Relay contract for the local sample: service metadata, +# API-key auth, source spreadsheet tables, aggregates, and public entities. +# Edit tables, entities, and aggregates when your workbook or API shape changes. + +# server controls the listener inside the container. The generated compose file +# maps it to http://127.0.0.1:4242 on your workstation. server: bind: 0.0.0.0:8080 openapi_requires_auth: false -metadata: - source: - path: /etc/registry-relay/metadata.yaml - +# catalog values appear in dataset discovery, metadata responses, and OpenAPI. catalog: title: Benefits Casework Gateway (registryctl sample) base_url: http://127.0.0.1:4242 publisher: Registryctl Local Sample +# The raw bearer keys live in secrets/local.env. Public config keeps only the +# fingerprint env var names, so it can be shared without leaking credentials. +# If you rotate a generated key, update the raw key and fingerprint together. auth: mode: api_key api_keys: @@ -38,11 +44,14 @@ auth: - benefits_casework:metadata - benefits_casework:aggregate +# Audit entries are emitted to container stdout as JSON Lines. The hash secret +# lets Relay pseudonymize sensitive audit values without storing the secret here. audit: sink: stdout format: jsonl hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET +# A dataset is the registry product exposed under /v1/datasets/. datasets: - id: benefits_casework title: Benefits Casework @@ -55,6 +64,9 @@ datasets: refresh: mode: manual + # Tables describe the source workbook. The sample uses one Excel file with + # three sheets; primary_key names the source column that uniquely identifies + # each row. tables: - id: households_table source: @@ -65,6 +77,9 @@ datasets: sheet: Households primary_key: household_id schema: + # strict catches workbook drift by rejecting unexpected source + # columns. Mark source fields sensitive when they contain personal + # data. The sample maps only the public-safe subset into entities below. strict: true fields: - name: household_id @@ -73,9 +88,21 @@ datasets: - name: district type: string nullable: false + - name: ward + type: string + nullable: false - name: poverty_band type: string nullable: false + - name: household_status + type: string + nullable: false + - name: registered_on + type: date + nullable: false + - name: declared_member_count + type: integer + nullable: false - name: address_line type: string nullable: true @@ -98,21 +125,153 @@ datasets: - name: household_id type: string nullable: false + - name: given_name + type: string + nullable: false + sensitive: true + - name: family_name + type: string + nullable: false + sensitive: true + - name: date_of_birth + type: date + nullable: false + sensitive: true - name: age_band type: string nullable: false + - name: relationship_to_head + type: string + nullable: false + - name: registration_status + type: string + nullable: false - name: eligibility_status type: string nullable: false - - name: full_name + - name: is_primary_applicant + type: boolean + nullable: false + - name: national_id type: string nullable: true sensitive: true - - name: national_id + + - id: applications_table + source: + type: file + path: /var/lib/registry-relay/data/benefits_casework.xlsx + format: + xlsx: + sheet: Applications + primary_key: application_id + schema: + strict: true + fields: + - name: application_id type: string - nullable: true + nullable: false + - name: household_id + type: string + nullable: false + - name: applicant_person_id + type: string + nullable: false + - name: program + type: string + nullable: false + - name: application_date + type: date + nullable: false + - name: intake_channel + type: string + nullable: false + - name: office_code + type: string + nullable: false + - name: application_status + type: string + nullable: false + - name: decision + type: string + nullable: false + - name: benefit_level + type: string + nullable: false + - name: review_due_on + type: date + nullable: false + - name: identity_verified + type: boolean + nullable: false + - name: residence_verified + type: boolean + nullable: false + - name: consent_reference + type: string + nullable: false sensitive: true + # Aggregates expose predeclared grouped statistics. aggregate_only_execution + # allows aggregate readers to run these definitions without granting row + # access. disclosure_control omits small groups from the response. + aggregates: + - id: by_district + title: Households by district + description: Count of households by district + source_entity: household + access: + aggregate_only_execution: true + default_group_by: + - district + dimensions: + - id: district + label: District + field: district + indicators: + - id: household_count + label: Households + function: count + column: id + unit_measure: households + disclosure_control: + min_group_size: 2 + suppression: omit + + - id: applications_by_program_status + title: Applications by program and status + description: Count of applications by program and application status + source_entity: application + access: + aggregate_only_execution: true + default_group_by: + - program + - application_status + dimensions: + - id: program + label: Program + field: program + - id: application_status + label: Application status + field: application_status + indicators: + - id: application_count + label: Applications + function: count + column: id + unit_measure: applications + allowed_filters: + - field: program + ops: [eq, in] + - field: application_status + ops: [eq, in] + disclosure_control: + min_group_size: 2 + suppression: omit + + # Entities are the public API surface. They choose which table fields are + # returned, define relationships for expand=..., attach access scopes, and + # restrict filters so collection reads stay intentional. entities: - name: household title: Household @@ -123,13 +282,25 @@ datasets: from: household_id - name: district from: district + - name: ward + from: ward - name: poverty_band from: poverty_band + - name: household_status + from: household_status + - name: registered_on + from: registered_on + - name: declared_member_count + from: declared_member_count relationships: - name: members kind: has_many target: person foreign_key: household_id + - name: applications + kind: has_many + target: application + foreign_key: household_id access: metadata_scope: benefits_casework:metadata aggregate_scope: benefits_casework:aggregate @@ -144,19 +315,9 @@ datasets: ops: [eq, in] - field: poverty_band ops: [eq, in] - allowed_expansions: [members] - aggregates: - - id: by_district - description: Count of households by district - group_by: - - district - measures: - - name: household_count - function: count - column: id - disclosure_control: - min_group_size: 1 - suppression: omit + - field: household_status + ops: [eq, in] + allowed_expansions: [members, applications] - name: person title: Person @@ -169,13 +330,23 @@ datasets: from: household_id - name: age_band from: age_band + - name: relationship_to_head + from: relationship_to_head + - name: registration_status + from: registration_status - name: eligibility_status from: eligibility_status + - name: is_primary_applicant + from: is_primary_applicant relationships: - name: household kind: belongs_to target: household foreign_key: household_id + - name: applications + kind: has_many + target: application + foreign_key: applicant_person_id access: metadata_scope: benefits_casework:metadata aggregate_scope: benefits_casework:aggregate @@ -184,13 +355,6 @@ datasets: default_limit: 100 max_limit: 1000 require_purpose_header: true - required_filters: - - id - - household_id - - eligibility_status - required_filter_bindings: - - field: id - source: principal_id allowed_filters: - field: id ops: [eq, in] @@ -198,4 +362,73 @@ datasets: ops: [eq, in] - field: eligibility_status ops: [eq, in] - allowed_expansions: [household] + - field: registration_status + ops: [eq, in] + allowed_expansions: [household, applications] + + - name: application + title: Application + description: A synthetic benefit application + table: applications_table + fields: + - name: id + from: application_id + - name: household_id + from: household_id + - name: applicant_person_id + from: applicant_person_id + - name: program + from: program + - name: application_date + from: application_date + - name: intake_channel + from: intake_channel + - name: office_code + from: office_code + - name: application_status + from: application_status + - name: decision + from: decision + - name: benefit_level + from: benefit_level + - name: review_due_on + from: review_due_on + - name: identity_verified + from: identity_verified + - name: residence_verified + from: residence_verified + relationships: + - name: household + kind: belongs_to + target: household + foreign_key: household_id + - name: applicant + kind: belongs_to + target: person + foreign_key: applicant_person_id + access: + metadata_scope: benefits_casework:metadata + aggregate_scope: benefits_casework:aggregate + read_scope: benefits_casework:rows + api: + default_limit: 100 + max_limit: 1000 + require_purpose_header: true + allowed_filters: + - field: id + ops: [eq, in] + - field: household_id + ops: [eq, in] + - field: applicant_person_id + ops: [eq, in] + - field: program + ops: [eq, in] + - field: application_status + ops: [eq, in] + - field: decision + ops: [eq, in] + - field: benefit_level + ops: [eq, in] + - field: review_due_on + ops: [gte, lte, between] + allowed_expansions: [household, applicant] diff --git a/crates/registryctl/src/templates/relay_metadata.yaml b/crates/registryctl/src/templates/relay_metadata.yaml deleted file mode 100644 index 8504ec48..00000000 --- a/crates/registryctl/src/templates/relay_metadata.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by registryctl. -schema_version: registry-manifest/v1 -catalog: - id: registryctl-benefits-sample - base_url: http://127.0.0.1:4242 - title: Benefits Casework Gateway (registryctl sample) - publisher: - name: Registryctl Local Sample - standards: - dcat: '3.0' - shacl: '1.1' - json_schema: 2020-12 -datasets: - - id: benefits_casework - title: Benefits Casework - description: Synthetic benefits casework registry for local registryctl evaluation. - owner: Registryctl Local Sample - sensitivity: personal - access_rights: restricted - update_frequency: monthly - entities: - - name: household - title: Household - description: A synthetic benefits household - identifiers: - - name: id - kind: primary - fields: - - name: id - type: string - required: true - - name: district - type: string - required: true - - name: poverty_band - type: string - required: true - relationships: - - name: members - target_entity: person - cardinality: many - - name: person - title: Person - description: A synthetic person in a benefits household - identifiers: - - name: id - kind: primary - fields: - - name: id - type: string - required: true - - name: household_id - type: string - required: true - - name: age_band - type: string - required: true - - name: eligibility_status - type: string - required: true - relationships: - - name: household - target_entity: household - cardinality: zero_or_one diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index fa60de1d..79f44582 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -67,8 +67,8 @@ export default defineConfig({ // quickstart's "Choose by question" router merged into the homepage (2026-06). '/start/quickstart/': internalRedirect('/'), '/start/your-first-call/': internalRedirect('/tutorials/first-run-with-registry-lab/'), - // verify-claim-own-api merged into the claim-verification tutorial (2026-06). - '/tutorials/verify-claim-own-api/': internalRedirect('/tutorials/verify-claim-registry-api/'), + // verify-claim-own-api moved into the Apply to your stack path (2026-06). + '/tutorials/verify-claim-own-api/': internalRedirect('/tutorials/run-notary-standalone-for-api/'), '/tutorials/verify-opencrvs-dci-claims/': internalRedirect('/tutorials/verify-opencrvs-claims/'), // Problems -> marketing /why '/problems/': `${marketing}/why/`, @@ -204,8 +204,9 @@ export default defineConfig({ // // "Get started" is orientation only: Overview (which carries the // "Choose by question" router), the zero-install demo, and the - // evaluation page. The hands-on pages live under Tutorials, ordered by - // weight: the lightest local run first, the full multi-service lab last. + // evaluation page. The core Tutorials path stays on one generated local + // project; operator paths live under Apply to your stack, and named + // source-system paths live under Integrate existing systems. sidebar: [ { label: 'Get started', @@ -220,11 +221,23 @@ export default defineConfig({ { label: 'Tutorials', items: [ - { label: 'Publish a spreadsheet', slug: 'tutorials/publish-spreadsheet-secured-registry-api' }, - { label: 'Verify a claim', slug: 'tutorials/verify-claim-registry-api' }, - { label: 'OpenCRVS claims', slug: 'tutorials/verify-opencrvs-claims' }, + { label: 'Run a protected API', slug: 'tutorials/publish-spreadsheet-secured-registry-api' }, + { label: 'Evaluate a claim', slug: 'tutorials/verify-claim-registry-api' }, + ], + }, + { + label: 'Apply to your stack', + items: [ + { label: 'Notary for a Registry Data API', slug: 'tutorials/run-notary-standalone-for-api' }, { label: 'Deploy with own data', slug: 'tutorials/deploy-standalone-with-own-data' }, { label: 'Run the lab', slug: 'tutorials/first-run-with-registry-lab' }, + ], + }, + { + label: 'Integrate existing systems', + items: [ + { label: 'OpenCRVS claims', slug: 'tutorials/verify-opencrvs-claims' }, + { label: 'DHIS2 claim checks', slug: 'tutorials/configure-dhis2-claim-checks' }, { label: 'FHIR evidence', slug: 'tutorials/getting-started-fhir-evidence' }, ], }, diff --git a/docs/site/scripts/check-tutorial.sh b/docs/site/scripts/check-tutorial.sh index eff1a67d..b5cde043 100755 --- a/docs/site/scripts/check-tutorial.sh +++ b/docs/site/scripts/check-tutorial.sh @@ -150,8 +150,8 @@ done # inside `sh` fences. Bump the expected count when you intentionally add or # remove a documented command. REGISTRYCTL_TUTORIALS=( - "publish-spreadsheet-secured-registry-api:31" - "verify-claim-registry-api:74" + "publish-spreadsheet-secured-registry-api:29" + "verify-claim-registry-api:55" ) count_sh_command_lines() { diff --git a/docs/site/src/components/HomeLanding.astro b/docs/site/src/components/HomeLanding.astro index 5d19dbac..3719c488 100644 --- a/docs/site/src/components/HomeLanding.astro +++ b/docs/site/src/components/HomeLanding.astro @@ -51,10 +51,10 @@ const cards: RouteCard[] = [ id: 'try-locally', question: 'How do I try it on my own machine?', start: { - label: 'Publish a spreadsheet as a secured registry API', + label: 'Run a protected registry API locally', href: '/tutorials/publish-spreadsheet-secured-registry-api/', }, - then: [{ label: 'Verify a claim with Registry Notary', href: '/tutorials/verify-claim-registry-api/' }], + then: [{ label: 'Evaluate a claim with Registry Notary', href: '/tutorials/verify-claim-registry-api/' }], }, { id: 'full-demo', @@ -64,10 +64,16 @@ const cards: RouteCard[] = [ }, { id: 'fhir', - question: 'How do I try claim checks from FHIR-shaped health data?', + question: 'How do I integrate FHIR-shaped health data?', start: { label: 'Getting started with FHIR evidence', href: '/tutorials/getting-started-fhir-evidence/' }, then: [{ label: 'Integration patterns', href: '/explanation/integration-patterns/' }], }, + { + id: 'dhis2', + question: 'How do I configure claim checks from DHIS2?', + start: { label: 'Configure DHIS2 claim checks', href: '/tutorials/configure-dhis2-claim-checks/' }, + then: [{ label: 'Integration patterns', href: '/explanation/integration-patterns/' }], + }, { id: 'own-data', question: 'How do I deploy Relay against my own data, not a sample?', @@ -79,16 +85,16 @@ const cards: RouteCard[] = [ }, { id: 'existing-api', - question: 'I already have an API. How do I verify claims from it?', + question: 'I already have a Registry Data API source. How do I evaluate claims from it?', start: { - label: 'Run Notary standalone for an API you operate', - href: '/tutorials/verify-claim-registry-api/#run-notary-standalone-for-an-api-you-operate', + label: 'Connect Notary to a Registry Data API source', + href: '/tutorials/run-notary-standalone-for-api/', }, then: [{ label: 'Registry Notary', href: '/products/registry-notary/' }], }, { id: 'opencrvs', - question: 'How do I verify claims from OpenCRVS?', + question: 'How do I integrate OpenCRVS claim checks?', start: { label: 'Verify OpenCRVS claims with registryctl', href: '/tutorials/verify-opencrvs-claims/' }, then: [ { diff --git a/docs/site/src/content/docs/explanation/integration-patterns.mdx b/docs/site/src/content/docs/explanation/integration-patterns.mdx index eaec76db..bef3cc6e 100644 --- a/docs/site/src/content/docs/explanation/integration-patterns.mdx +++ b/docs/site/src/content/docs/explanation/integration-patterns.mdx @@ -74,8 +74,8 @@ Wire Registry Stack in alongside a domain platform when: ## Workflow engines -Examples: [OpenFn](https://www.openfn.org/) (cross-system orchestration popular in humanitarian and -government data flows), [Camunda](https://camunda.com/), [Flowable](https://www.flowable.com/). +Examples: [Camunda](https://camunda.com/), [Flowable](https://www.flowable.com/), and other +casework or process orchestration engines. Workflow engines own process state, task assignment, timers, branching, retries, escalation, and history. @@ -90,8 +90,8 @@ Wire Registry Stack in alongside a workflow engine when: ## Exchange layers -Examples: [X-Road](https://x-road.global/), [OpenFn](https://www.openfn.org/) (when used as the -cross-institution data flow), GovStack-style reference exchanges. +Examples: [X-Road](https://x-road.global/), GovStack-style reference exchanges, and +country-specific interoperability layers. Exchange layers own participant onboarding, message transport, mutual trust, routing, addressing, and cross-institution policy. diff --git a/docs/site/src/content/docs/reference/apis/registry-notary.mdx b/docs/site/src/content/docs/reference/apis/registry-notary.mdx index 661159d2..0c7149b5 100644 --- a/docs/site/src/content/docs/reference/apis/registry-notary.mdx +++ b/docs/site/src/content/docs/reference/apis/registry-notary.mdx @@ -93,10 +93,8 @@ under `source_connections`: - **SP-DCI** connector sends DCI-shaped search requests and parses the response using configured field paths. - **Source adapter sidecar** connector calls a private sidecar over HTTP, using a static - bearer token, for sources reached through the built-in `http_json`, `http_flow`, or `fhir` - engines. (Note: OpenFn as an external caller, where OpenFn workflows call Registry Notary, remains - supported; what is retired is the OpenFn Node worker pool as a source-reading engine inside the - sidecar.) + bearer token, for sources reached through the built-in `http_json`, `http_flow`, `fhir`, or + `script_rhai` engines. There is no generic file-based or database connector; source connectors must reach their targets over HTTP. @@ -113,9 +111,9 @@ path cannot affect another. - **Holder binding.** `did:jwk` is the only supported proof-of-possession binding method. - **Audit events.** Every evaluated request emits an `EvidenceAuditEvent` in a Registry Platform audit envelope to a configured sink (stdout, file, or JSONL). -- **Demo helpers.** Demo OpenFn snippets and generated workflow helpers are examples, not production - replay-protection profiles. Add freshness, expiry, or nonce checks before copying a helper into a - production workflow. +- **Demo helpers.** Generated workflow helpers are examples, not production replay-protection + profiles. Add freshness, expiry, or nonce checks before copying a helper into a production + workflow. :::caution Audit write failures surface as request errors, not as silent log entries. If the audit sink is diff --git a/docs/site/src/content/docs/reference/environment-variables.mdx b/docs/site/src/content/docs/reference/environment-variables.mdx index d621ac5a..5d932e76 100644 --- a/docs/site/src/content/docs/reference/environment-variables.mdx +++ b/docs/site/src/content/docs/reference/environment-variables.mdx @@ -47,7 +47,7 @@ The audit hash secret and other secret material are read from operator-named var ## registryctl -registryctl reads the variables below. It also reads operator-provided source and credential variables whose names are passed to `registryctl openfn` and `registryctl init notary` flags; those are not fixed names. +registryctl reads the variables below. It also reads operator-provided source and credential variables whose names are passed to `registryctl init notary` and legacy compatibility flags; those are not fixed names. | Name | Purpose | Default or required | | --- | --- | --- | @@ -56,14 +56,14 @@ registryctl reads the variables below. It also reads operator-provided source an | `REGISTRYCTL_VERSION` | Pinned release the installer downloads. Read by the install script, not the running binary. | Defaults to the installer's pinned release. | | `CI` | When set to a non-empty value other than `0` or `false`, disables the automatic update check. | Optional. | -registryctl also passes through operator-named source and sidecar variables. The defaults below are the variable names registryctl writes into generated projects and OpenFn snippets; the operator can override them with flags. +registryctl also passes through operator-named source and sidecar variables. The defaults below are the variable names registryctl writes into generated projects and compatibility snippets; the operator can override them with flags. | Name | Purpose | Default or required | | --- | --- | --- | | `EVIDENCE_SOURCE_API_TOKEN` | Default source API bearer token variable for a `registry-data-api` Notary starter project. | Default name; override with `init notary --source-token-env`. | | `FHIR_SIDECAR_TOKEN` | Default source token variable for a `fhir-sidecar` Notary starter project. | Default name; override with `init notary --source-token-env`. | -| `OPENFN_TOKEN` | OpenFn API token for `registryctl openfn import` URL imports. | Default name; override with `--openfn-token-env`. | -| `OPENFN_SIDECAR_TOKEN` | Raw notary-to-sidecar bearer token written into the generated snippet. | Default name; override with `--sidecar-token-env`. | +| `OPENFN_TOKEN` | Legacy OpenFn API token name for `registryctl openfn import` URL imports. | Default name; override with `--openfn-token-env`. | +| `OPENFN_SIDECAR_TOKEN` | Legacy notary-to-sidecar bearer token name written into compatibility snippets. | Default name; override with `--sidecar-token-env`. | | `DEV_SIDECAR_TOKEN_HASH` | Notary-to-sidecar bearer token hash. | Default name; override with `--auth-hash-env`. | | `REGISTRY_NOTARY_BASE_URL`, `REGISTRY_NOTARY_BEARER_TOKEN`, `REGISTRY_NOTARY_API_KEY`, `REGISTRY_NOTARY_PURPOSE` | Shell exports emitted by `registryctl lab env` for hosted-lab SDK quickstarts. | Emitted by `lab env`; these are public synthetic lab values. | diff --git a/docs/site/src/content/docs/reference/registryctl.mdx b/docs/site/src/content/docs/reference/registryctl.mdx index 6caec390..6587217b 100644 --- a/docs/site/src/content/docs/reference/registryctl.mdx +++ b/docs/site/src/content/docs/reference/registryctl.mdx @@ -89,9 +89,12 @@ Smoke commands run built-in local checks and write a JSON result file. | `notary smoke` | Run built-in local Notary smoke checks. | | `notary open` | Open or print the local Notary API docs URL. | -## OpenFn +## Legacy OpenFn conversion -`openfn` converts OpenFn workflow exports into Registry Notary OpenFn sidecar runtime files. The flags below apply to both `openfn import` and `openfn convert` unless noted. +`openfn` is a compatibility command for converting existing OpenFn workflow exports into sidecar +runtime files. Current source-adapter projects use the built-in `http_json`, `http_flow`, `fhir`, +or `script_rhai` engines instead. The flags below apply to both `openfn import` and `openfn convert` +unless noted. | Subcommand or flag | Purpose | | --- | --- | diff --git a/docs/site/src/content/docs/start/see-it-live.mdx b/docs/site/src/content/docs/start/see-it-live.mdx index ad2d6c13..0a7819c3 100644 --- a/docs/site/src/content/docs/start/see-it-live.mdx +++ b/docs/site/src/content/docs/start/see-it-live.mdx @@ -6,7 +6,7 @@ owner: registry-docs source_repos: - registry-lab - registry-registryctl -last_reviewed: "2026-06-13" +last_reviewed: "2026-06-28" doc_type: tutorial locale: en standards_referenced: @@ -21,8 +21,7 @@ will read a protected registry API from your terminal (**Registry Relay**, which authorized callers), then have **Registry Notary** issue a signed credential, delivered to a hosted demo wallet, that answers a question without exposing the record. The Relay reads are pure `curl` with zero install; the credential step is a guided browser flow using the lab's hosted wallet, so it -needs no install either; only the optional developer round-trip at the end also uses `registryctl`, -`jq`, and `node`. Everything runs in the hosted lab at +needs no install either. Everything runs in the hosted lab at [lab.registrystack.org](https://lab.registrystack.org), so the main flow has no setup on your machine. @@ -30,7 +29,7 @@ machine. outcome="One protected registry read and one signed, privacy-preserving credential, against the public hosted lab." time="About 10 minutes" level="Hosted lab, zero install" - prerequisites={['A web browser', 'Optional: curl, registryctl, jq, node']} + prerequisites={['A web browser', 'Optional: curl']} /> This lab uses synthetic data and public demo-only credentials by design. @@ -45,7 +44,7 @@ The lab runs three services that you will touch directly: - A citizen **Notary**: an issuer that hands out a privacy-preserving credential instead of the raw record, at [citizen-notary.lab.registrystack.org](https://citizen-notary.lab.registrystack.org). - A health-program **Notary** in front of a demo DHIS2 health information system, used only by the - optional developer round-trip at the end of this page, at + DHIS2 integration tutorial, at [dhis2-notary.lab.registrystack.org](https://dhis2-notary.lab.registrystack.org). For how these connect to the rest of the stack, see the @@ -187,155 +186,25 @@ refuses: it will not issue a credential for a subject you did not authenticate a be talked into vouching for someone else. For more on this service, see [Registry Notary](../../products/registry-notary/). -### Developer API round-trip +### DHIS2 integration path -For a pure API credential flow, use the hosted DHIS2 Notary: the lab runs a demo -[DHIS2](https://dhis2.org/) health information system with a Notary in front of it, so the same -issuance contract can be exercised against a second, independent source system. This path does -not use the browser wallet sign-in, and unlike the curl-only reads above it also needs -`registryctl`, `jq`, and `node` on your machine. It evaluates DHIS2 child-program claims, issues an -`application/dc+sd-jwt` credential, fetches the issuer JWKS, verifies the Ed25519 signature, and -checks that each disclosure hash is listed in the issuer-signed JWT. - -Load the hosted Notary URL, current demo bearer token, and purpose URI from the lab manifest. -The `dhis2-bearer` credential is the demo evidence-client token for this Notary. - -```sh -eval "$(registryctl lab env --credential dhis2-bearer)" -``` - -Evaluate the credential claims for the live demo tracked entity: - -```sh -cat > evaluation-request.json <<'JSON' -{ - "target": { - "type": "TrackedEntity", - "identifiers": [ - { "scheme": "dhis2_tracked_entity", "value": "PQfMcpmXeFE" } - ] - }, - "claims": [ - "dhis2-tracked-entity-first-name", - "dhis2-tracked-entity-last-name", - "dhis2-child-program-active" - ], - "disclosure": "value", - "format": "application/dc+sd-jwt" -} -JSON - -curl -fsS -X POST "$REGISTRY_NOTARY_BASE_URL/v1/evaluations" \ - -H "Authorization: Bearer $REGISTRY_NOTARY_BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - -H "Data-Purpose: $REGISTRY_NOTARY_PURPOSE" \ - --data @evaluation-request.json \ - -o evaluation.json - -jq '.results[] | {claim_id, value, evaluation_id, format}' evaluation.json -``` - -Issue the SD-JWT VC from that evaluation: - -```sh -EVALUATION_ID="$(jq -r '.results[0].evaluation_id' evaluation.json)" - -jq -n --arg evaluation_id "$EVALUATION_ID" '{ - evaluation_id: $evaluation_id, - credential_profile: "dhis2_child_program_sd_jwt", - format: "application/dc+sd-jwt", - claims: [ - "dhis2-tracked-entity-first-name", - "dhis2-tracked-entity-last-name", - "dhis2-child-program-active" - ], - disclosure: "value" -}' > credential-request.json - -curl -fsS -X POST "$REGISTRY_NOTARY_BASE_URL/v1/credentials" \ - -H "Authorization: Bearer $REGISTRY_NOTARY_BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - --data @credential-request.json \ - -o credential.json - -jq '{credential_id, credential_profile, issuer, format, disclosure_count: (.disclosures | length)}' credential.json -``` - -Fetch the issuer keys and verify the credential. -The JWKS endpoint is public: any verifier can fetch the issuer keys without a credential. - -```sh -curl -fsS "$REGISTRY_NOTARY_BASE_URL/.well-known/evidence/jwks.json" -o jwks.json - -node <<'NODE' -const { createHash, createPublicKey, verify } = require('node:crypto'); -const { readFileSync } = require('node:fs'); - -const credential = JSON.parse(readFileSync('credential.json', 'utf8')); -const jwks = JSON.parse(readFileSync('jwks.json', 'utf8')); -const [header64, payload64, signature64] = credential.issuer_signed_jwt.split('.'); -const header = JSON.parse(Buffer.from(header64, 'base64url').toString('utf8')); -const payload = JSON.parse(Buffer.from(payload64, 'base64url').toString('utf8')); -const keys = Array.isArray(jwks.keys) ? jwks.keys : []; -const key = keys.find((candidate) => candidate.kid === header.kid); - -if (!key) throw new Error(`Missing JWKS key ${header.kid}`); - -const signed = Buffer.from(`${header64}.${payload64}`); -const signature = Buffer.from(signature64, 'base64url'); -const signatureValid = verify(null, signed, createPublicKey({ key, format: 'jwk' }), signature); - -if (!signatureValid) throw new Error('Bad issuer signature'); - -const disclosureDigests = new Set(payload._sd || []); -const disclosureClaims = []; - -for (const disclosure of credential.disclosures || []) { - const digest = createHash('sha256').update(disclosure).digest('base64url'); - if (!disclosureDigests.has(digest)) { - throw new Error(`Disclosure digest not present in _sd: ${digest}`); - } - const decoded = JSON.parse(Buffer.from(disclosure, 'base64url').toString('utf8')); - disclosureClaims.push(decoded[1]); -} - -console.log(JSON.stringify({ - issuer: payload.iss, - vct: payload.vct, - signature_valid: signatureValid, - disclosure_count: disclosureClaims.length, - disclosure_claims: disclosureClaims.sort() -}, null, 2)); -NODE -``` - -Expected verification summary: - -```json -{ - "issuer": "did:web:dhis2-notary.lab.registrystack.org", - "vct": "https://dhis2-notary.lab.registrystack.org/credentials/dhis2/child-program/v1", - "signature_valid": true, - "disclosure_count": 3, - "disclosure_claims": [ - "dhis2-child-program-active", - "dhis2-tracked-entity-first-name", - "dhis2-tracked-entity-last-name" - ] -} -``` +The lab also runs a DHIS2-backed Notary for API credential and claim-evaluation work. +Because that path is about integrating an existing source system, it lives in its own tutorial: +[Configure DHIS2 claim checks](../../tutorials/configure-dhis2-claim-checks/). ## Now run your own You have seen the two payoffs against the hosted lab. To run the same shapes on your own machine, start with the local single-node tutorials: -- [Publish a spreadsheet as a secured registry API](../../tutorials/publish-spreadsheet-secured-registry-api/): +- [Run a protected registry API locally](../../tutorials/publish-spreadsheet-secured-registry-api/): stand up your own protected Relay from a sample workbook. -- [Verify a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): run a Notary - against the Relay you published and evaluate a claim. Already have your own API? The same - tutorial ends with a - [standalone Notary path](../../tutorials/verify-claim-registry-api/#run-notary-standalone-for-an-api-you-operate). +- [Evaluate a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): run a Notary + against the Relay you published and evaluate a claim. +- [Connect Notary to a Registry Data API source](../../tutorials/run-notary-standalone-for-api/): + run Notary in a separate project against a Registry Data API-shaped source. +- [Configure DHIS2 claim checks](../../tutorials/configure-dhis2-claim-checks/): connect Registry + Notary to a DHIS2 source adapter and inspect the fields to change for your own DHIS2 deployment. Only assessing fit? You do not need to install anything: [When to use Registry Stack](../when-to-use/) covers fit and non-goals, and diff --git a/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx b/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx new file mode 100644 index 00000000..7b6e7dfe --- /dev/null +++ b/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx @@ -0,0 +1,334 @@ +--- +title: Configure DHIS2 claim checks +description: Run the DHIS2 source-adapter profile, evaluate DHIS2 Tracker claims with Registry Notary, and identify the config blocks to adapt for your own DHIS2 deployment. +status: current +owner: registry-docs +source_repos: + - registry-lab + - registry-notary +last_reviewed: "2026-06-28" +doc_type: tutorial +locale: en +standards_referenced: + - openapi + - sd-jwt-vc +--- + +import QuickstartMeta from '../../../components/QuickstartMeta.astro'; + +Use this tutorial when DHIS2 is already your source system and you want Registry Notary to answer +narrow programme-status claims without sharing full DHIS2 records. +The tutorial uses the public DHIS2 play instance as the source, runs a private source-adapter +sidecar, and exposes only the Registry Notary API to the caller. + +You will configure three pieces: + +- The DHIS2 source adapter, which calls the DHIS2 Tracker API through the built-in `http_json` + engine. +- Registry Notary source bindings, which map projected DHIS2 fields into claim rules. +- Credential profiles, which issue SD-JWT VCs from selected DHIS2 claim results. + + + +The demo uses the public DHIS2 sandbox at `https://play.im.dhis2.org/stable-2-43-0-1`. +The same configuration shape applies to a private DHIS2 deployment once you replace the source URL, +credentials, Tracker query, field projection, and claim rules. + +## Before you start + +You need: + +- Docker with Compose v2.20 or later. +- The `just` command runner. +- `jq` and `node` for manual checks and holder-proof generation. +- A Registry Lab checkout, which is the `lab` directory of the `registry-stack` monorepo. +- Network access to the DHIS2 source you are testing. + +Clone the monorepo if you do not already have it: + +```sh +git clone https://github.com/registrystack/registry-stack.git +cd registry-stack/lab +just setup +``` + +For an existing checkout, update submodules before running the profile: + +```sh +cd registry-stack/lab +just setup +``` + +## Set DHIS2 source access + +The local demo defaults to the public DHIS2 play credentials. The current lab profile still uses +legacy `OPENFN_*` variable names for this DHIS2 sidecar. To point the profile at another DHIS2 +environment, set these values before you run the profile: + +```sh +export OPENFN_DHIS2_HOST_URL="https://play.im.dhis2.org/stable-2-43-0-1" +export OPENFN_DHIS2_USERNAME="admin" +export OPENFN_DHIS2_PASSWORD="district" +``` + +In a private deployment, replace those values with a DHIS2 base URL and a reader account scoped to +the Tracker data you need. Do not use public sandbox credentials outside the demo. + +The Compose profile turns those values into the credential JSON consumed by the sidecar: + +```json +{ + "baseUrl": "https://play.im.dhis2.org/stable-2-43-0-1", + "username": "admin", + "password": "district" +} +``` + +## Generate local credentials + +Generate local Notary tokens, sidecar tokens, signing material, and static metadata: + +```sh +just generate +``` + +This writes `.env`, including: + +- `DHIS2_EVIDENCE_CLIENT_BEARER`, used by callers when they call Registry Notary. +- `OPENFN_SIDECAR_TOKEN_RAW`, the legacy variable name Registry Notary uses when it calls the + private DHIS2 sidecar. +- `OPENFN_SIDECAR_TOKEN_HASH`, the legacy variable name the sidecar uses to authenticate Notary. + +Do not use generated demo credentials outside the local lab. + +## Run the DHIS2 profile + +Build the lab images: + +```sh +just build +``` + +Run the DHIS2 smoke: + +```sh +just dhis2 +``` + +The profile starts: + +| Service | Local URL | Role | +| --- | --- | --- | +| `openfn-dhis2-sidecar` | private Compose network only | DHIS2 source adapter that calls DHIS2 Tracker through `http_json` | +| `dhis2-health-notary` | `http://127.0.0.1:4326` | Registry Notary claim evaluator and SD-JWT VC issuer | + +The service name keeps the legacy `openfn` prefix, but this path uses the built-in `http_json` +engine. There is no Node worker pool or OpenFn workflow runtime in the DHIS2 lookup path. + +Expected ending: + +```text +DHIS2 health evidence and VC smoke passed +``` + +## Inspect the results + +The smoke writes artifacts under: + +```text +output/dhis2-openfn/ +``` + +Inspect the predicate checks: + +```sh +jq '{claim_id: .results[0].claim_id, satisfied: .results[0].satisfied}' \ + output/dhis2-openfn/smoke-dhis2-child-program-active.json +``` + +Inspect the programme participation credential summary: + +```sh +jq '{ + credential_profile, + issuer, + format, + disclosure_count, + disclosure_claim_ids, + programme_active, + reconciliation_ref_available +}' output/dhis2-openfn/smoke-dhis2-programme-participation-credential-summary.json +``` + +The full credential response and holder proof are also written under `output/dhis2-openfn/`. +Treat those files as local debug artifacts and review them before sharing. + +## Call Notary manually + +Load the generated local credentials: + +```sh +set -a +. .env +set +a +``` + +Call Notary discovery: + +```sh +curl -fsS http://127.0.0.1:4326/.well-known/evidence-service \ + -H "Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}" | jq +``` + +Evaluate the programme participation claims for the demo tracked entity: + +```sh +curl -fsS -X POST http://127.0.0.1:4326/v1/evaluations \ + -H "Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}" \ + -H "Content-Type: application/json" \ + -H "Data-Purpose: https://demo.example.gov/purpose/dhis2-openfn-health-evidence" \ + -o output/dhis2-openfn/manual-programme-evaluation.json \ + -d '{ + "target": { + "type": "TrackedEntity", + "identifiers": [ + { "scheme": "dhis2_tracked_entity", "value": "PQfMcpmXeFE" } + ] + }, + "claims": [ + "dhis2-tracked-entity-first-name", + "dhis2-tracked-entity-last-name", + "dhis2-child-age-band", + "dhis2-programme-code", + "dhis2-child-program-active", + "dhis2-reconciliation-ref" + ], + "disclosure": "value", + "format": "application/dc+sd-jwt" + }' + +jq '.results[] | {claim_id, value, satisfied, evaluation_id}' \ + output/dhis2-openfn/manual-programme-evaluation.json +``` + +## Issue the programme credential + +Generate a holder proof for the returned evaluation: + +```sh +EVALUATION_ID="$( + jq -r '.results[0].evaluation_id' \ + output/dhis2-openfn/manual-programme-evaluation.json +)" + +CLAIMS='[ + "dhis2-tracked-entity-first-name", + "dhis2-tracked-entity-last-name", + "dhis2-child-age-band", + "dhis2-programme-code", + "dhis2-child-program-active", + "dhis2-reconciliation-ref" +]' + +scripts/generate-holder-proof.js \ + --audience dhis2-health-notary \ + --evaluation-id "$EVALUATION_ID" \ + --credential-profile dhis2_programme_participation_sd_jwt \ + --disclosure value \ + --claims-json "$CLAIMS" \ + > output/dhis2-openfn/manual-programme-holder.json +``` + +Issue the SD-JWT VC: + +```sh +curl -fsS -X POST http://127.0.0.1:4326/v1/credentials \ + -H "Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}" \ + -H "Content-Type: application/json" \ + -o output/dhis2-openfn/manual-programme-credential.json \ + -d "$(jq -nc \ + --arg evaluation_id "$EVALUATION_ID" \ + --argjson claims "$CLAIMS" \ + --slurpfile holder output/dhis2-openfn/manual-programme-holder.json \ + '{ + evaluation_id: $evaluation_id, + credential_profile: "dhis2_programme_participation_sd_jwt", + format: "application/dc+sd-jwt", + claims: $claims, + disclosure: "value", + holder: $holder[0].holder + }')" + +jq '{credential_id, credential_profile, issuer, format, disclosure_count: (.disclosures | length)}' \ + output/dhis2-openfn/manual-programme-credential.json +``` + +The issued credential is holder-bound to the generated `did:jwk` proof and uses the +`dhis2_programme_participation_sd_jwt` profile. + +## How the adapter is configured + +The DHIS2 source adapter is configured in `config/source-adapter/dhis2-health-sidecar.yaml`. + +Key settings: + +| Setting | What it does | +| --- | --- | +| `sources.dhis2_health.engine` | Uses the built-in `http_json` engine. | +| `sources.dhis2_health.credential_env` | Reads DHIS2 `baseUrl`, `username`, and `password` from the legacy `OPENFN_DHIS2_DEMO_CREDENTIAL_JSON` variable. | +| `sources.dhis2_health.allowed_base_urls` | Pins which DHIS2 base URLs the sidecar may call. | +| `sources.dhis2_health.http_json.path` | Calls `/api/tracker/trackedEntities`. | +| `sources.dhis2_health.http_json.query` | Maps the Notary lookup value into DHIS2 Tracker query parameters. | +| `sources.dhis2_health.http_json.response.records` | Projects the DHIS2 Tracker JSON response into the fields Notary can evaluate. | + +The demo maps one DHIS2 tracked entity into fields such as `first_name`, `last_name`, +`child_program_active`, `child_age_band`, `programme_code`, and `reconciliation_ref`. +For your deployment, change the DHIS2 base URL allow-list, program IDs, attribute IDs, source +projection, and smoke lookup subject to match your DHIS2 configuration. + +## How Notary is configured + +Registry Notary is configured in `config/notary/dhis2-health-notary.yaml`. + +Key settings: + +| Setting | What it does | +| --- | --- | +| `evidence.source_connections.dhis2_openfn.base_url` | Points Notary at the private sidecar. | +| `evidence.source_connections.dhis2_openfn.token_env` | Sends the shared sidecar token from the legacy `OPENFN_SIDECAR_TOKEN_RAW` variable. | +| `evidence.allowed_purposes` | Restricts which `Data-Purpose` values may be used. | +| `evidence.claims[].source_bindings` | Binds each claim to the projected DHIS2 fields. | +| `evidence.claims[].rule` | Extracts a value or evaluates a predicate from the projected fields. | +| `evidence.credential_profiles` | Controls which claims can be issued as SD-JWT VCs, validity, issuer, disclosure, and holder binding. | + +For your deployment, update the claims to match the programme facts your relying services need. +Keep the sidecar private; expose Notary, not DHIS2, to callers. + +## Stop the profile + +Stop the DHIS2 services: + +```sh +docker compose -f compose.yaml --profile dhis2 down +``` + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| `missing .env` | Local credentials have not been generated. | Run `just generate`. | +| Notary discovery returns `401` | The request is missing the local bearer token. | Add `Authorization: Bearer ${DHIS2_EVIDENCE_CLIENT_BEARER}`. | +| The sidecar cannot reach DHIS2 | The source URL, credentials, or network path is wrong, or the public sandbox reset. | Check `OPENFN_DHIS2_HOST_URL`, `OPENFN_DHIS2_USERNAME`, `OPENFN_DHIS2_PASSWORD`, then rerun `just dhis2`. | +| A claim is unsatisfied | The Tracker record does not contain the projected field or programme state. | Inspect the JSON artifact in `output/dhis2-openfn/`, then compare the field with `config/source-adapter/dhis2-health-sidecar.yaml` and `config/notary/dhis2-health-notary.yaml`. | +| `child_age_band` looks coarse | The public child programme demo does not expose date of birth for the tracked entity. | Treat `5_to_17` as lab-derived programme context, not a clinical age calculation. | + +## Next + +- [OpenCRVS claims](../verify-opencrvs-claims/) +- [Getting started with FHIR evidence](../getting-started-fhir-evidence/) +- [Integration patterns](../../explanation/integration-patterns/) diff --git a/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx b/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx index 5533c563..7e8a9aa9 100644 --- a/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx +++ b/docs/site/src/content/docs/tutorials/deploy-standalone-with-own-data.mdx @@ -19,7 +19,7 @@ Use this guide when you operate your own registry data and want to run Registry optionally, Registry Notary) as self-hosted services against that data, rather than the `registryctl` sample project. The sample-based tutorial, -[Publish a spreadsheet as a secured registry API](../publish-spreadsheet-secured-registry-api/), +[Run a protected registry API locally](../publish-spreadsheet-secured-registry-api/), generates a project from the built-in `benefits` sample. There is no `registryctl` initializer for your own dataset today: `registryctl init relay` accepts only `--sample benefits`. @@ -601,7 +601,7 @@ docker run --rm \ fingerprints, and broken source references. `explain-config --format json` prints the resolved configuration and the env vars it requires. For exercising the claim end to end (request shapes, outcomes, and credential issuance on top), -work through [Verify a claim with Registry Notary](../verify-claim-registry-api/), which runs +work through [Evaluate a claim with Registry Notary](../verify-claim-registry-api/), which runs the same claim lifecycle against a `registryctl` sample project. ### Go deeper on Notary diff --git a/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx b/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx index a9548141..643cea40 100644 --- a/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx +++ b/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx @@ -33,8 +33,8 @@ stack locally, start with [See it live](../../start/see-it-live/) instead. This local tour. For normal local adopter projects, start with the `registryctl` tutorials instead: -[publish a secured registry API](../publish-spreadsheet-secured-registry-api/) or -[verify a claim with Registry Notary](../verify-claim-registry-api/). Clone `registry-lab` only when +[run a protected registry API](../publish-spreadsheet-secured-registry-api/) or +[evaluate a claim with Registry Notary](../verify-claim-registry-api/). Clone `registry-lab` only when you want the full multi-service demo topology rather than a generated project. + +This tutorial uses synthetic data and local demo credentials. +Do not use the generated local keys in production. + +## Start with a running source + +For a local reproducible source, complete the first tutorial and leave the Relay project running: + +```sh +registryctl init relay my-first-api --sample benefits +cd my-first-api +registryctl start +registryctl smoke +set -a +. secrets/local.env +set +a +``` + +The `set -a` block exports `ROW_READER_RAW`, which the standalone Notary project uses once when it +is generated. + +## Create the Notary project + +From the parent directory of `my-first-api`, create the standalone Notary project: + +```sh +cd .. +registryctl init notary my-standalone-notary \ + --source-url http://registry-relay:8080 \ + --source-network my-first-api_default \ + --source-token-from-env ROW_READER_RAW +cd my-standalone-notary +``` + +`--source-token-from-env ROW_READER_RAW` reads the token value from your current shell and writes it +into the new project's `secrets/local.env`. + +`my-first-api_default` is the Compose network name Docker derives from the Relay project directory. +If you named that directory differently, use that name with the `_default` suffix. + +## Start Notary + +Start the standalone project: + +```sh +registryctl start +``` + +`registryctl` starts Notary, waits for readiness, and prints the local API URL: + +```text +Notary API: http://127.0.0.1:4255 +API docs: http://127.0.0.1:4255/docs +``` + +Run the smoke check: + +```sh +registryctl notary smoke +``` + +The smoke check lists claims, evaluates the generated starter claim, and writes a detailed report: + +```text +output/notary-smoke-results.json +``` + +## Evaluate the starter claim + +Load this project's local keys: + +```sh +set -a +. secrets/local.env +set +a +``` + +Evaluate the starter claim: + +```sh +curl -sS -X POST \ + -H "x-api-key: $REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW" \ + -H "Content-Type: application/json" \ + -H "Accept: application/vnd.registry-notary.claim-result+json" \ + -d '{ + "target": { "type": "person", "id": "per-2001" }, + "claims": ["benefits-person-exists"], + "disclosure": "predicate", + "purpose": "https://example.local/purpose/tutorial" + }' \ + http://127.0.0.1:4255/v1/evaluations +``` + +Notary returns a claim result without returning the source row. + +## Use another Registry Data API source + +Change the source options at project creation time. +The three `<...>` values must match the Registry Data API lookup route your source exposes: + +```sh +registryctl init notary my-notary \ + --source-url https://api.example.com \ + --source-token-env EVIDENCE_SOURCE_API_TOKEN \ + --source-dataset \ + --source-entity \ + --source-lookup-field +``` + +Notary will read: + +```text +GET /v1/datasets/{dataset}/entities/{entity}/records?{lookup_field}={lookup_value}&fields=...&limit=2 +Authorization: Bearer +Data-Purpose: +``` + +Successful responses must use: + +```json +{ "data": [{ "field": "value" }] } +``` + +`--source-token-env` names the environment variable the Notary container reads at runtime. +After project creation, edit `secrets/local.env` and set `EVIDENCE_SOURCE_API_TOKEN` to the source +token. + +If the source API is another Compose service, pass `--source-network ` so Notary +can join that network. + +## Adapt a custom HTTP source + +Use a source-adapter sidecar when your source API has its own routes, auth, response envelopes, or +matching behavior. + +The adapter runs privately next to Notary and exposes the Registry Data API-shaped route that Notary +expects. + +Start with the smallest engine that fits: + +| Engine | Use when | +| --- | --- | +| `http_json` | One governed HTTP JSON request can return the fields Notary needs. | +| `http_flow` | You need 2 to 5 dependent GET requests, such as search first and fetch detail second. | +| `fhir` | You are projecting bounded FHIR R4 resources into Notary-ready facts. | +| `script_rhai` | You need POST, fallback behavior, or small custom branching that the built-in declarative engines cannot express. | + +Keep the source-specific logic in the adapter. +Keep claim semantics, disclosure, caller policy, and credential issuance in Notary. + +## Clean up + +Stop the standalone Notary project from its directory: + +```sh +registryctl stop +``` + +If you also started the sample Relay source, stop it from the `my-first-api` directory. + +## Next + +- [Evaluate a claim with Registry Notary](../verify-claim-registry-api/): learn the core local + path where Relay and Notary run in one generated project. +- [Configure DHIS2 claim checks](../configure-dhis2-claim-checks/): see a named source-system + integration backed by a source-adapter `http_json` path. +- [Getting started with FHIR evidence](../getting-started-fhir-evidence/): see the FHIR + source-adapter path. +- [Deploy Relay and Notary standalone with your own data](../deploy-standalone-with-own-data/): + move from a local tutorial to an operator-shaped deployment. +- [Source and claim modeling](../../products/registry-notary/source-claim-modeling-guide/): + configure source connections, source adapters, and claim boundaries. + +## Troubleshooting + +| Symptom | Cause | Resolution | +| --- | --- | --- | +| `registryctl init notary` cannot read `ROW_READER_RAW` | The Relay keys were not exported in this shell. | Run the `set -a` block from the Relay project directory, then retry. | +| `registryctl start` fails with `network ... not found` | The source Compose network name is wrong or the source project is not running. | Start the source project and pass the correct `_default` network name. | +| Claim evaluation returns a source auth error | Notary cannot authenticate to the source. | Confirm the source token in `secrets/local.env` and restart Notary. | +| Claim evaluation returns `409 Evidence not available` | The target id is not available from the source, or the dataset, entity, or lookup field does not match the source contract. | Use a known target id or inspect the Registry Data API source contract. | diff --git a/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx b/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx index 4c0eaec5..15527029 100644 --- a/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx +++ b/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx @@ -1,13 +1,13 @@ --- -title: Verify a claim with Registry Notary -description: Add Registry Notary to the local registry API project, evaluate one claim, compare a claim result with a Relay row read, and run Notary standalone against an API you operate. +title: Evaluate a claim with Registry Notary +description: Add Registry Notary to the local registry API project, evaluate one claim, and compare the claim result with a protected row read. status: current owner: registry-docs source_repos: - registry-registryctl - registry-relay - registry-notary -last_reviewed: "2026-06-26" +last_reviewed: "2026-06-28" doc_type: tutorial locale: en standards_referenced: [] @@ -15,27 +15,26 @@ standards_referenced: [] import QuickstartMeta from '../../../components/QuickstartMeta.astro'; -Use this tutorial after you publish the spreadsheet registry API. -You will add Registry Notary to the same local project, start Relay and Notary together, prove -anonymous claim access is denied, and evaluate one claim backed by the registry API. -A final section, [Run Notary standalone for an API you operate](#run-notary-standalone-for-an-api-you-operate), -shows the same flow with Notary in its own project pointed at a source API you choose. +Use this tutorial after you run the protected registry API locally. +You will add Registry Notary to the same project, evaluate one claim, and see the difference +between a source row read and a minimized claim result. This tutorial uses synthetic data and local demo credentials. Do not use the generated local keys in production. -Estimated time: about 10 minutes after the first tutorial passes. -## Before you start +## Start from the local API -Complete [Publish a spreadsheet as a secured registry API](../publish-spreadsheet-secured-registry-api/) -first: +Complete [Run a protected registry API locally](../publish-spreadsheet-secured-registry-api/) +first. + +If you need to recreate the project: ```sh registryctl init relay my-first-api --sample benefits @@ -44,14 +43,9 @@ registryctl start registryctl smoke ``` -The Relay smoke test must pass before you add Notary. - -If you already completed that tutorial and `my-first-api` is still on disk, do not rerun the -`init` line: `registryctl init` refuses to overwrite a directory that is not empty. -From `my-first-api`, run `registryctl start` and `registryctl smoke` to confirm the project is -healthy, then continue. +The Relay smoke check must pass before you add Notary. -## Add claim verification +## Add Notary From the Relay project directory, add Notary: @@ -59,47 +53,11 @@ From the Relay project directory, add Notary: registryctl add notary --from local-relay ``` -The command updates the local project: - -```text -my-first-api/ - registryctl.yaml - compose.yaml - README.md - relay/ - config.yaml - metadata.yaml - notary/ - config.yaml - data/ - benefits_casework.xlsx - bruno/ - registry-api/ - secrets/ - local.env - output/ - .gitignore -``` +The command adds `notary/config.yaml`, updates `compose.yaml`, refreshes the Bruno collection, and +adds Notary demo keys to `secrets/local.env`. -`registryctl` keeps Relay and Notary credentials in `secrets/local.env`. -The Relay and Notary runtime configs contain only fingerprint references and environment -variable names. -This local demo uses generated API keys only; it does not require OIDC, eSignet, or an -assisted-access service. - -The generated Compose file also starts a local Redis replay store for Notary readiness. -It is part of the local demo runtime and does not require manual configuration. - -Notary reads from Relay through the Compose network: - -```text -http://registry-relay:8080 -``` - -`registry-relay` is the service name `registryctl` writes into the generated `compose.yaml`; -it is fixed by the generator, not derived from your project directory name. - -Local browser and curl examples use the host URL: +Notary reads from Relay through the local Compose network. +You still call Notary from your workstation at: ```text http://127.0.0.1:4255 @@ -113,8 +71,7 @@ Start the project again: registryctl start ``` -The command starts both services and, after the Docker Compose progress lines, waits for both -health and readiness checks: +`registryctl` waits for both services and prints the local URLs: ```text Relay API: http://127.0.0.1:4242 @@ -123,26 +80,7 @@ Notary API: http://127.0.0.1:4255 API docs: http://127.0.0.1:4255/docs ``` -Check the project status: - -```sh -registryctl status -``` - -The Relay and Notary services report healthy and ready when startup is complete. - -Validate both generated runtime configs with the product doctors: - -```sh -registryctl doctor --format json -``` - -The JSON report uses `schema_version: registryctl.validation.report.v1`. It contains separate -Relay and Notary product reports under `products[]`; each embedded product report uses -`schema_version: registry.config.diagnostic_report.v1` and a `status` of `ok`, `warning`, -`error`, or `not_run`. - -## Run the Notary smoke test +## Run the Notary smoke check Run the Notary smoke checks: @@ -150,7 +88,7 @@ Run the Notary smoke checks: registryctl notary smoke ``` -The smoke test passes with these checks: +You should see the core checks pass: ```text PASS notary healthz is public @@ -162,15 +100,12 @@ PASS notary evaluator can list claims PASS notary evaluator can verify benefits person exists ``` -`registryctl` writes detailed results to: +`registryctl` writes the detailed report to: ```text output/notary-smoke-results.json ``` -The smoke result must not contain raw API keys, the Relay source token, local env values, Relay -source rows, or sensitive sample column values. - ## Load local demo keys Load the generated local keys into your shell: @@ -181,26 +116,11 @@ set -a set +a ``` -The Notary tutorial adds these local values: - -| Value | Environment variable | What it is for | -| --- | --- | --- | -| Notary evaluator key | `REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW` | Lets you call Notary evaluation routes | -| Notary evaluator fingerprint | `REGISTRY_NOTARY_TUTORIAL_EVALUATOR_HASH` | Lets Notary verify the local evaluator key | -| Notary audit hash secret | `REGISTRY_NOTARY_AUDIT_HASH_SECRET` | Lets Notary hash audit subjects without logging raw values | -| Relay source token for Notary | `EVIDENCE_SOURCE_REGISTRY_RELAY_TOKEN` | Lets Notary read the Relay source API | -| Notary demo issuer JWK | `REGISTRY_NOTARY_ISSUER_JWK` | Local demo signing key used only to make Notary readiness pass | -| Notary replay Redis URL | `REGISTRY_NOTARY_REPLAY_REDIS_URL` | Points Notary at the local demo Redis replay store | - The Notary evaluator key is for you calling Notary. The Relay source token is for Notary calling Relay. They are intentionally separate. -The same `secrets/local.env` still carries the keys from the Relay tutorial, including the -row-reader key `ROW_READER_RAW`, which this page uses later to compare a row read with a claim -result. - -## Prove anonymous claim access is denied +## Make one denied claim request Call the claim list without a credential: @@ -210,6 +130,8 @@ curl -i http://127.0.0.1:4255/v1/claims Notary returns `401 Unauthorized`. +## List the available claim + Call the same route with the Notary evaluator key: ```sh @@ -218,10 +140,9 @@ curl -sS \ http://127.0.0.1:4255/v1/claims ``` -The response is a JSON list of the configured claim definitions. -The starter claim appears in it with `"id": "benefits-person-exists"`. +The starter claim appears with `"id": "benefits-person-exists"`. -## Evaluate a claim from registry data +## Evaluate one claim Evaluate whether the synthetic person `per-2001` exists in the Relay-backed benefits dataset: @@ -231,10 +152,7 @@ curl -sS -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/vnd.registry-notary.claim-result+json" \ -d '{ - "target": { - "type": "person", - "id": "per-2001" - }, + "target": { "type": "person", "id": "per-2001" }, "claims": ["benefits-person-exists"], "disclosure": "predicate", "purpose": "https://example.local/purpose/tutorial" @@ -242,58 +160,28 @@ curl -sS -X POST \ http://127.0.0.1:4255/v1/evaluations ``` -Notary returns a successful claim result for `benefits-person-exists`. -The response shows the claim outcome, not the Relay source row. -It has this shape (identifiers, timestamps, and the target handle vary per run): +Notary returns a claim result. +The response contains the outcome, provenance, timestamps, and a pseudonymous target reference. +It does not return the Relay source row. + +The important fields look like this: ```json { "results": [ { "claim_id": "benefits-person-exists", - "claim_version": "2026-06", "disclosure": "predicate", - "evaluation_id": "01KTS319VPVAJWKT8SZF4R8ZVN", - "expires_at": null, "format": "application/vnd.registry-notary.claim-result+json", - "issued_at": "2026-06-10T15:39:53Z", - "provenance": { - "schema_version": "registry-notary-claim-provenance/v1", - "generated_by": { - "type": "claim_evaluation", - "service_id": "registryctl.benefits.notary", - "evaluation_id": "01KTS319VPVAJWKT8SZF4R8ZVN", - "claim_id": "benefits-person-exists", - "claim_version": "2026-06" - }, - "used": { - "source_count": 1, - "source_versions": {}, - "source_runtimes": [] - }, - "derived_from": [] - }, "satisfied": true, "subject_type": "person", - "target_ref": { - "handle": "rnref:v1:hmac-sha256:...", - "type": "person" - }, "value": true } ] } ``` -What happened: - -- You called Notary with the Notary evaluator key. -- Notary checked that the key can evaluate the configured claim. -- Notary used its internal Relay source token to query Relay. -- Relay enforced its row-read scope and purpose-header rules. -- Notary returned the configured claim result without exposing the spreadsheet row. - -## Evaluate a claim for an unknown person +## Try an unknown person Run the same evaluation for an id that is not in the sample workbook: @@ -311,24 +199,10 @@ curl -sS -X POST \ http://127.0.0.1:4255/v1/evaluations ``` -Notary does not return `satisfied: false` for a missing person. -It returns a `409` problem document, because there is no evidence to evaluate: - -```json -{ - "code": "evidence.not_available", - "detail": "the evidence is not available", - "request_id": "01KTS31QC7184F5487VW0P7XM3", - "status": 409, - "title": "Evidence not available", - "type": "https://docs.registry-notary.dev/problems/evidence/not_available" -} -``` - -This distinction matters to integrators: a claim result answers the configured question about a -known subject, while a missing subject is an evidence error, not a negative answer. +Notary returns `409 Evidence not available`. +A missing subject is an evidence error, not a negative claim result. -## Compare a row read and a claim result +## Compare row read and claim result Relay still exposes the source-facing consultation API: @@ -340,7 +214,7 @@ curl -sS -G \ http://127.0.0.1:4242/v1/datasets/benefits_casework/entities/person/records ``` -Notary exposes a claim API: +Notary exposes the claim API: ```sh curl -sS -X POST \ @@ -357,137 +231,34 @@ curl -sS -X POST \ ``` Use Relay when a caller is allowed to consult configured records. -Use Notary when a caller receives a narrow claim result. - -## Explore requests in Bruno +Use Notary when a caller should receive a narrow claim result. -`registryctl add notary --from local-relay` refreshes the generated Bruno collection with Notary -requests. -Bruno is optional. -The tutorial and API work without it. +## Inspect the generated claim -Open the generated collection: +Now that the evaluation works, inspect the generated Notary contract: ```sh -registryctl bruno open +sed -n '1,220p' notary/config.yaml ``` -If Bruno is installed, the collection opens with Relay and Notary folders. -If Bruno is not installed, the command prints the collection path and an install link. +The generated config defines the source connection to Relay, the evaluator key fingerprint, the +starter claim, and the disclosure format. +Raw evaluator keys and Relay source tokens stay in `secrets/local.env`. -If the Bruno CLI is installed, you can run the collection: - -```sh -registryctl bruno run -``` - -If `bru` is not installed, the command prints a fallback and exits without blocking Relay or -Notary. - -## Open the Notary API reference - -Open the Notary API surface: +Open the local Notary API reference: ```sh registryctl notary open ``` -The command opens the docs page in your default browser. -In an environment that cannot launch one, such as a remote shell, it prints the URLs instead: +In an environment that cannot launch a browser, the command prints: ```text Notary API docs: http://127.0.0.1:4255/docs OpenAPI JSON: http://127.0.0.1:4255/openapi.json ``` -## Run Notary standalone for an API you operate - -Everything so far ran Notary inside the Relay project. -When you already operate an API and only want claim verification, create a standalone Notary -project instead and point it at your source API. -The source must expose the Registry Data API-shaped record route Notary is configured to call. - -If you arrived here directly, for example from the routing table on the docs homepage, note that the -reproducible demo below uses the local registry API from the sections above as its stand-in -source, so it needs that project on disk and running (the [Before you start](#before-you-start) -block creates it in about two minutes). -If you already have a live source API of your own, skim the demo for the shape of the flow, -then start from [For an API you operate](#for-an-api-you-operate) below and substitute your own -URL, dataset, and token. - -To keep the steps reproducible without external credentials, this section reuses the local -registry API as the stand-in source. -Leave the Relay project running, and load its demo keys in your current shell (the `set -a` -block in [Load local demo keys](#load-local-demo-keys)); the project-creation step reads -`ROW_READER_RAW` from your shell. - -From the parent directory of `my-first-api`, create the Notary project: - -```sh -cd .. -registryctl init notary my-standalone-notary \ - --source-url http://registry-relay:8080 \ - --source-network my-first-api_default \ - --source-token-from-env ROW_READER_RAW -cd my-standalone-notary -``` - -`--source-token-from-env ROW_READER_RAW` reads the token value from your current shell once, at -project-creation time, and writes it into the new project's `secrets/local.env`. -`my-first-api_default` is the Compose network name Docker derives from the Relay project -directory name; if you named that directory differently, use that name with the `_default` -suffix. -The generated Notary config contains the source API URL, source dataset, entity, lookup field, -and environment variable names. -The raw keys and tokens live only in this project's own `secrets/local.env`. - -### For an API you operate - -Change the source options at project creation time. -The three `<...>` values are placeholders for what your source API exposes; the demo values -above (`benefits_casework`, `person`, `id`) are also the CLI defaults, so leaving these flags -off wires Notary to a dataset that does not exist on your API: - -```sh -registryctl init notary my-notary \ - --source-url https://api.example.com \ - --source-token-env EVIDENCE_SOURCE_API_TOKEN \ - --source-dataset \ - --source-entity \ - --source-lookup-field -``` - -`--source-token-env` differs from `--source-token-from-env` above: it does not read a value -from your shell now, it names the environment variable the Notary container resolves at -runtime. -Supply the value afterwards by editing `secrets/local.env` and setting -`EVIDENCE_SOURCE_API_TOKEN` to the source token. -If the source API is another Compose service, pass `--source-network ` so -Notary can join that network. - -### For a FHIR source-adapter sidecar - -If you already have a FHIR source-adapter sidecar running, start from the -FHIR profile instead of the Registry Data API defaults: - -```sh -registryctl init notary my-fhir-notary --source-kind fhir-sidecar -``` - -This generates a starter `patient-record-exists` claim, points Notary at the -sidecar URL `http://host.docker.internal:4360`, and uses -`FHIR_SIDECAR_TOKEN` in `secrets/local.env`. Change `--source-url`, -`--source-token-env`, `--source-dataset`, `--source-entity`, or -`--source-lookup-field` if your sidecar uses different local names. - -From the standalone project directory, the rest of the flow is the same as the co-located -sections earlier on this page: `registryctl start`, `registryctl notary smoke`, load this -project's keys with the `set -a` block, and evaluate the claim against -`http://127.0.0.1:4255/v1/evaluations`. -The standalone project has its own `secrets/local.env`; load it from this directory, not the -Relay project's. - -## Clean up +## Stop the stack When you are done: @@ -495,22 +266,17 @@ When you are done: registryctl stop ``` -This stops both local services. +This stops the local services. It does not delete your workbook, generated configs, local keys, or smoke results. -If you created the standalone Notary project, stop it from its own directory, then stop the -source registry API from `my-first-api`. - ## Next -- [Source and claim modeling](../../products/registry-notary/source-claim-modeling-guide/): - configure source connections and claim boundaries. -- [Registry Relay client integration](../../products/registry-relay/client-integration/): call - Relay from an application. -- [See it live](../../start/see-it-live/): explore the hosted demo to see Relay, Notary, and - cross-authority flows in action. -- [First run with Registry Lab](../first-run-with-registry-lab/): run the full multi-service - topology locally, with three Relays, cross-authority Notaries, and an identity provider. +- Define policy for this claim: purpose binding, context constraints, denial reasons, and audit + inspection are the next local rung. +- [Connect Notary to a Registry Data API source](../run-notary-standalone-for-api/): run Notary in + a separate project against a Registry Data API-shaped source. +- [See it live](../../start/see-it-live/): explore the hosted demo to see credential issuance and + cross-authority flows. ## Troubleshooting @@ -519,11 +285,6 @@ source registry API from `my-first-api`. | `registryctl add notary --from local-relay` cannot find a Relay project | The current directory does not contain a generated `registryctl.yaml` with a Relay section. | Run the command from the Relay tutorial project directory. | | `registryctl add notary --from local-relay` cannot find a source token | `secrets/local.env` is missing or does not contain the Relay row-reader key. | Recreate the Relay project or restore the generated local env file. | | `registryctl start` starts Relay but Notary is not ready | Notary config, source token, or Compose service wiring is invalid. | Run `registryctl status`, then `registryctl logs` and check the Notary service errors. | -| The Notary container log shows `failed to parse config YAML ... unknown field` | The locally cached container image does not match the digest-pinned image in the generated `compose.yaml`. | Run `docker compose pull` in the project directory to refresh the pinned images, then `registryctl start` again. | -| Notary `/ready` is degraded while `/healthz` is healthy | The local replay store or demo issuer key is not available to Notary. | Run `registryctl stop`, then `registryctl start`; if it persists, inspect `registryctl logs`. | | `registryctl notary smoke` returns `401` for authorized calls | The Notary evaluator key was not loaded or does not match the generated Notary fingerprint. | Run `. secrets/local.env`, then retry. | | Claim evaluation returns a source auth error | Notary cannot authenticate to Relay with `EVIDENCE_SOURCE_REGISTRY_RELAY_TOKEN`. | Confirm `secrets/local.env` has the source token and Relay is running. | | Claim evaluation returns `409 Evidence not available` | The target id is not in the sample workbook or the Relay entity lookup changed. | Use `per-2001` for the tutorial target, or inspect the Relay `person` entity. | -| `registryctl init notary` fails with `failed to read source token from $ROW_READER_RAW: environment variable not found` | The Relay keys were loaded in a different shell session, or not at all. | Run the `set -a; . secrets/local.env; set +a` block from the Relay project directory in this shell, then rerun the command. | -| Standalone `registryctl start` fails and the Notary log shows `network ... not found` | The Relay project is not running, or the `--source-network` name does not match the Relay project directory. | Start the Relay project, and pass the Compose network named after that directory with the `_default` suffix. | -| Standalone `registryctl notary smoke` returns `401` for authorized calls | The shell still holds the Relay project's keys, not the standalone Notary keys. | Run `set -a; . secrets/local.env; set +a` from the standalone project directory, then retry. | diff --git a/products/notary/docs/README.md b/products/notary/docs/README.md index 103cd253..afb65d3d 100644 --- a/products/notary/docs/README.md +++ b/products/notary/docs/README.md @@ -10,7 +10,9 @@ format, or issues a short-lived SD-JWT VC credential. Pick your path below. New to Registry Notary? Start with the hosted walkthrough or a runnable local tutorial. If you are configuring or operating Notary, start with the [architecture overview](architecture-overview.md). - [See it live](https://docs.registrystack.org/start/see-it-live/): watch Notary issue a privacy-preserving credential against a hosted lab, with zero install. -- [Verify a claim with Registry Notary](https://docs.registrystack.org/tutorials/verify-claim-registry-api/): add Notary to a local registry API project with `registryctl`. Its final section, [Run Notary standalone for an API you operate](https://docs.registrystack.org/tutorials/verify-claim-registry-api/#run-notary-standalone-for-an-api-you-operate), covers creating a standalone Notary project for a source API you operate. +- [Verify a claim with Registry Notary](https://docs.registrystack.org/tutorials/verify-claim-registry-api/): add Notary to a local registry API project with `registryctl`. +- [Connect Notary to a Registry Data API source](https://docs.registrystack.org/tutorials/run-notary-standalone-for-api/): run Notary in a separate project against a Registry Data API-shaped source. +- [Configure DHIS2 claim checks](https://docs.registrystack.org/tutorials/configure-dhis2-claim-checks/): use the built-in `http_json` source adapter to evaluate DHIS2 Tracker claims and issue an SD-JWT VC from the result. - [Architecture overview](architecture-overview.md): what Registry Notary is, the request lifecycle, and how the layers relate. - [Capability matrix](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/notary-capability-matrix.md): which flows Notary supports today, by persona and system. @@ -21,7 +23,6 @@ Pick your path below. New to Registry Notary? Start with the hosted walkthrough For application and wallet developers calling the API or the SDKs. - [Client SDK guide](client-sdk-guide.md): evaluate claims and issue credentials from Rust, Python, and Node.js. -- [Call Notary from OpenFn](openfn-notary-caller-guide.md): use the Registry Stack OpenFn Notary adaptor to branch a workflow on a minimized claim result or certified value claim. - [API reference](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/api-reference.md): the route-to-client-method matrix and the stable problem-code registry. - [Wallet interop with OID4VCI](oid4vci-wallet-interop.md): the OpenID4VCI wallet facade contract and compatibility checklist. - [SD-JWT VC conformance](sd-jwt-vc-conformance-profile.md): the supported credential wire contract and the explicit non-support list. @@ -38,6 +39,7 @@ For operators deploying, configuring, and running a Registry Notary. - [Configuration reference](operator-config-reference.md): the config blocks for auth, evidence, sources, replay, status, self-attestation, OID4VCI, and federation. - [Model sources and claims](source-claim-modeling-guide.md): design source connectors, source adapter sidecars, claim boundaries, disclosure, and batch reads. +- [DHIS2 source adapter tutorial](https://docs.registrystack.org/tutorials/configure-dhis2-claim-checks/): configure a sidecar that calls DHIS2 Tracker through the built-in `http_json` engine. - [FHIR source adapter](fhir-source-adapter-guide.md): project bounded FHIR R4 graphs into Notary-ready source facts without exposing raw FHIR Bundles. - [Script (Rhai) source adapter](script-rhai-source-adapter-guide.md): run a sandboxed, orchestration-only Rhai script for sources that need a little branching across a few governed reads. - [Signing key providers](signing-key-provider.md): SD-JWT VC signing-key configuration, rotation, and PKCS#11 setup. diff --git a/products/notary/docs/source-claim-modeling-guide.md b/products/notary/docs/source-claim-modeling-guide.md index 32d0238e..5ed59d52 100644 --- a/products/notary/docs/source-claim-modeling-guide.md +++ b/products/notary/docs/source-claim-modeling-guide.md @@ -26,7 +26,7 @@ Keep source connectors narrow and keep claim semantics in Notary config. | --- | --- | --- | | DCI | The upstream speaks a DCI-style search envelope | `connector: dci` | | Registry Data API | The upstream exposes `/v1/datasets/{dataset}/entities/{entity}/records` lookups | `connector: registry_data_api` | -| Source adapter sidecar | A private sidecar must normalize a target system outside Notary, using built-in `http_json`, `http_flow`, or `fhir` source engines | `connector: source_adapter_sidecar` | +| Source adapter sidecar | A private sidecar must normalize a target system outside Notary, using built-in `http_json`, `http_flow`, `fhir`, or `script_rhai` source engines | `connector: source_adapter_sidecar` | Prefer the simplest direct source. Add a sidecar when the target system needs private credentials, governed request shaping, or output normalization outside @@ -118,8 +118,10 @@ The source adapter sidecar is a separate private process that normalizes a targe system into Notary's source-read contracts. The Notary connector value remains `source_adapter_sidecar`. Inside the sidecar, a source can use the built-in `http_json` engine for straightforward HTTP JSON APIs, the built-in `http_flow` -engine for short dependent GET-only HTTP JSON reads, or the built-in `fhir` -engine. Use the first-class connector for new configs: +engine for short dependent GET-only HTTP JSON reads, the built-in `fhir` +engine, or the sandboxed `script_rhai` engine for small branching cases that +do not fit the declarative engines. Use the first-class connector for new +configs: ```yaml evidence: @@ -188,7 +190,9 @@ Boundary rules: - The sidecar must be reachable only over localhost or a private pod network from Notary. Do not expose it publicly or place it behind an internet-facing ingress. -- Pin worker runtime and adaptor versions for source-adapter sources. +- Pin the sidecar image and governed runtime config for source-adapter sources. + If you use a compatibility path that carries separate runtime or adaptor + versions, pin those too. - Store sidecar target credentials in sidecar env, not in Notary config. - Return no more than two records for a lookup. - Return only normalized fields needed by Notary. From fdd153146c7fffb9217373c793c757f543c67369 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 12:47:25 +0000 Subject: [PATCH 02/25] docs: add "Records stay home" explanation pilot page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the trust-spine pilot explanation at explanation/records-stay-home.mdx — the quality exemplar for the documentation effort. It explains how an institution proves facts from registries it already holds without the records leaving: what stays inside the boundary, what crosses it, the three disclosure modes (value / predicate / redacted), and an honest statement of what the design does and does not guarantee. Resolves the shippable frontmatter TODO by setting owner to the registry-docs area (matching sibling explanation pages) and quotes last_reviewed for house-style consistency. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../docs/explanation/records-stay-home.mdx | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/site/src/content/docs/explanation/records-stay-home.mdx diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx new file mode 100644 index 00000000..b9c5c05b --- /dev/null +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -0,0 +1,194 @@ +--- +title: Records stay home +description: How an institution proves facts from registries it already holds — without the records leaving. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: [] +--- + +An institution that runs a civil registry, a social-protection database, or a health +registry already holds the records it needs. Registry Stack lets it **answer questions +about those records** — *is this person alive? is this household eligible?* — and return +a result another system can trust, while the records themselves are **read where they +already live, never written back, and never handed over**. + +This page explains what that means in practice: what stays inside the institution's +boundary, what crosses it, and — equally important — what the design does and does not +guarantee. + +## A question goes in, an answer comes out + +The mental model is one sentence: **a scoped question crosses into the institution, the +record is read in place, and only a computed answer crosses back out.** + +A caller never sends the value it is asking about and never receives the underlying +record. It sends a subject identifier and the id of a *claim* — a single, pre-modelled +question — and receives one of a few narrow shapes of answer: a yes/no, a single value, a +machine-readable evaluation result, or a credential the subject can carry in a wallet. The source row that the +answer was computed from stays behind. + +## The boundary + +```mermaid +flowchart LR + subgraph inst["Institution — data stays here"] + src[("Source registry\nCSV · XLSX · Parquet · PostgreSQL")] + relay["Registry Relay\nprotected read API"] + notary["Registry Notary\nevaluate · disclose · issue"] + key>"Signing key\n(private half never leaves)"] + audit[("Audit log")] + src -- read in place --> relay + relay -- governed read --> notary + notary -. records .-> audit + key -. signs .-> notary + end + caller["Caller / verifier"] + holder(["Subject / wallet"]) + caller == "request: subject id + claim id + scope" ==> notary + notary == "answer: yes/no · value · evaluation result" ==> caller + notary == "holder-bound credential" ==> holder + + classDef inside fill:#eef,stroke:#334,stroke-width:1px; + classDef outside fill:#f7f7f7,stroke:#777,stroke-dasharray:3 3; + class src,relay,notary,key,audit inside; + class caller,holder outside; +``` + +*A request and an answer cross the boundary. The source record does not.* Registry Relay +turns an existing file or database table into a read-only, access-controlled API without +replacing the source. Registry Notary evaluates one modelled question against that source +and returns a shaped result; it is the only component that evaluates claims, applies +disclosure policy, and issues credentials. + +## What stays home + +- **Source data is read in place.** Relay reads sources as batch snapshots or table scans; + there is no write-back to the source registry, and runtime services expose no + data-mutation routes. The source keeps running as it always has. +- **Storage internals stay private.** The paths, table names, and backend credentials that + point at the source live in the service's runtime configuration, decided at startup. They + are never part of the public API surface, and never part of a portable metadata file that + gets distributed. +- **The institution keeps custody.** The design premise is *distributed custody*: each + authority retains control of its own registry data, and the stack does not aggregate + records into a central system. It provides the exchange surface, not a data lake. +- **Private signing keys never leave the issuer.** The institution publishes the *public* + half of its signing key so anyone can verify a result; the private half stays inside. + +## What crosses the boundary + +Only a computed answer crosses out — never the source row. The answer takes one of a few +shapes: + +- **A yes/no** — only the true/false satisfaction of the modelled rule. +- **A single value** — the evaluated value itself, when the claim's disclosure mode is + `value` (this returns the full value). +- **A machine-readable evaluation result** — a claim-result document carrying *provenance + metadata*: which evaluation produced it, under which policy, across how many sources. This + provenance lets a receiving system trace the result; it is not a cryptographic signature. +- **A holder-bound credential** — an SD-JWT VC the subject can store in a wallet and present + later. Unlike the plain result, the credential is cryptographically verifiable against the + issuer's published keys. + +Across a federation boundary — one institution's Notary asking another's — what crosses is +a scoped, signed evaluation result, never a credential. + +## How much an answer reveals: the three disclosure modes + +Every claim carries a **disclosure mode** that fixes how much of the answer the caller +receives. There are exactly three: + +| Mode | Discloses | Withholds | +|------|-----------|-----------| +| `value` | the full evaluated value | nothing about the value | +| `predicate` | only the true/false satisfaction | the underlying value | +| `redacted` | neither — the result carries no value **and** no yes/no | the value *and* the outcome | + +The mode is policy-bound, not caller's choice: a claim defines an `allowed` set and a +`default`, the service refuses a mode outside the allowed set, and every result records +which mode was applied. A privacy-sensitive claim is expected to default to the +least-revealing mode that still answers the question. + +This is the mechanism behind "prove a fact without sharing the record". To check whether a +person has a registered record, model the question as an *existence* rule and disclose it +as a `predicate`: the caller learns `true` or `false`, and the row never crosses the +boundary. To check eligibility without exposing an income figure, derive the decision with +an expression rule and disclose the eligibility boolean as a `predicate`; the income value +stays home. + +## Why the answer is not the record + +A credential is not a copy of the record. It is an **SD-JWT VC**: the signed body carries a +SHA-256 *digest* of each selectively disclosable field rather than the field value, so a +field the holder does not present stays hidden. It is **holder-bound** — tied to the +holder's key, and not presentable without the matching private key — and the holder chooses +which fields to reveal to which verifier. Anyone can verify it against the issuer's +published public keys, served without authentication so a verifier needs no credential of +its own. The issued credential carries no full record payload. + +## How the boundary is enforced + +The "stays home" property rests on a few enforced rules, covered in depth in the Trust & +Security material: + +- **Scope-before-source, deny-by-default.** A service checks the caller's scope *before* it + reads any source or evaluates any claim, and does not widen a caller's reach at request + time beyond what its configuration grants. Only liveness probes and the public verification + keys are reachable without authentication; anything that touches a record or a claim + requires it. +- **A permit, or a closed door.** On a governed read, the policy decision point must return + a permit before data is returned; a denial fails closed with a stable reason rather than + falling back to an ungoverned read. +- **Every person-level request is audited.** An audit record captures at least the caller, + the scopes exercised, a request id, and the declared purpose where one was supplied. A + deployment can run audit fail-closed, so a request whose audit record cannot be written + does not return success. + +## What this guarantees — and what it does not + +"Records stay home" is a precise, narrow promise. Reading it as more than it is would be a +mistake, so the limits are stated plainly here. + +- **It is not "data never moves" and not "air-gapped".** The promise is *read-in-place, no + write-back, retained custody*. Authorized, minimized answers do leave the boundary by + design — that is the point of the system. +- **Minimization is modelled, not automatic.** `value` mode discloses the full value. A + claim reveals only what its author configured it to reveal; least disclosure is a design + choice the claim makes, not a property the stack imposes on every answer. +- **Correctness depends on the source.** Notary reports what the configured source says; it + does not independently vouch for whether the source is correct or current. +- **A plain result is provenance-tagged, not signed.** The everyday evaluation response + carries provenance metadata, not a cryptographic signature. Cryptographic verifiability + comes from the SD-JWT VC credential and the signed federation result — a receiving system + that must verify an answer cryptographically uses the credential, not the default response. +- **Matching is only as strict as it is configured.** Notary resolves a subject through its + configured matching policy and does not independently verify identity beyond that. By + default a matching failure collapses to a single public reason, so the lookup surface + cannot be used as an existence oracle. +- **This is not zero-knowledge.** A `predicate` answer is a policy-enforced boolean computed + inside the service; SD-JWT selective disclosure is digest omission. Neither is a + zero-knowledge proof, and the documentation should not imply one. +- **No revocation or erasure flow is defined.** This version specifies issuance, + disclosure, presentation, and verification, but not credential revocation or + data-subject erasure. A key rotated out may remain published so existing results stay + verifiable — that is not a revocation mechanism. +- **Several guarantees are the operator's to provide.** Network egress limits, key custody, + tenant isolation, audit retention, and transport security are supplied by the deployment, + not guaranteed by the stack. +- **The model is specified in draft.** The behaviour above is defined in the `RS-*` + specifications, which are still drafts and may change. Alignment with an external standard + is not a claim of conformance to it or of legal compliance. + +## Related + +- The security model and protocol contracts: [RS-SEC-G](/spec/rs-sec-g/), + [RS-PR-RELAY](/spec/rs-pr-relay/), [RS-PR-NOTARY](/spec/rs-pr-notary/), + [RS-DM-CLAIM](/spec/rs-dm-claim/) +- Evidence issuance, end to end *(explanation)* +- Disclosure and minimization in depth *(Trust & Security)* +- The threat model and security posture *(Trust & Security)* From 077dcb043d43a859293208b14bbb11ba8c40a78e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 13:14:28 +0000 Subject: [PATCH 03/25] docs(records-stay-home): fix archive links and tighten security claims Make the Related links archive-safe (../../spec/... instead of root- relative /spec/...), unblocking the check:links:built CI gate that rejects archived pages linking outside their version. Tighten two Tier-C security claims to match RS-SEC-G and the page's own honesty thesis: - the published public key verifies a signed credential or signed result, not "a result" in general (default results are provenance- tagged, not signed); - describe the unauthenticated surface accurately (liveness/readiness probes, public verification keys, credential-issuance discovery metadata) while keeping the strong rule that anything touching a record or claim requires authentication. Declare the SD-JWT VC standard in standards_referenced. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../docs/explanation/records-stay-home.mdx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index b9c5c05b..8696946c 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -8,7 +8,8 @@ source_repos: last_reviewed: "2026-06-28" doc_type: explanation locale: en -standards_referenced: [] +standards_referenced: + - sd-jwt-vc --- An institution that runs a civil registry, a social-protection database, or a health @@ -78,7 +79,8 @@ disclosure policy, and issues credentials. authority retains control of its own registry data, and the stack does not aggregate records into a central system. It provides the exchange surface, not a data lake. - **Private signing keys never leave the issuer.** The institution publishes the *public* - half of its signing key so anyone can verify a result; the private half stays inside. + half of its signing key so anyone can verify a signed credential or signed result; the + private half stays inside. ## What crosses the boundary @@ -138,9 +140,10 @@ Security material: - **Scope-before-source, deny-by-default.** A service checks the caller's scope *before* it reads any source or evaluates any claim, and does not widen a caller's reach at request - time beyond what its configuration grants. Only liveness probes and the public verification - keys are reachable without authentication; anything that touches a record or a claim - requires it. + time beyond what its configuration grants. Anything that touches a record or a claim + requires authentication; the routes reachable without it are limited to operational and + discovery surfaces: liveness and readiness probes, the public verification keys, and + credential-issuance discovery metadata. - **A permit, or a closed door.** On a governed read, the policy decision point must return a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. @@ -186,9 +189,9 @@ mistake, so the limits are stated plainly here. ## Related -- The security model and protocol contracts: [RS-SEC-G](/spec/rs-sec-g/), - [RS-PR-RELAY](/spec/rs-pr-relay/), [RS-PR-NOTARY](/spec/rs-pr-notary/), - [RS-DM-CLAIM](/spec/rs-dm-claim/) +- The security model and protocol contracts: [RS-SEC-G](../../spec/rs-sec-g/), + [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), + [RS-DM-CLAIM](../../spec/rs-dm-claim/) - Evidence issuance, end to end *(explanation)* - Disclosure and minimization in depth *(Trust & Security)* - The threat model and security posture *(Trust & Security)* From 578cf681164c5602167b3a98932d58b841442078 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 06:30:17 +0000 Subject: [PATCH 04/25] docs: add Stage-1 Trust & Security explanation set; refresh the Relay tutorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add five reviewer/operator-facing explanation pages and wire them into the sidebar (a new "Trust & security" group, plus the disclosure page and the records-stay-home pilot under Explanation): - Trust posture and security guarantees — a plain-language overview with an at-a-glance posture table. - Disclosure modes and computed answers — the three modes and what each one reveals. - Data minimization and purpose limitation — the privacy / DPO view. - Threat model — boundaries, assets, threats, and residual risks. - Known limitations and non-guarantees — the canonical limits hub the other pages link to instead of restating. Refresh the Relay tutorial (publish-spreadsheet-secured-registry-api) with a clearer framing, the exact problem+json codes (auth.missing_credential / auth.scope_denied / auth.purpose_required), and a "what you built" recap, on top of its existing verified command output. The sh-command count is unchanged, so the tutorial drift check still holds. All pages follow the house style (second person, sentence-case headings, no inline spec citations — the specs are linked at the foot of each page), and every load-bearing security/privacy claim was re-verified against both the RS-* specs and the implementation. The content stages of `npm run check` pass (frontmatter, Vale, markdown, openapi lint, tutorial drift, build, llms, links, seo). Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- docs/site/astro.config.mjs | 14 + ...ta-minimization-and-purpose-limitation.mdx | 87 +++++++ .../disclosure-modes-and-computed-answers.mdx | 87 +++++++ .../docs/explanation/known-limitations.mdx | 119 +++++++++ .../content/docs/explanation/threat-model.mdx | 246 ++++++++++++++++++ .../trust-posture-and-security-guarantees.mdx | 135 ++++++++++ ...blish-spreadsheet-secured-registry-api.mdx | 26 +- 7 files changed, 709 insertions(+), 5 deletions(-) create mode 100644 docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx create mode 100644 docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx create mode 100644 docs/site/src/content/docs/explanation/known-limitations.mdx create mode 100644 docs/site/src/content/docs/explanation/threat-model.mdx create mode 100644 docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index fa60de1d..704dc02b 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -237,14 +237,28 @@ export default defineConfig({ collapsed: true, items: [ { label: 'Architecture', slug: 'explanation/architecture' }, + { label: 'Records stay home', slug: 'explanation/records-stay-home' }, { label: 'Boundaries and map', slug: 'map/boundaries-and-map' }, { label: 'Consultation flow', slug: 'explanation/consultation-flow' }, { label: 'Evidence issuance', slug: 'explanation/evidence-issuance' }, + { label: 'Disclosure modes', slug: 'explanation/disclosure-modes-and-computed-answers' }, { label: 'Trusted context', slug: 'explanation/trusted-context-constraints' }, { label: 'Integration patterns', slug: 'explanation/integration-patterns' }, { label: 'DPI safeguards', slug: 'explanation/dpi-safeguards-alignment' }, ], }, + { + // Trust & Security rail (roadmap §6): the reviewer/auditor-facing + // posture, threat model, privacy story, and the canonical limits hub. + label: 'Trust & security', + collapsed: true, + items: [ + { label: 'Trust posture', slug: 'explanation/trust-posture-and-security-guarantees' }, + { label: 'Threat model', slug: 'explanation/threat-model' }, + { label: 'Data minimization', slug: 'explanation/data-minimization-and-purpose-limitation' }, + { label: 'Known limitations', slug: 'explanation/known-limitations' }, + ], + }, { label: 'Reference', collapsed: true, diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx new file mode 100644 index 00000000..8ab9644b --- /dev/null +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -0,0 +1,87 @@ +--- +title: Data minimization and purpose limitation by design +description: How the Registry Stack architecture supports data minimization and purpose limitation, where those supports depend on operator configuration, and which data-subject-rights obligations it does not address. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc +--- + +As a data protection officer or privacy reviewer, you are asked whether a system supports the principles you work to — data minimization, purpose limitation, accountability — and where those supports stop. This page answers one question about Registry Stack: how does its architecture support data minimization and purpose limitation by design, where are those supports conditional on how an operator configures a deployment, and which data-protection obligations does the architecture not address at all? + +Two things this page does not do. It makes no compliance claim: conformance to the underlying specifications does not imply conformance to any external standard, nor to GDPR or any data-protection regulation. And it gives no legal advice and no jurisdiction-specific reading — that judgement remains yours. The specifications here are all draft, pre-1.0 documents and may change, so treat the design as a posture under development rather than a finished or warranted product. + +A note on terms, since you do not need Registry Stack internals to follow this. A *claim* is one pre-modelled question — one decision or one extracted value — that a system can ask about a person against a registry the institution already holds. A *source binding* is the configured rule that connects a claim to the specific source fields it reads. *Minimized evidence* means a response shaped by data minimization or selective disclosure; a *purpose-bound request* is a request that carries or is evaluated against a stated purpose. Those last two are the principles you already know, expressed in the system's own vocabulary. + +## How the claim model limits collection at the source + +The first place minimization appears is in the unit of data the system is built around. A claim definition describes one decision or one extracted value — not a whole record — so returning a full record would over-collect relative to the question actually asked. The design treats the narrow question, not the record, as the thing to be answered. + +That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads only the fields its rule needs, and a request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. + +## Purpose limitation as an enforced gate, not a label + +Purpose limitation in this design is more than a field written into a log. It is enforced as a policy decision before any source is read. Where a source is configured to require a purpose, a request that omits the purpose header is rejected, and the supplied purpose is recorded in the audit record. Beyond recording it, the evaluation component combines the claim's permitted purposes with the source binding's permitted purposes and denies the request before source access if either set rejects the stated purpose. + +A matching policy carries the same idea further: it binds a source read to a declared purpose and relationship context and constrains which inputs may identify the subject, and a request whose purpose, relationship, or inputs the policy does not admit is refused before the source is touched. The effect a reviewer should take away is that purpose, when configured, acts as a precondition for access rather than an after-the-fact annotation. + +The phrase "when configured" is load-bearing, and the section on operator responsibilities returns to it. + +## Minimization at the output: disclosure modes and selective credentials + +Minimization also applies to what leaves the boundary. A claim's result can be shaped into one of three disclosure modes: *value* returns the full value, *predicate* returns only the true/false satisfaction, and *redacted* hides the value entirely. A privacy-sensitive claim is expected to default to the least-revealing mode that still answers the question. (How a mode is selected and policy-bound is covered in [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) and is out of scope here.) + +The redacted mode is worth a reviewer's attention because it shows minimization without loss of accountability: a redacted result carries neither the underlying value nor the satisfaction outcome, yet the evaluation stays referenceable and verifiable through an evaluation identifier, a verification identifier, and a claim hash. You can audit that an evaluation happened and verify it later without the result itself disclosing anything about the person. + +When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC — a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Holder binding — tying the credential to a holder key with `did:jwk` — is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. + +A further minimization detail guards against a subtler leak. A failed subject match collapses by default to a single public reason (evidence not available), with the granular reason kept only in the audit record, so the lookup surface cannot by default be used to confirm whether a person exists in a registry. + +## Data stays at source: distributed custody + +Underneath all of this is an architectural choice that matters for minimization at the system level: data stays at source. The design premise is distributed custody — the stack provides an API surface for lawful exchange between authorities and does not aggregate data into a central system. The read component must not mutate source registry data and exposes no write-back route, and runtime services are read-only in this version, with no source-registry data-mutation routes at all. + +For a reviewer this has a direct consequence. Because the architecture cannot alter or delete source records, it is also why no erasure flow exists at the source layer: the design has no mechanism to reach into and change the registry it reads from. That is described more fully in the next section as a limit, not a feature. + +One related boundary is positive rather than cautionary. The portable metadata the stack publishes must not carry person-level data or runtime secrets and bindings, which is precisely why those artifacts are safe to share — they hold no personal data. But publishing metadata only describes; it does not authorize access, enforce a policy, or assert that any record exists, so a published manifest carries no data-protection guarantee about live data. + +## Audit as an accountability primitive + +Accountability is supported through audit, which is treated as a security control rather than an optional log. Every request that returns person-level records or claim results must be recorded with at least the caller principal, a request identifier, and the purpose value where one was supplied, and a deployment can run audit fail-closed so that a request whose audit record cannot be written does not succeed. The security model expects the scopes exercised to be recorded too; in practice Relay's audit record captures them, but Notary's audit record does not yet include that field — a gap noted in the limitations inventory. + +One precise reading matters here. Audit fail-closed is a capability a deployment can turn on — not a guarantee that every route in any given build has been individually audited against it. Whether a particular deployment meets it is something a reviewer verifies in that deployment, not something the design asserts on its behalf. + +## What the architecture does not provide + +Some of the most important things for a DPO to know are the absences. Stating them plainly prevents the design from being read as more than it is. The full, canonical inventory is in [Known limitations and non-guarantees](../known-limitations/). + +- **No data-subject erasure or right-to-be-forgotten workflow.** There is no built-in erasure or deletion flow anywhere in the design, so it does not satisfy erasure obligations on its own. As noted above, the read-only design cannot mutate source records, so erasure, where it is required, remains an operation on the source registry outside this system. +- **No rectification or data-subject-rights flow.** Beyond erasure, there is no rectification or general data-subject-rights mechanism. +- **No credential revocation service.** There is no revocation flow. Key rotation exists — a rotated-out key may remain published so existing results stay verifiable — but that is not a way to revoke an already-issued credential. Don't count on revocation as a supported capability. +- **No broad cross-authority exchange beyond static peering.** Federation between authorities is static-peer only; dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of scope, so the design supports a narrower cross-authority data-exchange surface than the word "federation" might suggest. +- **No privacy-budgeted analytics.** Aggregate routes produce statistical outputs, not a longitudinal privacy budget. A data-protection impact assessment should not describe an aggregate route as privacy-budgeted unless a separate, deployed control actually provides that. +- **No compliance claim.** Conformance to these specifications does not imply conformance to any external standard or any data-protection regulation, and the specifications themselves are draft. + +## Operator responsibilities: what the design leaves to you + +The minimization and purpose-limitation posture described above is the design default, but the operator owns the configuration. Several of the protections are conditional, and a reviewer should test the deployment, not the design, for each one. + +The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding falls back to unrestricted, identifier-only resolution with no purpose gating, no relationship gating, and no input minimization. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. + +The existence-oracle protection is also defeasible. The matching surface collapses failures to a single public reason by default, but a deployment may disable that collapse and surface not-found, ambiguous, or rejected outcomes — an over-disclosure risk the operator controls. + +More broadly, the architecture defines primitives and leaves a large set of data-protection-relevant controls to the operator. Secret and key provisioning, audit retention and storage, tenant isolation, transport security, edge rate limiting, deployment configuration, and incident response are not defined by the design; they are responsibilities you provision and verify in a deployment. The clear-eyed view, then, is this: Registry Stack offers minimization, purpose limitation, and accountability as enforceable design primitives, but whether a given deployment realizes them — and whether it meets any legal obligation — depends on how the operator configures and runs it. + +## Related + +- [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) — how disclosure modes are selected and policy-bound +- [Threat model](../threat-model/) — the boundaries, assets, and threats behind this posture +- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/) — the high-level security summary +- [Known limitations and non-guarantees](../known-limitations/) — the full inventory of edges +- [Records stay home](../records-stay-home/) — what stays inside the institution's boundary and what crosses it +- The security and protocol specifications: [RS-SEC-G](../../spec/rs-sec-g/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx new file mode 100644 index 00000000..a638153c --- /dev/null +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -0,0 +1,87 @@ +--- +title: Disclosure modes and computed answers +description: Why Registry Notary can answer a question about a subject by returning a computed result — value, predicate, or redacted — instead of handing back the source record. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: [] +--- + +You are weighing whether Registry Stack actually keeps a sensitive registry record private while still answering questions about the subjects in it. The short answer is that a caller asks a question and receives a *computed answer*, not the record. This page explains how that works, what each kind of answer does and does not reveal, and where the privacy claim has edges. + +You will see one product term used throughout: a **claim**, which is a single pre-modelled question — one decision or one extracted value, such as "is this person registered?" or "what is this person's registration date?". A claim is deliberately narrow: a claim that tried to return a whole record would over-collect and be hard to authorize, which is exactly the outcome this design avoids. + +## How Registry Notary controls what leaves the service + +Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. They are the only modes — there is no fourth mode and no gradation between them. + +Per-claim disclosure control is enforced at runtime by the service. It is not left to a caller's good behaviour. + +## The evaluation pipeline: computed answers, not record handoffs + +The reason an answer can stand in for a record is that the caller never supplies the value being asked about. To evaluate a claim, the caller sends a **subject identifier** and a **claim id** over a REST call authenticated the way the rest of the service is — an API key or bearer token in static-credential mode, or an OIDC token, whichever your deployment is configured for. Registry Notary then performs the evaluation against its own configured sources. The caller does not supply the evaluated value, and cannot inject one. + +The evaluation runs as a pipeline: a source connector resolves facts about the subject, a rule computes the configured condition, a disclosure mode shapes what leaves the service, and a response format carries the result. Because Notary computes the answer itself, it can return that computed answer rather than handing back the source record it read. + +The rule that does the computing is one of three kinds. An **exists** rule checks for the presence of **exactly one** matching source record. An **extract** rule reads a value from a source field. A **cel** rule derives a value from source fields or earlier claim results through a hardened expression. Each kind produces an answer about the subject without that answer being a copy of the source row. + +## The three disclosure modes + +The disclosure mode fixes how much of the computed answer the caller receives: + +| Mode | What the caller receives | +|------|--------------------------| +| `value` | the full evaluated value | +| `predicate` | only the true/false satisfaction of the rule | +| `redacted` | no value and no satisfaction outcome | + +`value` returns the full value, so this mode does hand over the computed result in full — it is the least private of the three and is appropriate only where the use case calls for the actual value. `predicate` returns only the boolean satisfaction. `redacted` returns neither the underlying value nor the yes/no outcome. + +Every evaluation result records which disclosure mode was applied, so a downstream system can tell how much was revealed. + +## Why predicate and redacted avoid sharing the underlying record + +This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` or `false`, and the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. + +A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. + +The difference between the two matters. `predicate` is not "zero disclosure" — it still tells the caller a true-or-false fact about the subject. `redacted` goes one step further: it does not leak even a yes/no. They are not interchangeable. + +## How disclosure policy is configured per claim + +The caller does not freely pick a mode. Each claim configures a **disclosure policy**: a `default` mode plus an `allowed` set drawn from the three modes. When a caller requests a mode, Notary refuses any mode not in that claim's allowed set; when the caller requests no mode, Notary applies the claim's default. The mode is bound to the claim's policy, not the caller's choice. A privacy-sensitive claim can be configured to default to the least-revealing mode that still answers the question. + +## What a redacted result still reveals (and to whom) + +"Reveals nothing to the caller" is not the same as "leaves no trace." A `redacted` evaluation can still be pointed back at: it carries an `evaluation_id`, and the audit record carries a `verification_id` and a `claim_hash`. That lets you reference the evaluation that happened — it is not a cryptographic signature on the answer. Redaction shapes what the caller sees; it does not erase the fact that an evaluation happened. + +That trace is by design. Every evaluation — redacted or not — is audited, and a deployment can run audit fail-closed, so that a request whose audit record cannot be written does not succeed. So redaction does not mean an unlogged request; it means an unrevealed answer. + +There is a related protection on the matching surface. By default, a matching failure collapses to a single public reason, `evidence.not_available`, so the lookup cannot be used as an existence oracle — a "not found" answer does not tell the caller whether a record exists. This complements `redacted` disclosure by not leaking record existence. A deployment can turn this off, but only in a controlled environment; it is not always on. + +## Disclosure control is one safeguard among several + +Disclosure modes are one runtime safeguard within a broader posture that also includes authentication, scoped routes, no source mutation, and audit. They are not the whole privacy guarantee. A few protections are not the software's to make at all: secret and key custody, audit retention, tenant isolation, transport, and rate limiting are your deployment's job, not built-in guarantees. + +## Honest limits + +A few boundaries matter when you evaluate this design: + +- **Minimization is conditional, not absolute.** A minimized answer can be narrower than the full source record only where the configured use case supports that pattern. Notary does not always return less than the record — `value` mode returns the full value, and a claim reveals only what its policy was configured to reveal. +- **Governance depends on configured policy.** A source binding with no matching policy falls back to unrestricted, identifier-only resolution, with no purpose, relationship, or input-minimization gating. Minimization is not automatic; it follows the configuration. +- **The caller cannot inject a value, but matching strictness is configured.** The "computed answer avoids sharing the record" guarantee rests on Notary computing the answer from a subject id and claim id. Identity matching is only as strict as its configured policy. +- **A sidecar token is not a per-subject boundary.** A source adapter sidecar's internal static bearer token is not by itself an end-user, tenant, or subject authorization boundary; the sidecar hop does not enforce per-subject access. +- **Disclosure modes are not credential selective disclosure.** A holder's later choice of which credential fields to present is a separate mechanism from Notary's three evaluation modes. Do not read `value`/`predicate`/`redacted` as the same thing as a wallet holder selectively disclosing credential fields. +- **Aligning with a standard is not conforming to it.** This page describes Notary's own disclosure behaviour. It is not an endorsement of, or conformance to, any outside specification. + +## Related + +- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/) — the full posture this fits into +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) — the privacy view +- [Known limitations and non-guarantees](../known-limitations/) — where the edges are +- [Records stay home](../records-stay-home/) — what stays inside the institution and what crosses out +- How a claim and its disclosure policy are defined: [RS-DM-CLAIM](../../spec/rs-dm-claim/); disclosure modes, credentials, and federation: [RS-PR-NOTARY](../../spec/rs-pr-notary/) diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx new file mode 100644 index 00000000..0fe96e48 --- /dev/null +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -0,0 +1,119 @@ +--- +title: Known limitations and non-guarantees +description: The canonical, linked inventory of what Registry Stack does not yet do or guarantee, so you know exactly where the boundaries are before committing. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc + - oid4vci + - cccev + - verifiable-credentials + - odrl +--- + +You already know what Registry Stack is for: answer a question about a record without sharing the underlying record. This page answers a different question — *what does Registry Stack not yet do or guarantee?* — by giving you, in one place, the honest list of where the boundaries are. + +## What this page is and how to use it + +This is the canonical inventory of current limitations and non-guarantees across the whole stack. Each entry states the boundary plainly and links to where it is discussed in depth, so the rest of the documentation can point here whenever it says "this is not X." + +The limitations are not invented for this page. Each protocol and data-model specification carries its own limitations section; this hub collects those and links to them so you can read any one in full context. + +Three things are out of scope here, and each is a link instead: + +- **Why a given limit exists** — read the relevant specification. +- **Whether it will change** — see the project roadmap (`ROADMAP.md` in the repository root). This page describes current behavior only. +- **How to work around a limit** — mitigations are not covered here. Follow the linked specification for each boundary, and see `release/VERIFY.md` in the repository for release verification. + +Use this page as a checklist before you commit. If a limit below is a dealbreaker for your deployment, better to find that out before integrating than after. + +## Everything here is draft + +Every governing specification — architecture, security, protocol, and data-model — is currently at lifecycle status `draft`: under development or review, not `current` (in force). The contracts these limitations are stated against are themselves not yet finalized. + +Registry Stack is a pre-1.0 technical release for evaluation, integration pilots, and public review. It is not a production support commitment and carries no hosted service-level agreement. + +Two things follow, and both matter as you read the rest of the page. First, the behaviors and boundaries here may change as the specifications move toward `current`. Second, aligning with an external standard is not the same as conforming to it: speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, or the rest does not certify conformance to any of them. The [standards register](../../reference/standards/) records how each one is actually adopted. + +## Stack-wide boundaries at a glance + +These boundaries cut across components. Read them first. + +- **No source mutation, no event stream.** Registry Relay does not write back to or otherwise mutate source registry data, and it has no event-stream backend; sources are read as batch snapshots or table scans only. Registry Notary likewise has no file-based or database connector — a source connector reaches its target over HTTP, so the connector-to-target hop is HTTP-only. +- **Aggregate routes are not a privacy budget.** Relay aggregate routes return the configured statistical observations, but provide no built-in longitudinal privacy budget and do not track cumulative disclosure across repeated or overlapping queries. Do not describe an aggregate route as privacy-budgeted unless a separate deployed control provides that protection. +- **A published manifest grants nothing.** Publishing a dataset, policy, evidence offering, or federation relationship in a discovery artifact grants no access, enforces nothing, and asserts no fact about a live record. Federation discovery metadata is not a trust anchor: a published manifest does not bind a consumer to trust a JWKS endpoint, federation API host, or peer identity. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- **Registry Lab and its hosted instance are demonstrations, not infrastructure.** The local Registry Lab is a compose-based runnable demonstration that uses fixture credentials and demo-grade configuration and ships no production deployment guidance. Any hosted instance of the lab is likewise a demonstration, not production infrastructure: it carries no uptime or data-retention commitment. + +For the rationale behind each of these, follow the linked specifications in the sections below. + +## Credential issuance and lifecycle limits (Notary) + +Registry Notary issues credentials, but the issuance surface is deliberately narrow. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/). + +- **One credential format, one binding method when binding is enabled.** Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. +- **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. Discovery declares this explicitly as `openid4vci.support: not_full_issuer` — that is, it announces it is not a full issuer. +- **No delegated-attestation issuance.** Delegated-attestation transaction tokens are rejected by the credential endpoint; this profile issues only for direct self-attestation principals in this version. +- **No revocation, no issuer-metadata discovery endpoint, no erasure workflow.** At this version there is no credential revocation service, no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. +- **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. +- **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. + +## Federation limits + +Federation across institutions is static-peer only. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/) and the [security model](../../spec/rs-sec-g/). + +- **Static peers only.** There is no dynamic trust-chain discovery, no shared replay storage, and no federated credential issuance. All three are out of scope for this version. +- **Delegated evaluation returns a result, never a credential.** Across a federation boundary, what crosses is a scoped evaluation result — never an issued credential. + +## Registry Relay limits + +Registry Relay exposes protected, read-only registry routes. Its boundaries are covered in depth in the [Registry Relay protocol](../../spec/rs-pr-relay/). + +- **Read-only, batch or scan.** Relay does not mutate source data in v1 and has no event-stream backend; it reads sources as batch snapshots or table scans only. +- **Feature-gated surfaces.** Several Relay surfaces — the OGC API Features, Records, and EDR adapters, the SP DCI sync adapter, and signed response credentials — are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. +- **Aggregates are not privacy-budgeted.** As noted stack-wide, Relay aggregate routes provide no longitudinal privacy-budget or cumulative-differencing protection. +- **Relay does not evaluate or own the manifest.** Relay does not perform claim evaluation or issue credentials — that is Notary's role — and it does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. +- **Governed enforcement is one PDP profile, not full interoperability.** Governed runtime policy enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1` — the single PDP profile this build recognizes). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of [ODRL](../../reference/standards/) terms outside the supported set. + +## Data-model limits (Claim and Manifest) + +These limits concern what the claim and manifest configuration loaders do and do not enforce. Read them in full in the [Claim data model](../../spec/rs-dm-claim/) and [Manifest data model](../../spec/rs-dm-manifest/). + +- **The `plugin` rule type is unimplemented.** The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. +- **Load-time invariants are not all enforced.** The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. These surface as request or evaluation errors at runtime, not as startup failures. +- **Identifier-only matching has no further gating.** A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. +- **Manifests describe; they do not enforce.** The portable metadata layer only describes — publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- **Rendered standards artifacts are well-formed, not certified.** Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. +- **Unknown manifest keys are ignored, not rejected.** Manifest runtime-binding exclusion is an enumerated list, not a deny-unknown-fields schema: an unrecognized key outside the runtime-only list is ignored rather than rejected. +- **Vocabulary influence is not vocabulary emission.** PROV-O is a design influence only: provenance-shaped concepts appear in audit fields and the claim provenance struct, but no PROV-O vocabulary terms are emitted as JSON-LD. No standalone SKOS artifact is published yet — only embedded SKOS-shaped nodes — and CPSV-AP has no Relay runtime metadata route. + +## Security-model operator boundary + +A healthy, reachable, internally consistent deployment is not the same as a production-secure one. Read the full security boundary in the [security model](../../spec/rs-sec-g/). + +- **Health checks do not certify key custody.** Readiness, liveness, and protocol-conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and key-provider approval remain operator responsibilities. +- **Many guarantees are the operator's to provide.** Secret and key provisioning, key custody and rotation schedule, audit retention and storage, tenant isolation, transport termination, edge rate limiting, deployment configuration, and incident response are operator responsibilities — not behaviors the stack guarantees. +- **Audit fail-closed is a capability, not a guarantee about every build.** A deployment can run audit fail-closed, so that a request whose audit record cannot be written does not return success. That is a switch a deployment is able to turn on — not a promise that every route in a given build has been individually audited, nor that every build ships with it on. +- **The Notary audit record omits the scopes field.** The security model says an audit record captures at least the caller principal, the scopes exercised, a request identifier, and the purpose. Notary's audit record body records the principal, request id, and purpose, but does not include the caller's exercised scopes — so a downstream review of Notary audit alone cannot reconstruct which scopes authorized a request. (Relay's audit record does record scopes.) +- **The source-adapter sidecar is an internal connector, not a public API.** The source adapter sidecar is an internal connector surface, not a public subject-facing API. Its static bearer token is not by itself a tenant or subject authorization boundary, and it relies on deployment-network egress controls for outbound source traffic. Direct public access, broad reuse of one sidecar token across unrelated sources, or shared cache or backoff state across authorization contexts is outside the security model. +- **Demo and template code is not a production profile.** Demo helper code and generated workflow snippets are integration examples, not a production freshness or replay-protection profile, and must not be relied on for freshness, expiry, or replay protection. + +## Supply-chain and release limits + +These concern how releases are signed and what that signing currently covers. + +- **Partial release signing.** GitHub Release assets are signed with keyless cosign and, when tag-triggered, carry SLSA provenance. However, OCI image signatures are not yet published, and Git version tags are not yet cryptographically signed (GPG, SSH, or Sigstore). The `v0.8.0` prerelease was published before release-asset signing and does not currently include cosign signatures. + +For how to verify what *is* signed, see the release verification guidance in `release/VERIFY.md` in the repository. + +## Where to go next + +- **Why a given limit exists** — the rationale and trade-offs live in each specification's own limitations section: [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), and [RS-DM-MANIFEST](../../spec/rs-dm-manifest/). For the security and architecture boundaries, read [RS-SEC-G](../../spec/rs-sec-g/) and [RS-ARC-G](../../spec/rs-arc-g/). +- **How each external standard is actually adopted** — the [standards register](../../reference/standards/) records the adoption mode for every standard named across the stack. +- **What might change** — the project roadmap (`ROADMAP.md` in the repository root). This page does not track it. +- **How to verify what is signed** — release verification lives in `release/VERIFY.md` in the repository. diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx new file mode 100644 index 00000000..748ca9bb --- /dev/null +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -0,0 +1,246 @@ +--- +title: Threat model +description: The trust boundaries, assets, and threats the Registry Stack design considers — what it mitigates, and the residual risks it leaves to the operator or treats as out of scope. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc + - oid4vci + - w3c-did + - odrl +--- + +This page is for a reviewer or auditor who already reasons in terms of assets, trust +boundaries, and adversaries, but does not yet know where the Registry Stack's boundaries +actually sit. It answers one question: **why does this design produce the security +properties it claims — which threats does the architecture consider, what does it actually +mitigate, and where do the residual risks lie?** + +It is deliberately a map of boundaries and limits, not a runbook. Hardening procedures +belong to the Operate section, the high-level posture summary to the Trust & Security +overview, and privacy obligations to [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/). Here the goal is a +defensible threat model: the boundaries the design draws, the threats it places in and out +of scope, and the residual risks stated honestly. + +## The two-layer architecture and its primary trust boundary + +The stack is a two-layer design, and the split between the layers is the first trust +boundary the model relies on. A **portable metadata layer** only *describes*: it carries no +production data, no authentication, and no secrets. A **runtime services layer** *enforces*: +it is where authentication, authorization, and disclosure actually happen. + +The consequence for a reviewer is that the metadata layer authorizes nothing, enforces +nothing, and does not assert that any record exists or satisfies a claim. Publishing a +dataset, a policy, an offering, or a federation relationship does not grant access and is +not proof that a record exists; a published ODRL policy only describes intent until a +runtime service binds it into an enforced profile it supports. This also closes one threat +directly: because the metadata layer is meant to be distributed and inspected, secrets are +never embedded in it. The operator injects them at deployment, which keeps secret material +out of the portable artifact entirely. + +## Components and where they sit on the boundaries + +Five components sit on or beside the trust boundaries: + +- **Registry Manifest** — the offline metadata producer. No production data, no auth, no + secrets. It lives entirely on the describe side of the primary boundary. +- **Registry Relay** — a read-only consultation gateway over sources. +- **Registry Notary** — claim evaluation, credential issuance, and federation. This is the + component that evaluates a modelled question, applies disclosure policy, and issues + credentials. +- **Registry Platform** — the shared security primitives that the runtime services build on. +- **Registry Lab** — demonstration only, running on fixture credentials. Treat it as out of + scope for production trust: its demo and template integrations are integration examples, + not a production freshness or replay-protection profile, and a team copying them into + production must add request freshness, expiry, or nonce checks itself. + +Security-critical primitives are concentrated in Registry Platform so that the behavior is +identical across services and reviewable in one place. This is a deliberate attack-surface +decision: it removes the risk of divergent, per-service security code that would each need +auditing separately. + +## Trust boundaries in detail + +**The service edge (authentication).** Authentication is the trust boundary at the service +edge. Each service runs exactly one authentication mode — either static-credential +fingerprint or OIDC — and authenticates every claim- or record-bearing route before +responding. The only unauthenticated surfaces are probes and public JWKS / `did:web` +discovery. + +**Notary to its sources.** A runtime boundary sits between Registry Notary and the sources +it reads. Notary calls Relay over HTTP as a data source; it does not link Relay code. The +only connectors are HTTP (`registry_data_api`, `dci`, and the source adapter sidecar) — there +is no file or database connector. + +**The source adapter sidecar.** The sidecar is an *internal* trust boundary, not a +subject-authorization boundary, and this distinction matters for an auditor. Its bearer +token authenticates only the Notary-to-sidecar hop; it is not an end-user, tenant, or +subject authorization boundary. It is an internal, private deployment surface behind Notary, +not a public subject-facing API. Keeping the token off the public network and scoping its +credentials, cache, and backoff state is outside the security model — it is an operator +responsibility — and shared cache or backoff state across authorization contexts is not +something the model reasons about. + +**Federation between two Notary services.** Federation is a deliberate trust boundary +between two Notary services, and it is **static-peer only**: only configured static peers are +admitted. It is not a dynamic trust mesh. + +**Delegated self-attestation.** Delegated self-attestation is a distinct trust context from +peer federation — don't confuse the two. It does not delegate trust to a peer +Notary; it stays inside the citizen / OIDC trust context. + +## Assets protected by the design + +The assets the design sets out to protect are: + +- **Private signing keys.** The private signing key never leaves the issuer; only the public + half is published — through JWKS, and through `did:web` in Relay gateway mode. A verifier + never needs a credential to verify, which keeps verification off the protected side + entirely. +- **Person-level source data.** The source rows behind a claim are read in place and never + cross the boundary; only a computed, disclosure-shaped answer does. +- **Audit integrity.** Audit is treated as a security control rather than best-effort + logging (see below). +- **Secret material.** Bearer tokens, raw credentials, and key material are kept out of both + the portable layer and client-facing surfaces. + +The project itself treats a specific set of threat classes as security-relevant, which is a +useful framing for "threats considered": authentication bypass, credential disclosure, +audit redaction or integrity failure, signing-key handling bugs, source-connector data +leakage, and privacy regressions that expose raw subject identifiers. + +## Threats the design considers and mitigates + +- **Secret disclosure via stored credentials or timing.** Static credentials are stored only + as SHA-256 fingerprints — never the raw secret — and compared in constant time. +- **Token forgery / acceptance of untrusted tokens (OIDC mode).** A token is trusted only + after signature verification against the configured issuer JWKS, plus issuer, audience, and + algorithm checks. The service still owns its route scopes regardless of the token. +- **Privilege escalation / over-reach.** Authorization is scope-based, deny-by-default, and + scope-before-source: a caller is refused before any source read or claim evaluation if it + lacks the required scope, and reach is never widened at request time. +- **An existence / matching oracle.** By default a matching failure collapses to a single + public reason (`evidence.not_available`), with the granular reason kept only in audit, so + the matching surface cannot be used to probe whether a record exists. A deployment may + disable this collapse only in a controlled environment; disabling it is a residual + confidentiality risk. +- **Information leakage in error and cache surfaces.** `problem+json` error bodies carry + stable codes only. Bearer tokens, private keys, source values, filesystem paths, and + internal error chains stay in protected operator logs, never in responses; principal-scoped + responses are not public cache entries. +- **Over-collection and value injection in claim evaluation.** The caller supplies only a + subject id and a claim id and must not supply the evaluated value; bindings read only the + fields they need and reject input paths outside a declared allow-list. +- **Disclosure leakage.** A redacted result carries neither the value nor the predicate + outcome. Selective-disclosure credentials carry SHA-256 digests of unselected fields, so a + holder cannot present an undisclosed field. Holder binding is configurable per credential + profile and defaults to off; where a profile enables it (using `did:jwk`) — and on the + self-attestation issuance path, which requires it — the holder is bound by a fresh + audience-bound proof-of-possession. Binding applies only to an issued credential; the plain + evaluation result is not a credential and is never holder-bound. +- **Self-attested data crossing a trust gate.** Self-attestation and delegated + self-attestation derive subject, requester, and relationship from the authenticated + principal and scoped authorization details; caller-supplied `requester`, `relationship`, and + `on_behalf_of` fields are rejected before any source read, and self-asserted target fields do + not satisfy trusted-principal gates. +- **Spoofed trust context and ungoverned reads (Relay side).** Relay ignores trust-context + headers unless the principal is scoped to assert that exact value, and a governed read must + receive a PDP permit or fail closed with a stable `pdp.*` code rather than falling back to an + ungoverned read. +- **SSRF / uncontrolled egress.** Outbound source fetches are meant to be constrained by Registry + Platform's outbound-HTTP-policy. Where it is applied, it limits the destinations a source + connector may reach. This is recommended posture rather than a hard guarantee — confirm it + is enabled rather than assuming it. One detail an auditor should note: the guard exposes an + `allow_insecure_private_network` opt-in that, per source connection, re-enables plain-HTTP + private-network and link-local destinations the strict policy would otherwise refuse. Even + with that opt-in, cloud-metadata addresses (such as `169.254.169.254` and its IPv6 + equivalent) stay denied. Treat the opt-in as a deployment decision to review, not a default. +- **Untrusted federation peers, replay, and wrong-purpose requests.** Only configured static + peers are admitted, and before any source read the server verifies peer identity, request + signature, freshness, single use, purpose, profile, and audience. The single-use and + freshness checks are backed by a real replay-store primitive that tracks one-time JWT `jti` + and nonce values (federation request `jti`, OID4VCI `c_nonce`, and holder-proof `jti`). + +**Audit as a control.** Every request touching person-level data is recorded — principal, +request id, and `Data-Purpose` — and a deployment can run audit fail-closed, so an +unrecordable request does not return success. Two caveats for the auditor. First, this is a +capability a deployment can turn on, not a guarantee that every route in a given build has +been individually audited; treat per-route audit coverage as something to verify in your +deployment. Second, the security model expects the audit record to capture the scopes +exercised as well, but Notary's audit record does not yet include that field — Relay's does — +so Notary audit alone cannot reconstruct which scopes authorized a request. + +## Residual risks and what is left to the operator + +The canonical inventory of every current limit is [Known limitations and non-guarantees](../known-limitations/); the residual risks below are the subset a threat model must weigh. + +These are the risks the design does *not* close and states honestly: + +- **Key custody is not certified by health checks.** Readiness, liveness, and conformance + checks do not certify production-grade private-key custody. A deployment using software + keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet + not production-secure. Custody, rotation, and provider approval remain operator + responsibilities. +- **The operator boundary is where coverage ends.** Secret and key provisioning, key custody + and rotation, audit retention and storage, tenant isolation, TLS termination and + certificates, edge rate limiting, deployment configuration, and incident response are + operator responsibilities, not behavior the model defines. +- **The sidecar depends on deployment-network controls.** The source adapter sidecar relies + on deployment-network egress controls; the model does not enforce them. +- **Aggregate routes are not a privacy budget.** Relay aggregate routes do not constitute a + longitudinal privacy budget; v1 does not track cumulative disclosure across repeated or + overlapping aggregate queries. Do not describe a Relay aggregate as privacy-budgeted unless + a separate deployed control provides it. This is a residual disclosure risk left to the + operator. +- **Some claim-definition invariants surface only at runtime.** Duplicate claim ids, a + disclosure default outside the allowed set, a rule source naming an undeclared binding, and + the unimplemented plugin rule are not enforced at configuration load — they surface only at + evaluation or request time. A source binding with no matching policy resolves on identifiers + alone, with no purpose, relationship, or input-minimization gating. These are residual + misconfiguration risks left to the operator. +- **Known product gaps.** There is no revocation service, no `/.well-known/jwt-vc-issuer` + endpoint, and no built-in data-subject erasure workflow in this version. +- **Admin reload is non-functional standalone.** Notary's admin reload route returns HTTP 501 + (`registry.admin.capability.not_supported`) in the standalone router and performs no + reload; key and configuration changes require a service restart. Do not treat hot reload as + a capability. + +## What is explicitly out of scope + +These are non-goals for this version. None of them should be read into a Registry Stack +conformance claim: + +- **Dynamic federation.** Dynamic trust-chain discovery, shared replay storage, audit + checkpoint exchange, and federated credential issuance are out of scope. Federation is + static-peer only — not a dynamic trust mesh. +- **Full Evidence Gateway / external-policy interoperability.** Governed Relay PDP enforcement + covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`). + It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not + dynamic external policy discovery, and not enforcement of ODRL terms outside the supported + profile. Unsupported ODRL terms or invalid policy identity fail closed. +- **A full credential issuer.** The OID4VCI surface is a scoped self-attestation issuance + subset of OID4VCI Draft 13, not a full issuer or general external-wallet interoperability. + Notary advertises `openid4vci.support: not_full_issuer`, and delegated-attestation + transaction tokens are rejected at the credential endpoint. +- **Certified standards compliance.** Aligning with a standard is not conforming to it: + speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, W3C DID, or the rest does + not certify conformance to any of them, and CCCEV-shaped output is not conformant to CCCEV + 2.00. +- **Feature-gated surfaces of other builds.** Several runtime surfaces — OGC API + Features/Records/EDR adapters, SP DCI sync, and signed response credentials — are + feature-gated and mount only when the build is configured with the matching Cargo feature. + Do not infer their presence from the route catalog of a different build. Admin routes sit on + a separate optional listener and are not in the public OpenAPI document. + +## Related + +- [RS-SEC-G](../../spec/rs-sec-g/) — the security model +- [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) — protocol contracts +- [RS-ARC-G](../../spec/rs-arc-g/) — the two-layer architecture +- [RS-DM-CLAIM](../../spec/rs-dm-claim/) — claim definitions and disclosure +- Hardening procedures *(Operate)* · Trust posture summary *(Trust & Security overview)* · [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx new file mode 100644 index 00000000..89232e5a --- /dev/null +++ b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx @@ -0,0 +1,135 @@ +--- +title: Trust posture and security guarantees +description: A plain account of what Registry Stack protects for you, what it recommends you confirm, and what it deliberately leaves to your deployment. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc + - oid4vci + - cccev + - w3c-did + - odrl +--- + +Before you adopt Registry Stack you want a straight answer to three questions: what does it +guarantee about security and privacy, what does it leave for you to get right, and what does +it deliberately *not* do? This page answers all three in plain terms. It is not a substitute +for the specifications — at the end you'll find links to the exact documents where each +property is defined — but it is the honest summary you can read first. + +## At a glance + +| What | Where it stands | +|------|-----------------| +| Authentication on every route that touches a record or a claim | **Built in** | +| Deny-by-default authorization, checked before any data is read | **Built in** | +| Only the public half of a signing key is ever published | **Built in** | +| Person-level access is written to an audit log | **Built in** (fail-closed is yours to switch on) | +| Three disclosure modes, with `redacted` revealing neither value nor answer | **Built in** | +| Selective-disclosure credentials, with holder binding configurable per profile (off by default) | **Built in** | +| Federation limited to peers you configure | **Built in** | +| Hardened HTTP headers and outbound-traffic limits | **Recommended** — confirm your build applies them | +| Key custody, retention, isolation, TLS, rate limiting | **Your deployment's job** | +| Compliance with any external standard it cites | **Not claimed** | + +## What's built in + +**Nothing answers without authenticating first.** Every route that returns a record or a +claim result checks the caller's credential before it does anything else. A service runs one +authentication mode at a time — either API-key fingerprints (it stores a hash, never the raw +key, and compares in constant time) or OIDC tokens (it checks the signature, issuer, audience, +and algorithm). The only routes open to an anonymous caller are health probes and the public +keys a verifier needs to check a signature. + +**Authorization is deny-by-default, and it happens before any data is touched.** A caller has +to hold the scope a route requires, and that check runs *before* Relay reads a source or +Notary evaluates a claim — so a request can never quietly widen its own reach at the last +moment. + +**Access leaves a trail.** Every request that returns person-level data is recorded with who +asked, a request id, and the declared purpose. (Relay's audit record also captures the scopes +exercised; Notary's audit record does not yet include that field — see the limitations hub.) You +can run audit fail-closed, so that if the audit record can't be written the request doesn't +succeed — that's a switch you turn on for a deployment that needs it, not something that's +always on by default. + +**An answer is shaped, not a copy of the record.** Every claim is answered in one of three +modes: `value` hands back the computed value, `predicate` returns only a yes/no, and `redacted` +returns neither the value nor the yes/no. The mode is fixed by the claim's policy, not chosen +by the caller. [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) +covers this in depth. + +**Credentials are selective, and can be holder-bound.** When Notary issues a credential it's an +SD-JWT VC: the signed body carries hashes of each disclosable field rather than the values, so +the holder reveals only what they choose. Holder binding is configurable per credential profile +and defaults to off — when a profile enables it (using `did:jwk`), the credential is tied to the +holder's own key and can't be presented by anyone else. The self-attestation (wallet) issuance +path requires binding with proof-of-possession, so credentials issued there are always bound. +Anyone can verify a credential against the issuer's published keys without needing a credential +of their own. The plain claim or evaluation result is not a credential, so it is never +holder-bound — binding applies only to an issued SD-JWT VC. Credentials are signed with +EdDSA/Ed25519 by default, with ES256/P-256 also available per profile. + +**Federation only talks to peers you configured.** There's no automatic trust discovery. Peers +are loaded from configuration at startup, anything else is rejected, and a federated request is +checked for identity, signature, freshness, purpose, and audience before any source is read. +What crosses a federation boundary is a scoped answer — never a credential. + +## What's recommended — and worth confirming + +Two things are strong recommendations rather than hard guarantees, so they're worth a moment in +a review: + +- **Shared security primitives.** Authentication, audit, cryptography, and the credential + helpers are meant to come from one shared component (Registry Platform) so they behave the + same everywhere and can be reviewed in one place. The specs recommend this rather than force + it — so confirm a given service actually does it rather than reimplementing its own. +- **HTTP hardening.** Security headers and limits on outbound traffic are recommended posture. + Check that your build applies them rather than assuming it. + +## Where the guarantees stop + +A trustworthy posture is as honest about its edges as its strengths. The full inventory lives in +[Known limitations and non-guarantees](../known-limitations/); the three that matter most for a +trust review are: + +- **Your deployment owns the operational security.** Key custody and rotation, audit retention, + tenant isolation, TLS, rate limiting, and incident response are yours, not the stack's. A + deployment can pass every health and conformance check and still be running on demo keys — so + passing checks is not the same as production-grade key custody. +- **Aligning with a standard is not conforming to it.** Registry Stack speaks several standards' + shapes — SD-JWT VC, OID4VCI, CCCEV, W3C DID, ODRL — but conformance to a Registry Stack spec + does not imply certified conformance to any of them. The OID4VCI and CCCEV surfaces in + particular are deliberately partial subsets. +- **A published catalog grants nothing.** Listing a dataset or policy in a discovery artifact + describes intent; it doesn't grant access or prove a record exists. Only an authorized runtime + read decides that. + +## Verifying a release, and reading the source + +For checking what you're actually running, GitHub release assets are signed with keyless +Sigstore cosign, and tag-built releases also publish SLSA provenance. You can verify both with +`cosign verify-blob` and `slsa-verifier`; the steps are in `SECURITY.md` and `release/VERIFY.md` +in the repository. (Git tags themselves and container images aren't signed yet.) + +Everything above is defined normatively in the specifications, which are the place to go when +you want to check a claim rather than take it on trust: + +- [RS-SEC-G](../../spec/rs-sec-g/) — the security model (authentication, authorization, audit, + key boundary, the operator boundary) +- [RS-PR-NOTARY](../../spec/rs-pr-notary/) — disclosure modes, credentials, and federation +- [RS-PR-RELAY](../../spec/rs-pr-relay/) — the protected read API +- [RS-DM-CLAIM](../../spec/rs-dm-claim/) — how a claim and its disclosure policy are defined +- [RS-ARC-G](../../spec/rs-arc-g/) — the component boundaries these rest on + +## Related + +- [Records stay home](../records-stay-home/) — what stays inside the institution and what crosses out +- [Threat model](../threat-model/) — the boundaries, assets, and threats behind this posture +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) — the privacy view +- [Known limitations and non-guarantees](../known-limitations/) — the full list of edges diff --git a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx index c62fb655..4cca49ec 100644 --- a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx +++ b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx @@ -6,7 +6,7 @@ owner: registry-docs source_repos: - registry-registryctl - registry-relay -last_reviewed: "2026-06-26" +last_reviewed: "2026-06-28" doc_type: tutorial locale: en standards_referenced: @@ -15,9 +15,14 @@ standards_referenced: import QuickstartMeta from '../../../components/QuickstartMeta.astro'; +Registry Relay turns a sensitive tabular source — a spreadsheet or a database table — into a +protected, read-only consultation API, without copying the data out and without you editing a +config file by hand: the CLI writes a working configuration for you. + Use this tutorial to create a local Registry Relay project from a sample Excel workbook. -You will install `registryctl`, start a protected registry API, prove anonymous access is denied, -read spreadsheet data through a secured route, and open the local API reference. +You will install `registryctl`, start a protected registry API, prove an anonymous read is +denied, read spreadsheet data through an authorized request, and confirm that a key without the +right scope is refused. Date: Mon, 29 Jun 2026 06:46:37 +0000 Subject: [PATCH 05/25] docs: correct trust/credential claims against the implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply seven code-grounded accuracy corrections from review of this PR. Each was verified against the notary crates and the generated OpenAPI before editing: - Trust posture: broaden the anonymous-route list — it also includes credential-issuer discovery, the OID4VCI wallet-flow endpoints, the docs, public credential-status reads, and credential type metadata, not only probes and public keys. - Known limitations: qualify the absolute "no credential revocation service" — the RS-* specs define no revocation flow, but an optional, off-by-default credential-status surface can mark a credential revoked; and name the not_full_issuer flag to the /.well-known/evidence-service capability-discovery document rather than the OID4VCI credential-issuer metadata. - Threat model: admin routes ARE documented in the generated OpenAPI (the prior "not in the public OpenAPI document" claim was wrong); standalone mode still returns HTTP 501 for runtime reload. - Data minimization: field minimization is driven by the configured binding.fields (plus lookup/freshness fields), not auto-derived from the rule. - records-stay-home (pilot): the audit-record claim no longer implies Notary records exercised scopes (Relay does; Notary's record does not), and holder binding is qualified as per-profile (default unbound; the wallet path binds). check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../data-minimization-and-purpose-limitation.mdx | 2 +- .../content/docs/explanation/known-limitations.mdx | 4 ++-- .../content/docs/explanation/records-stay-home.mdx | 13 +++++++------ .../src/content/docs/explanation/threat-model.mdx | 5 +++-- .../trust-posture-and-security-guarantees.mdx | 7 +++++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 8ab9644b..364c063f 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -22,7 +22,7 @@ A note on terms, since you do not need Registry Stack internals to follow this. The first place minimization appears is in the unit of data the system is built around. A claim definition describes one decision or one extracted value — not a whole record — so returning a full record would over-collect relative to the question actually asked. The design treats the narrow question, not the record, as the thing to be answered. -That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads only the fields its rule needs, and a request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. +That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads the fields it is configured to project — the declared `binding.fields`, plus the lookup and freshness fields — so "read only what the rule needs" depends on the binding being configured that way rather than being automatically derived from the rule. A request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. ## Purpose limitation as an enforced gate, not a label diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 0fe96e48..0822e49c 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -56,9 +56,9 @@ For the rationale behind each of these, follow the linked specifications in the Registry Notary issues credentials, but the issuance surface is deliberately narrow. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/). - **One credential format, one binding method when binding is enabled.** Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. -- **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. Discovery declares this explicitly as `openid4vci.support: not_full_issuer` — that is, it announces it is not a full issuer. +- **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. The capability-discovery document (`/.well-known/evidence-service`) declares `openid4vci.support: not_full_issuer` — announcing it is not a full issuer; this flag lives there rather than in the OID4VCI credential-issuer metadata. - **No delegated-attestation issuance.** Delegated-attestation transaction tokens are rejected by the credential endpoint; this profile issues only for direct self-attestation principals in this version. -- **No revocation, no issuer-metadata discovery endpoint, no erasure workflow.** At this version there is no credential revocation service, no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, a public status endpoint and an admin status endpoint can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. - **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. - **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. - **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 8696946c..33ef2b05 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -127,9 +127,10 @@ stays home. A credential is not a copy of the record. It is an **SD-JWT VC**: the signed body carries a SHA-256 *digest* of each selectively disclosable field rather than the field value, so a -field the holder does not present stays hidden. It is **holder-bound** — tied to the -holder's key, and not presentable without the matching private key — and the holder chooses -which fields to reveal to which verifier. Anyone can verify it against the issuer's +field the holder does not present stays hidden. It **can be** holder-bound — tied to the +holder's key so it is not presentable without the matching private key — when the issuing +profile enables binding (the wallet issuance path does); the default profile issues an unbound +credential. Either way, the holder chooses which fields to reveal to which verifier. Anyone can verify it against the issuer's published public keys, served without authentication so a verifier needs no credential of its own. The issued credential carries no full record payload. @@ -148,9 +149,9 @@ Security material: a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. - **Every person-level request is audited.** An audit record captures at least the caller, - the scopes exercised, a request id, and the declared purpose where one was supplied. A - deployment can run audit fail-closed, so a request whose audit record cannot be written - does not return success. + a request id, and the declared purpose where one was supplied; Relay additionally records + the scopes exercised (Notary's record does not). A deployment can run audit fail-closed, so + a request whose audit record cannot be written does not return success. ## What this guarantees — and what it does not diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index 748ca9bb..11dc17ca 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -234,8 +234,9 @@ conformance claim: - **Feature-gated surfaces of other builds.** Several runtime surfaces — OGC API Features/Records/EDR adapters, SP DCI sync, and signed response credentials — are feature-gated and mount only when the build is configured with the matching Cargo feature. - Do not infer their presence from the route catalog of a different build. Admin routes sit on - a separate optional listener and are not in the public OpenAPI document. + Do not infer their presence from the route catalog of a different build. Admin routes run on + a separate optional listener and are documented in the generated OpenAPI, but standalone mode + does not implement runtime config reload — it returns HTTP 501. ## Related diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx index 89232e5a..51efa935 100644 --- a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx +++ b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx @@ -43,8 +43,11 @@ property is defined — but it is the honest summary you can read first. claim result checks the caller's credential before it does anything else. A service runs one authentication mode at a time — either API-key fingerprints (it stores a hash, never the raw key, and compares in constant time) or OIDC tokens (it checks the signature, issuer, audience, -and algorithm). The only routes open to an anonymous caller are health probes and the public -keys a verifier needs to check a signature. +and algorithm). The routes open to an anonymous caller are the operational and discovery +surfaces — health and readiness probes, the public verification keys, credential-issuer +discovery and the OID4VCI wallet-flow endpoints, the docs, public credential-status lookups, +and credential type metadata — while everything that returns a record or claim result still +requires a credential. **Authorization is deny-by-default, and it happens before any data is touched.** A caller has to hold the scope a route requires, and that check runs *before* Relay reads a source or From 1f6e72b24488670d8f0aa551c0ec8702c740f7c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 07:02:13 +0000 Subject: [PATCH 06/25] docs: converge disclosure, revocation, and anonymous-surface accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply nine code-verified corrections so four claim-classes read consistently and accurately across the trust/privacy pages (and the records-stay-home pilot): - Revocation: requalify the absolute "no revocation service" as no spec-defined revocation flow plus an optional, off-by-default credential-status surface (public GET + admin POST status endpoints) that can mark a credential `revoked`. (data-minimization, threat-model, records-stay-home; matches the earlier known-limitations fix.) - Anonymous surface: broaden "only probes + keys" to the full auth-exempt set (probes, public verification keys, credential-issuer discovery, OID4VCI wallet-flow endpoints, docs, public credential-status reads, type metadata), and note the federation route is exempt from the API-key/OIDC middleware but still requires a verified peer-signed JWS, so it is not anonymous. (threat-model, records-stay-home.) - value-mode gradation: keep "exactly three modes, no fourth", but note that within `value` mode an object-valued claim can have configured fields redacted (`redaction_fields`), so `value` is not always all-or-nothing. (disclosure-modes.) - Caller choice: the policy bounds which modes are allowed; within the allowed set the caller can request one (refused with claim.disclosure_not_allowed otherwise), and the default applies when none is requested — corrects the "never the caller's choice" overstatement. (disclosure-modes, trust-posture, records-stay-home.) check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- ...ta-minimization-and-purpose-limitation.mdx | 2 +- .../disclosure-modes-and-computed-answers.mdx | 4 ++-- .../docs/explanation/records-stay-home.mdx | 24 +++++++++++-------- .../content/docs/explanation/threat-model.mdx | 14 +++++++---- .../trust-posture-and-security-guarantees.mdx | 4 ++-- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 364c063f..63224aeb 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -62,7 +62,7 @@ Some of the most important things for a DPO to know are the absences. Stating th - **No data-subject erasure or right-to-be-forgotten workflow.** There is no built-in erasure or deletion flow anywhere in the design, so it does not satisfy erasure obligations on its own. As noted above, the read-only design cannot mutate source records, so erasure, where it is required, remains an operation on the source registry outside this system. - **No rectification or data-subject-rights flow.** Beyond erasure, there is no rectification or general data-subject-rights mechanism. -- **No credential revocation service.** There is no revocation flow. Key rotation exists — a rotated-out key may remain published so existing results stay verifiable — but that is not a way to revoke an already-issued credential. Don't count on revocation as a supported capability. +- **No specified revocation flow; an optional status surface.** The specifications define no revocation flow, but the implementation includes an optional, off-by-default credential-status surface — a public `GET /v1/credentials/{id}/status` and an admin `POST /admin/v1/credentials/{id}/status` — that an operator can enable to mark a credential `revoked`. Key rotation exists — a rotated-out key may remain published so existing results stay verifiable — but that is not a way to revoke an already-issued credential. Treat status-based revocation as an operator-enabled capability, not an always-on one. - **No broad cross-authority exchange beyond static peering.** Federation between authorities is static-peer only; dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of scope, so the design supports a narrower cross-authority data-exchange surface than the word "federation" might suggest. - **No privacy-budgeted analytics.** Aggregate routes produce statistical outputs, not a longitudinal privacy budget. A data-protection impact assessment should not describe an aggregate route as privacy-budgeted unless a separate, deployed control actually provides that. - **No compliance claim.** Conformance to these specifications does not imply conformance to any external standard or any data-protection regulation, and the specifications themselves are draft. diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index a638153c..db60246c 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -17,7 +17,7 @@ You will see one product term used throughout: a **claim**, which is a single pr ## How Registry Notary controls what leaves the service -Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. They are the only modes — there is no fourth mode and no gradation between them. +Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. There are exactly three — there is no fourth mode. `value` is not always all-or-nothing, though: when a claim returns an object, `value` mode can have specific fields redacted (configured through `redaction_fields`), and the result carries the object minus those fields plus a list of what was withheld. Per-claim disclosure control is enforced at runtime by the service. It is not left to a caller's good behaviour. @@ -53,7 +53,7 @@ The difference between the two matters. `predicate` is not "zero disclosure" — ## How disclosure policy is configured per claim -The caller does not freely pick a mode. Each claim configures a **disclosure policy**: a `default` mode plus an `allowed` set drawn from the three modes. When a caller requests a mode, Notary refuses any mode not in that claim's allowed set; when the caller requests no mode, Notary applies the claim's default. The mode is bound to the claim's policy, not the caller's choice. A privacy-sensitive claim can be configured to default to the least-revealing mode that still answers the question. +The caller does not freely pick from all three modes. Each claim configures a **disclosure policy**: a `default` mode plus an `allowed` set drawn from the three modes. A caller may request a mode: Notary honours it when it is in that claim's allowed set and refuses it (`claim.disclosure_not_allowed`) otherwise; when the caller requests no mode, Notary applies the claim's default. So the policy bounds which modes are allowed, and the caller chooses among the allowed modes. A privacy-sensitive claim can be configured to default to the least-revealing mode that still answers the question. ## What a redacted result still reveals (and to whom) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 33ef2b05..fb052b88 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -111,10 +111,11 @@ receives. There are exactly three: | `predicate` | only the true/false satisfaction | the underlying value | | `redacted` | neither — the result carries no value **and** no yes/no | the value *and* the outcome | -The mode is policy-bound, not caller's choice: a claim defines an `allowed` set and a -`default`, the service refuses a mode outside the allowed set, and every result records -which mode was applied. A privacy-sensitive claim is expected to default to the -least-revealing mode that still answers the question. +The policy bounds which modes are allowed, and the caller chooses among them: a claim +defines an `allowed` set and a `default`, the service honours a requested mode when it is in +the allowed set and refuses it otherwise, applies the default when none is requested, and +every result records which mode was applied. A privacy-sensitive claim is expected to default +to the least-revealing mode that still answers the question. This is the mechanism behind "prove a fact without sharing the record". To check whether a person has a registered record, model the question as an *existence* rule and disclose it @@ -142,9 +143,10 @@ Security material: - **Scope-before-source, deny-by-default.** A service checks the caller's scope *before* it reads any source or evaluates any claim, and does not widen a caller's reach at request time beyond what its configuration grants. Anything that touches a record or a claim - requires authentication; the routes reachable without it are limited to operational and - discovery surfaces: liveness and readiness probes, the public verification keys, and - credential-issuance discovery metadata. + requires authentication; the routes reachable without it are the operational, discovery, + and protocol-bootstrap surfaces: liveness and readiness probes, the public verification + keys, credential-issuance discovery metadata, the OID4VCI wallet-flow endpoints, the docs, + public credential-status reads, and credential type metadata. - **A permit, or a closed door.** On a governed read, the policy decision point must return a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. @@ -177,9 +179,11 @@ mistake, so the limits are stated plainly here. - **This is not zero-knowledge.** A `predicate` answer is a policy-enforced boolean computed inside the service; SD-JWT selective disclosure is digest omission. Neither is a zero-knowledge proof, and the documentation should not imply one. -- **No revocation or erasure flow is defined.** This version specifies issuance, - disclosure, presentation, and verification, but not credential revocation or - data-subject erasure. A key rotated out may remain published so existing results stay +- **No revocation flow is specified, and no erasure flow.** This version specifies issuance, + disclosure, presentation, and verification, but no credential-revocation flow and no + data-subject erasure. An optional, off-by-default credential-status surface can be enabled + to mark a credential `revoked`, but that is an operator-enabled capability rather than a + specified flow. A key rotated out may remain published so existing results stay verifiable — that is not a revocation mechanism. - **Several guarantees are the operator's to provide.** Network egress limits, key custody, tenant isolation, audit retention, and transport security are supplied by the deployment, diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index 11dc17ca..ad1f1d76 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -69,8 +69,12 @@ auditing separately. **The service edge (authentication).** Authentication is the trust boundary at the service edge. Each service runs exactly one authentication mode — either static-credential fingerprint or OIDC — and authenticates every claim- or record-bearing route before -responding. The only unauthenticated surfaces are probes and public JWKS / `did:web` -discovery. +responding. The unauthenticated surfaces are the operational, discovery, and +protocol-bootstrap routes: liveness and readiness probes, the public verification keys +(JWKS / `did:web`), credential-issuer discovery, the OID4VCI wallet-flow endpoints, the +docs, public credential-status reads, and credential type metadata. The federation +evaluations route is exempt from the API-key/OIDC middleware but still requires a verified +peer-signed JWS, so it is not anonymous. **Notary to its sources.** A runtime boundary sits between Registry Notary and the sources it reads. Notary calls Relay over HTTP as a data source; it does not link Relay code. The @@ -203,8 +207,10 @@ These are the risks the design does *not* close and states honestly: evaluation or request time. A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. These are residual misconfiguration risks left to the operator. -- **Known product gaps.** There is no revocation service, no `/.well-known/jwt-vc-issuer` - endpoint, and no built-in data-subject erasure workflow in this version. +- **Known product gaps.** No revocation flow is specified — though an optional, + off-by-default credential-status surface can be enabled to mark a credential `revoked` — + there is no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure + workflow in this version. - **Admin reload is non-functional standalone.** Notary's admin reload route returns HTTP 501 (`registry.admin.capability.not_supported`) in the standalone router and performs no reload; key and configuration changes require a service restart. Do not treat hot reload as diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx index 51efa935..9472fb5a 100644 --- a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx +++ b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx @@ -63,8 +63,8 @@ always on by default. **An answer is shaped, not a copy of the record.** Every claim is answered in one of three modes: `value` hands back the computed value, `predicate` returns only a yes/no, and `redacted` -returns neither the value nor the yes/no. The mode is fixed by the claim's policy, not chosen -by the caller. [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) +returns neither the value nor the yes/no. The claim's policy fixes which modes are allowed; +within that set the caller can request one, and otherwise the default applies. [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) covers this in depth. **Credentials are selective, and can be holder-bound.** When Notary issues a credential it's an From fd06aa9bcaad0d9d3f177e01d4a204c60e0ce4e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 07:49:34 +0000 Subject: [PATCH 07/25] docs: refine credential-status, delegated-attestation, and disclosure-granularity claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four code-verified refinements from continued review: - known limitations (revocation): only the admin status endpoint (requires the registry:notary:admin scope) marks a credential revoked; the public status endpoint reads status. Corrected the wording that implied the public endpoint could revoke. - known limitations (delegated attestation): reframed the absolute "no delegated-attestation issuance" — the OID4VCI transaction-token path is rejected, but direct issuance via /v1/credentials is supported when the stored evaluation is delegated-attestation and the relationship allow-lists the profile. - disclosure modes (redacted + CCCEV): note a current renderer limitation — the CCCEV JSON-LD renderer emits cccev:isConformantTo: false for a redacted result instead of omitting it, so the no-outcome guarantee is not fully upheld in that render. (Tracked as a product issue.) - data minimization (SD-JWT granularity): selective disclosure is at the claim (or configured-projection) level — each claim is one disclosure carrying its whole value, so an object-valued claim is revealed as a unit unless an explicit projection splits it. check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../explanation/data-minimization-and-purpose-limitation.mdx | 2 +- .../explanation/disclosure-modes-and-computed-answers.mdx | 2 ++ docs/site/src/content/docs/explanation/known-limitations.mdx | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 63224aeb..706d6fa3 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -38,7 +38,7 @@ Minimization also applies to what leaves the boundary. A claim's result can be s The redacted mode is worth a reviewer's attention because it shows minimization without loss of accountability: a redacted result carries neither the underlying value nor the satisfaction outcome, yet the evaluation stays referenceable and verifiable through an evaluation identifier, a verification identifier, and a claim hash. You can audit that an evaluation happened and verify it later without the result itself disclosing anything about the person. -When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC — a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Holder binding — tying the credential to a holder key with `did:jwk` — is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. +When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC — a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Selective disclosure here is at the claim (or configured-projection) level: by default each claim becomes one disclosure carrying its whole value, so an object-valued claim is revealed as a unit unless an explicit projection splits it into separately-disclosable fields. Holder binding — tying the credential to a holder key with `did:jwk` — is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. A further minimization detail guards against a subtler leak. A failed subject match collapses by default to a single public reason (evidence not available), with the granular reason kept only in the audit record, so the lookup surface cannot by default be used to confirm whether a person exists in a registry. diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index db60246c..947d2d25 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -49,6 +49,8 @@ This is the core of the privacy story. Because Notary computes the answer from i A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. +One current renderer limitation to note: this no-outcome guarantee holds for the standard result body, but the CCCEV JSON-LD renderer currently emits `cccev:isConformantTo: false` for a redacted result instead of omitting it. A deployment that allows both redacted disclosure and CCCEV output should be aware that the outcome is not fully withheld in that render. + The difference between the two matters. `predicate` is not "zero disclosure" — it still tells the caller a true-or-false fact about the subject. `redacted` goes one step further: it does not leak even a yes/no. They are not interchangeable. ## How disclosure policy is configured per claim diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 0822e49c..5505de4f 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -57,8 +57,8 @@ Registry Notary issues credentials, but the issuance surface is deliberately nar - **One credential format, one binding method when binding is enabled.** Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. - **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. The capability-discovery document (`/.well-known/evidence-service`) declares `openid4vci.support: not_full_issuer` — announcing it is not a full issuer; this flag lives there rather than in the OID4VCI credential-issuer metadata. -- **No delegated-attestation issuance.** Delegated-attestation transaction tokens are rejected by the credential endpoint; this profile issues only for direct self-attestation principals in this version. -- **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, a public status endpoint and an admin status endpoint can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- **Delegated-attestation issuance is limited to the direct path.** The OID4VCI transaction-token path for delegated attestation is rejected at the credential endpoint. Direct credential issuance via `/v1/credentials` is supported when the stored evaluation's access mode is delegated-attestation and the relationship allow-lists the credential profile. +- **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, the public status endpoint reads a credential's status, and only the admin status endpoint — which requires the `registry:notary:admin` scope — can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. - **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. - **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. - **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. From 5e51ab03102601cf7d7adacdcb892ee9eabd995e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 08:12:59 +0000 Subject: [PATCH 08/25] docs: qualify absolute limitation claims that are conditional in the implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six code-verified refinements where an absolute "limit" is actually conditional: - data minimization: claim-level purpose constraints (the claim's permitted purposes and the deployment allowed-purposes list) are enforced before a source read even with no binding matching policy; only the binding-level gating is missing in that case. - known limitations (federation): replay protection defaults to in-memory, but a Redis-backed shared replay store is available for multi-instance deployments — "no shared replay storage" was too absolute. - known limitations (Relay): signed response credentials are config-gated via provenance, not Cargo-feature-gated; and with provenance enabled Relay can attach its own VC-JWT signed response credential (it still does not issue Notary's SD-JWT VC credentials). - known limitations (reload): standalone /admin/v1/reload returns 501 and non-swappable changes need a restart, but governed config apply and the file-watch signer hot-apply some changes (signing-key rotation, auth-policy swaps) without one. - threat model + known limitations (duplicate ids): duplicate claim ids are not rejected at load and are never reported — the runtime lookup returns the first match, so a later duplicate is silently shadowed (corrected the earlier "surface at runtime" wording on both pages). check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../data-minimization-and-purpose-limitation.mdx | 2 +- .../content/docs/explanation/known-limitations.mdx | 10 +++++----- .../src/content/docs/explanation/threat-model.mdx | 14 ++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 706d6fa3..f9b6daa1 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -71,7 +71,7 @@ Some of the most important things for a DPO to know are the absences. Stating th The minimization and purpose-limitation posture described above is the design default, but the operator owns the configuration. Several of the protections are conditional, and a reviewer should test the deployment, not the design, for each one. -The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding falls back to unrestricted, identifier-only resolution with no purpose gating, no relationship gating, and no input minimization. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. +The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding skips the binding-level gating — no purpose gating, no relationship gating, and no input minimization — and falls back to identifier-only resolution. Claim-level purpose constraints still apply, though: the claim's own permitted purposes and the deployment's allowed-purposes list are enforced before the source is read, so a claim-purpose mismatch is refused with no source reads. Purpose limitation is therefore not entirely absent without a matching policy; what is missing is the binding-level gating. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a binding-level hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. The existence-oracle protection is also defeasible. The matching surface collapses failures to a single public reason by default, but a deployment may disable that collapse and surface not-found, ambiguous, or rejected outcomes — an over-disclosure risk the operator controls. diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 5505de4f..e4645eb0 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -61,13 +61,13 @@ Registry Notary issues credentials, but the issuance surface is deliberately nar - **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, the public status endpoint reads a credential's status, and only the admin status endpoint — which requires the `registry:notary:admin` scope — can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. - **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. - **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. -- **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. +- **Standalone admin reload is not implemented.** The standalone Notary `/admin/v1/reload` route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload, and non-swappable changes require a service restart. Governed config apply and the file-watch signer do hot-apply some changes without one — signing-key rotation reports `restart_required: false`, client and OpenAPI auth-policy changes are swapped in, and a same-public-key signer replacement reloads — so not every key or configuration change needs a restart. ## Federation limits Federation across institutions is static-peer only. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/) and the [security model](../../spec/rs-sec-g/). -- **Static peers only.** There is no dynamic trust-chain discovery, no shared replay storage, and no federated credential issuance. All three are out of scope for this version. +- **Static peers only.** Replay protection defaults to in-process, in-memory state, but a Redis-backed shared replay store is available for multi-instance deployments (`replay.storage = redis`, with `federation.replay.storage = redis` when the top-level store is Redis), and federation uses it for request replay and `jti` checks. There is still no dynamic trust-chain discovery and no federated credential issuance; both are out of scope for this version. - **Delegated evaluation returns a result, never a credential.** Across a federation boundary, what crosses is a scoped evaluation result — never an issued credential. ## Registry Relay limits @@ -75,9 +75,9 @@ Federation across institutions is static-peer only. Read the full context in the Registry Relay exposes protected, read-only registry routes. Its boundaries are covered in depth in the [Registry Relay protocol](../../spec/rs-pr-relay/). - **Read-only, batch or scan.** Relay does not mutate source data in v1 and has no event-stream backend; it reads sources as batch snapshots or table scans only. -- **Feature-gated surfaces.** Several Relay surfaces — the OGC API Features, Records, and EDR adapters, the SP DCI sync adapter, and signed response credentials — are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. +- **Feature-gated surfaces.** Several Relay surfaces — the OGC API Features, Records, and EDR adapters and the SP DCI sync adapter — are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. (Signed response credentials are not Cargo-gated; they are config-gated through `provenance`.) - **Aggregates are not privacy-budgeted.** As noted stack-wide, Relay aggregate routes provide no longitudinal privacy-budget or cumulative-differencing protection. -- **Relay does not evaluate or own the manifest.** Relay does not perform claim evaluation or issue credentials — that is Notary's role — and it does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. +- **Relay does not evaluate or own the manifest.** Relay does not evaluate claims or issue Notary's SD-JWT VC credentials — that is Notary's role — though with `provenance` enabled and a caller requesting `Accept: application/vc+jwt` it can attach its own VC-JWT signed response credential. It also does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. - **Governed enforcement is one PDP profile, not full interoperability.** Governed runtime policy enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1` — the single PDP profile this build recognizes). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of [ODRL](../../reference/standards/) terms outside the supported set. ## Data-model limits (Claim and Manifest) @@ -85,7 +85,7 @@ Registry Relay exposes protected, read-only registry routes. Its boundaries are These limits concern what the claim and manifest configuration loaders do and do not enforce. Read them in full in the [Claim data model](../../spec/rs-dm-claim/) and [Manifest data model](../../spec/rs-dm-manifest/). - **The `plugin` rule type is unimplemented.** The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. -- **Load-time invariants are not all enforced.** The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. These surface as request or evaluation errors at runtime, not as startup failures. +- **Load-time invariants are not all enforced.** The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. A disclosure default outside the allowed set and a dangling rule source surface as request or evaluation errors at runtime, not as startup failures. Duplicate claim ids are different: they are never reported, because the runtime lookup returns the first match by id, so a later duplicate is silently shadowed. - **Identifier-only matching has no further gating.** A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. - **Manifests describe; they do not enforce.** The portable metadata layer only describes — publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. - **Rendered standards artifacts are well-formed, not certified.** Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index ad1f1d76..1d495ca8 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -201,12 +201,14 @@ These are the risks the design does *not* close and states honestly: overlapping aggregate queries. Do not describe a Relay aggregate as privacy-budgeted unless a separate deployed control provides it. This is a residual disclosure risk left to the operator. -- **Some claim-definition invariants surface only at runtime.** Duplicate claim ids, a - disclosure default outside the allowed set, a rule source naming an undeclared binding, and - the unimplemented plugin rule are not enforced at configuration load — they surface only at - evaluation or request time. A source binding with no matching policy resolves on identifiers - alone, with no purpose, relationship, or input-minimization gating. These are residual - misconfiguration risks left to the operator. +- **Some claim-definition invariants surface only at runtime.** A disclosure default outside + the allowed set, a rule source naming an undeclared binding, and the unimplemented plugin + rule are not enforced at configuration load — they surface only at evaluation or request + time. Duplicate claim ids are different and worse: they are not rejected at load and are + never reported, because the runtime lookup returns the first match by id, so a later + duplicate is silently shadowed. A source binding with no matching policy resolves on + identifiers alone, with no purpose, relationship, or input-minimization gating. These are + residual misconfiguration risks left to the operator. - **Known product gaps.** No revocation flow is specified — though an optional, off-by-default credential-status surface can be enabled to mark a credential `revoked` — there is no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure From 1ecd22f2a6741a7ab0e4875cb7a2d97136a7d31d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 08:31:27 +0000 Subject: [PATCH 09/25] docs: snapshot-cache + exists-not_available caveats; align threat model with limitations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new code-verified corrections plus three threat-model alignments: - records-stay-home (read-in-place): in snapshot mode (the default) Relay materializes a projected copy into its local cache (cache_dir). There is still no write-back to the source and no external handoff, but a cached copy exists, with its own retention/access considerations — distinguished from "no copy". - disclosure modes (exists/predicate): an `exists` claim returns `true` when a record matches, but a subject with no matching record returns `evidence.not_available` (collapsed to a single public reason by default), not a bare `false` — absence is no-evidence, not a negative result. - threat model: align with the limitations page on three points the threat model still stated absolutely — shared replay storage is supported (Redis), Relay signed response credentials are config-gated via provenance (not Cargo-gated), and some config/key changes hot-apply without a restart. check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../disclosure-modes-and-computed-answers.mdx | 2 +- .../docs/explanation/records-stay-home.mdx | 5 ++++- .../content/docs/explanation/threat-model.mdx | 20 ++++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index 947d2d25..fa788014 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -45,7 +45,7 @@ Every evaluation result records which disclosure mode was applied, so a downstre ## Why predicate and redacted avoid sharing the underlying record -This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` or `false`, and the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. +This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` when a matching record is found, while a subject with no matching record returns `evidence.not_available` (collapsed to a single public reason by default) rather than `false` — absence is surfaced as no-evidence, not as a negative result. Either way the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index fb052b88..b6db6e6f 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -70,7 +70,10 @@ disclosure policy, and issues credentials. - **Source data is read in place.** Relay reads sources as batch snapshots or table scans; there is no write-back to the source registry, and runtime services expose no - data-mutation routes. The source keeps running as it always has. + data-mutation routes. The source keeps running as it always has. "No write-back to the + source and no external handoff" is not the same as "no copy exists": in snapshot mode — + the default — Relay materializes a projected copy into its local cache (`cache_dir`), which + carries its own retention and access considerations. - **Storage internals stay private.** The paths, table names, and backend credentials that point at the source live in the service's runtime configuration, decided at startup. They are never part of the public API surface, and never part of a portable metadata file that diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index 1d495ca8..3947d7f9 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -215,17 +215,18 @@ These are the risks the design does *not* close and states honestly: workflow in this version. - **Admin reload is non-functional standalone.** Notary's admin reload route returns HTTP 501 (`registry.admin.capability.not_supported`) in the standalone router and performs no - reload; key and configuration changes require a service restart. Do not treat hot reload as - a capability. + reload, and non-swappable changes require a service restart. Some changes do hot-apply + without a restart, though — signing-key rotation and auth-policy swaps among them. ## What is explicitly out of scope These are non-goals for this version. None of them should be read into a Registry Stack conformance claim: -- **Dynamic federation.** Dynamic trust-chain discovery, shared replay storage, audit - checkpoint exchange, and federated credential issuance are out of scope. Federation is - static-peer only — not a dynamic trust mesh. +- **Dynamic federation.** Dynamic trust-chain discovery, audit checkpoint exchange, and + federated credential issuance are out of scope. Federation is static-peer only — not a + dynamic trust mesh. Replay protection defaults to in-memory, with a Redis-backed shared + store available. - **Full Evidence Gateway / external-policy interoperability.** Governed Relay PDP enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not @@ -239,10 +240,11 @@ conformance claim: speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, W3C DID, or the rest does not certify conformance to any of them, and CCCEV-shaped output is not conformant to CCCEV 2.00. -- **Feature-gated surfaces of other builds.** Several runtime surfaces — OGC API - Features/Records/EDR adapters, SP DCI sync, and signed response credentials — are - feature-gated and mount only when the build is configured with the matching Cargo feature. - Do not infer their presence from the route catalog of a different build. Admin routes run on +- **Feature-gated surfaces of other builds.** Several runtime surfaces — the OGC API + Features/Records/EDR adapters and SP DCI sync — are feature-gated and mount only when the + build is configured with the matching Cargo feature. Do not infer their presence from the + route catalog of a different build. Relay signed response credentials are not Cargo-gated; + they are config-gated through `provenance`. Admin routes run on a separate optional listener and are documented in the generated OpenAPI, but standalone mode does not implement runtime config reload — it returns HTTP 501. From 6b61b796b0a190d93dd9790992fed0d063cf5237 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 11:23:03 +0700 Subject: [PATCH 10/25] docs: honest standalone-Notary framing, platform fallback, DHIS2 tutorial lead The standalone-Notary tutorial now states its dependency on the local Relay project (or an operator-owned Registry Data API source) instead of implying independence. Restores the unsupported-platform install fallback as a troubleshooting row. Reshapes the DHIS2 opener to the tutorial pattern and promotes deployment adaptation into a Make-it-yours section. Signed-off-by: Jeremi Joslin --- .../configure-dhis2-claim-checks.mdx | 32 ++++++++++++------- ...blish-spreadsheet-secured-registry-api.mdx | 1 + .../run-notary-standalone-for-api.mdx | 23 +++++++------ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx b/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx index 7b6e7dfe..b09271fc 100644 --- a/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx +++ b/docs/site/src/content/docs/tutorials/configure-dhis2-claim-checks.mdx @@ -16,10 +16,11 @@ standards_referenced: import QuickstartMeta from '../../../components/QuickstartMeta.astro'; -Use this tutorial when DHIS2 is already your source system and you want Registry Notary to answer -narrow programme-status claims without sharing full DHIS2 records. -The tutorial uses the public DHIS2 play instance as the source, runs a private source-adapter -sidecar, and exposes only the Registry Notary API to the caller. +This tutorial runs Registry Notary against the public DHIS2 play sandbox. +A private source-adapter sidecar calls the DHIS2 Tracker API, Registry Notary evaluates +programme-status claims from the projected fields, and Notary issues a holder-bound SD-JWT VC +(Selective Disclosure JWT Verifiable Credential) from the evaluated results, with only the Notary +API exposed to the caller. You will configure three pieces: @@ -45,7 +46,8 @@ You need: - Docker with Compose v2.20 or later. - The `just` command runner. -- `jq` and `node` for manual checks and holder-proof generation. +- `jq` and `node` for manual checks and holder-proof generation (a holder proof is a signed + attestation binding an issued credential to the holder's key). - A Registry Lab checkout, which is the `lab` directory of the `registry-stack` monorepo. - Network access to the DHIS2 source you are testing. @@ -268,7 +270,8 @@ jq '{credential_id, credential_profile, issuer, format, disclosure_count: (.disc output/dhis2-openfn/manual-programme-credential.json ``` -The issued credential is holder-bound to the generated `did:jwk` proof and uses the +The issued credential is holder-bound to the generated `did:jwk` proof (a DID that encodes a +public key directly, with no separate registry lookup) and uses the `dhis2_programme_participation_sd_jwt` profile. ## How the adapter is configured @@ -288,8 +291,6 @@ Key settings: The demo maps one DHIS2 tracked entity into fields such as `first_name`, `last_name`, `child_program_active`, `child_age_band`, `programme_code`, and `reconciliation_ref`. -For your deployment, change the DHIS2 base URL allow-list, program IDs, attribute IDs, source -projection, and smoke lookup subject to match your DHIS2 configuration. ## How Notary is configured @@ -306,9 +307,6 @@ Key settings: | `evidence.claims[].rule` | Extracts a value or evaluates a predicate from the projected fields. | | `evidence.credential_profiles` | Controls which claims can be issued as SD-JWT VCs, validity, issuer, disclosure, and holder binding. | -For your deployment, update the claims to match the programme facts your relying services need. -Keep the sidecar private; expose Notary, not DHIS2, to callers. - ## Stop the profile Stop the DHIS2 services: @@ -317,6 +315,18 @@ Stop the DHIS2 services: docker compose -f compose.yaml --profile dhis2 down ``` +## Make it yours + +- Update `sources.dhis2_health.allowed_base_urls` in `config/source-adapter/dhis2-health-sidecar.yaml` + to your DHIS2 base URL when you point the sidecar at a private deployment. +- Change the program IDs and attribute IDs in that same file's `http_json.query` and + `http_json.response.records` blocks to match your DHIS2 Tracker configuration. +- Update `evidence.claims[].source_bindings` and `evidence.claims[].rule` in + `config/notary/dhis2-health-notary.yaml` to match the programme facts your relying services need. +- Replace `OPENFN_DHIS2_HOST_URL`, `OPENFN_DHIS2_USERNAME`, and `OPENFN_DHIS2_PASSWORD` with a + reader account scoped to your DHIS2 instance instead of the public sandbox credentials. +- Keep the DHIS2 sidecar private and expose only Registry Notary to callers, as this demo does. + ## Troubleshooting | Symptom | Cause | Fix | diff --git a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx index 445e18b6..6f2b0cd8 100644 --- a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx +++ b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx @@ -241,6 +241,7 @@ stayed where it started, and only an authorized, scoped request got an answer ou | Symptom | Cause | Resolution | | --- | --- | --- | | `registryctl` is not found | The install directory is not on `PATH`. | Add the directory printed by the installer, usually `~/.local/bin`, to `PATH`. | +| The installer reports an unsupported platform | No binary is published for that OS or CPU. | Install Rust with [`rustup`](https://rustup.rs), then run `cargo install --git https://github.com/registrystack/registry-stack --tag v0.8.3 registryctl --locked`. | | `registryctl start` cannot find Docker | Docker or another Compose provider is not installed or running. | Start Docker Desktop, OrbStack, Colima, Podman, or your supported provider, then run `registryctl start` again. | | `registryctl start` fails and the container log shows `failed to parse config YAML ... unknown field` | The locally cached container image does not match the digest-pinned image in the generated `compose.yaml`. | Run `docker compose pull` in the project directory, then `registryctl start` again. | | A row read returns `403 Forbidden` | The key is valid but lacks the row-read scope. | Use `ROW_READER_RAW` for row reads. | diff --git a/docs/site/src/content/docs/tutorials/run-notary-standalone-for-api.mdx b/docs/site/src/content/docs/tutorials/run-notary-standalone-for-api.mdx index 7107a289..56db8d21 100644 --- a/docs/site/src/content/docs/tutorials/run-notary-standalone-for-api.mdx +++ b/docs/site/src/content/docs/tutorials/run-notary-standalone-for-api.mdx @@ -14,23 +14,28 @@ standards_referenced: [] import QuickstartMeta from '../../../components/QuickstartMeta.astro'; -Use this tutorial when you already operate a source that exposes the Registry Data API lookup -contract and want Registry Notary to evaluate claims against it from a separate project. +Registry Notary runs as its own project against any Registry Data API-shaped source: a source +exposing the Registry Data API contract, the protected read API shape Registry Relay serves. +This tutorial generates a standalone Notary project, points it at that source, and evaluates a +starter claim against it. -The reproducible path below uses the local Relay project from +The reproducible path in this tutorial uses the local Relay project from [Run a protected registry API locally](../publish-spreadsheet-secured-registry-api/) as a stand-in source. -If your source is not Registry Data API-shaped, choose a source-adapter path instead: +If your source is not Registry Data API-shaped, put a source-adapter sidecar in front of it: a +small service that runs next to Notary and translates the source's own routes, auth, and response +shape into the Registry Data API-shaped lookup route Notary expects. +Choose the sidecar engine that fits your source: | Source shape | Use | | --- | --- | -| Single HTTP JSON lookup | Built-in source-adapter `http_json` engine | -| Short dependent GET sequence | Built-in source-adapter `http_flow` engine | +| Single HTTP JSON lookup | Built-in source-adapter `http_json` engine (one governed HTTP JSON request) | +| Short dependent GET sequence | Built-in source-adapter `http_flow` engine (2 to 5 dependent GET requests) | | FHIR R4 source | [Getting started with FHIR evidence](../getting-started-fhir-evidence/) | | OpenCRVS DCI source | [OpenCRVS claims](../verify-opencrvs-claims/) | | DHIS2 Tracker source | [Configure DHIS2 claim checks](../configure-dhis2-claim-checks/) | -| POSTs, fallback behavior, or small custom branching | Source-adapter `script_rhai` engine | +| POSTs, fallback behavior, or small custom branching | Source-adapter `script_rhai` engine (custom scripted branching) | The source-adapter paths still present the same Registry Data API-shaped lookup route to Notary. The adapter owns the source-specific request shape and normalization. @@ -39,7 +44,7 @@ The adapter owns the source-specific request shape and normalization. outcome="A standalone Notary project evaluating a starter claim against a Registry Data API source." time="About 10 minutes after a Registry Data API source is available" level="Local single-node" - prerequisites={['registryctl', 'A running Registry Data API source', 'A Docker Compose provider']}/> + prerequisites={['registryctl', 'The running local Relay project from Run a protected registry API locally, or your own Registry Data API-shaped source you already operate', 'A Docker Compose provider']}/> This tutorial uses synthetic data and local demo credentials. Do not use the generated local keys in production. @@ -184,7 +189,7 @@ Start with the smallest engine that fits: | --- | --- | | `http_json` | One governed HTTP JSON request can return the fields Notary needs. | | `http_flow` | You need 2 to 5 dependent GET requests, such as search first and fetch detail second. | -| `fhir` | You are projecting bounded FHIR R4 resources into Notary-ready facts. | +| `fhir` (FHIR R4 resource projection engine) | You are projecting bounded FHIR R4 resources into Notary-ready facts. | | `script_rhai` | You need POST, fallback behavior, or small custom branching that the built-in declarative engines cannot express. | Keep the source-specific logic in the adapter. From 79519b65e724b71e7bf4fe7fb8f68002b80d917c Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 11:23:03 +0700 Subject: [PATCH 11/25] docs: apply style guide mechanics to trust and security pages Removes em dashes and bold-led list items, merges duplicate lead paragraphs, and fixes reference words across the six trust and security explanation pages. No factual claim, qualifier, or hedge changed; Tier-C sign-off on the content itself is still pending. Signed-off-by: Jeremi Joslin --- ...ta-minimization-and-purpose-limitation.mdx | 58 ++++---- .../disclosure-modes-and-computed-answers.mdx | 46 +++--- .../docs/explanation/known-limitations.mdx | 86 +++++------ .../docs/explanation/records-stay-home.mdx | 86 +++++------ .../content/docs/explanation/threat-model.mdx | 135 +++++++++--------- .../trust-posture-and-security-guarantees.mdx | 62 ++++---- 6 files changed, 241 insertions(+), 232 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index f9b6daa1..fff614a9 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -12,17 +12,23 @@ standards_referenced: - sd-jwt-vc --- -As a data protection officer or privacy reviewer, you are asked whether a system supports the principles you work to — data minimization, purpose limitation, accountability — and where those supports stop. This page answers one question about Registry Stack: how does its architecture support data minimization and purpose limitation by design, where are those supports conditional on how an operator configures a deployment, and which data-protection obligations does the architecture not address at all? - -Two things this page does not do. It makes no compliance claim: conformance to the underlying specifications does not imply conformance to any external standard, nor to GDPR or any data-protection regulation. And it gives no legal advice and no jurisdiction-specific reading — that judgement remains yours. The specifications here are all draft, pre-1.0 documents and may change, so treat the design as a posture under development rather than a finished or warranted product. - -A note on terms, since you do not need Registry Stack internals to follow this. A *claim* is one pre-modelled question — one decision or one extracted value — that a system can ask about a person against a registry the institution already holds. A *source binding* is the configured rule that connects a claim to the specific source fields it reads. *Minimized evidence* means a response shaped by data minimization or selective disclosure; a *purpose-bound request* is a request that carries or is evaluated against a stated purpose. Those last two are the principles you already know, expressed in the system's own vocabulary. +As a data protection officer or privacy reviewer, you are asked whether a system supports the principles you work to (data minimization, purpose limitation, accountability) and where those supports stop. +This page answers one question about Registry Stack: how does its architecture support data minimization and purpose limitation by design, where are those supports conditional on how an operator configures a deployment, and which data-protection obligations does the architecture not address at all? +Two things this page does not do. +It makes no compliance claim: conformance to the underlying specifications does not imply conformance to any external standard, nor to GDPR or any data-protection regulation. +And it gives no legal advice and no jurisdiction-specific reading: that judgement remains yours. +The specifications here are all draft, pre-1.0 documents and may change, so treat the design as a posture under development rather than a finished or warranted product. +A note on terms, since you do not need Registry Stack internals to follow this. +A *claim* is one pre-modelled question (one decision or one extracted value) that a system can ask about a person against a registry the institution already holds. +A *source binding* is the configured rule that connects a claim to the specific source fields it reads. +*Minimized evidence* means a response shaped by data minimization or selective disclosure; a *purpose-bound request* is a request that carries or is evaluated against a stated purpose. +Those last two are the principles you already know, expressed in the system's own vocabulary. ## How the claim model limits collection at the source -The first place minimization appears is in the unit of data the system is built around. A claim definition describes one decision or one extracted value — not a whole record — so returning a full record would over-collect relative to the question actually asked. The design treats the narrow question, not the record, as the thing to be answered. +The first place minimization appears is in the unit of data the system is built around. A claim definition describes one decision or one extracted value, not a whole record, so returning a full record would over-collect relative to the question actually asked. The design treats the narrow question, not the record, as the thing to be answered. -That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads the fields it is configured to project — the declared `binding.fields`, plus the lookup and freshness fields — so "read only what the rule needs" depends on the binding being configured that way rather than being automatically derived from the rule. A request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. +That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads the fields it is configured to project (the declared `binding.fields`, plus the lookup and freshness fields), so "read only what the rule needs" depends on the binding being configured that way rather than being automatically derived from the rule. A request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. ## Purpose limitation as an enforced gate, not a label @@ -38,50 +44,50 @@ Minimization also applies to what leaves the boundary. A claim's result can be s The redacted mode is worth a reviewer's attention because it shows minimization without loss of accountability: a redacted result carries neither the underlying value nor the satisfaction outcome, yet the evaluation stays referenceable and verifiable through an evaluation identifier, a verification identifier, and a claim hash. You can audit that an evaluation happened and verify it later without the result itself disclosing anything about the person. -When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC — a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Selective disclosure here is at the claim (or configured-projection) level: by default each claim becomes one disclosure carrying its whole value, so an object-valued claim is revealed as a unit unless an explicit projection splits it into separately-disclosable fields. Holder binding — tying the credential to a holder key with `did:jwk` — is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. +When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC: a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Selective disclosure here is at the claim (or configured-projection) level: by default each claim becomes one disclosure carrying its whole value, so an object-valued claim is revealed as a unit unless an explicit projection splits it into separately-disclosable fields. Holder binding (tying the credential to a holder key with `did:jwk`) is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. A further minimization detail guards against a subtler leak. A failed subject match collapses by default to a single public reason (evidence not available), with the granular reason kept only in the audit record, so the lookup surface cannot by default be used to confirm whether a person exists in a registry. ## Data stays at source: distributed custody -Underneath all of this is an architectural choice that matters for minimization at the system level: data stays at source. The design premise is distributed custody — the stack provides an API surface for lawful exchange between authorities and does not aggregate data into a central system. The read component must not mutate source registry data and exposes no write-back route, and runtime services are read-only in this version, with no source-registry data-mutation routes at all. +Underneath all of this is an architectural choice that matters for minimization at the system level: data stays at source. The design premise is distributed custody: the stack provides an API surface for lawful exchange between authorities and does not aggregate data into a central system. The read component must not mutate source registry data and exposes no write-back route, and runtime services are read-only in this version, with no source-registry data-mutation routes at all. For a reviewer this has a direct consequence. Because the architecture cannot alter or delete source records, it is also why no erasure flow exists at the source layer: the design has no mechanism to reach into and change the registry it reads from. That is described more fully in the next section as a limit, not a feature. -One related boundary is positive rather than cautionary. The portable metadata the stack publishes must not carry person-level data or runtime secrets and bindings, which is precisely why those artifacts are safe to share — they hold no personal data. But publishing metadata only describes; it does not authorize access, enforce a policy, or assert that any record exists, so a published manifest carries no data-protection guarantee about live data. +One related boundary is positive rather than cautionary. The portable metadata the stack publishes must not carry person-level data or runtime secrets and bindings, which is precisely why those artifacts are safe to share: they hold no personal data. But publishing metadata only describes; it does not authorize access, enforce a policy, or assert that any record exists, so a published manifest carries no data-protection guarantee about live data. ## Audit as an accountability primitive -Accountability is supported through audit, which is treated as a security control rather than an optional log. Every request that returns person-level records or claim results must be recorded with at least the caller principal, a request identifier, and the purpose value where one was supplied, and a deployment can run audit fail-closed so that a request whose audit record cannot be written does not succeed. The security model expects the scopes exercised to be recorded too; in practice Relay's audit record captures them, but Notary's audit record does not yet include that field — a gap noted in the limitations inventory. +Accountability is supported through audit, which is treated as a security control rather than an optional log. Every request that returns person-level records or claim results must be recorded with at least the caller principal, a request identifier, and the purpose value where one was supplied, and a deployment can run audit fail-closed so that a request whose audit record cannot be written does not succeed. The security model expects the scopes exercised to be recorded too; in practice Relay's audit record captures them, but Notary's audit record does not yet include that field, a gap noted in the limitations inventory. -One precise reading matters here. Audit fail-closed is a capability a deployment can turn on — not a guarantee that every route in any given build has been individually audited against it. Whether a particular deployment meets it is something a reviewer verifies in that deployment, not something the design asserts on its behalf. +One precise reading matters here. Audit fail-closed is a capability a deployment can turn on, not a guarantee that every route in any given build has been individually audited against it. Whether a particular deployment meets it is something a reviewer verifies in that deployment, not something the design asserts on its behalf. ## What the architecture does not provide Some of the most important things for a DPO to know are the absences. Stating them plainly prevents the design from being read as more than it is. The full, canonical inventory is in [Known limitations and non-guarantees](../known-limitations/). -- **No data-subject erasure or right-to-be-forgotten workflow.** There is no built-in erasure or deletion flow anywhere in the design, so it does not satisfy erasure obligations on its own. As noted above, the read-only design cannot mutate source records, so erasure, where it is required, remains an operation on the source registry outside this system. -- **No rectification or data-subject-rights flow.** Beyond erasure, there is no rectification or general data-subject-rights mechanism. -- **No specified revocation flow; an optional status surface.** The specifications define no revocation flow, but the implementation includes an optional, off-by-default credential-status surface — a public `GET /v1/credentials/{id}/status` and an admin `POST /admin/v1/credentials/{id}/status` — that an operator can enable to mark a credential `revoked`. Key rotation exists — a rotated-out key may remain published so existing results stay verifiable — but that is not a way to revoke an already-issued credential. Treat status-based revocation as an operator-enabled capability, not an always-on one. -- **No broad cross-authority exchange beyond static peering.** Federation between authorities is static-peer only; dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of scope, so the design supports a narrower cross-authority data-exchange surface than the word "federation" might suggest. -- **No privacy-budgeted analytics.** Aggregate routes produce statistical outputs, not a longitudinal privacy budget. A data-protection impact assessment should not describe an aggregate route as privacy-budgeted unless a separate, deployed control actually provides that. -- **No compliance claim.** Conformance to these specifications does not imply conformance to any external standard or any data-protection regulation, and the specifications themselves are draft. +- No data-subject erasure or right-to-be-forgotten workflow: There is no built-in erasure or deletion flow anywhere in the design, so it does not satisfy erasure obligations on its own. As the Data stays at source: distributed custody section notes, the read-only design cannot mutate source records, so erasure, where it is required, remains an operation on the source registry outside this system. +- No rectification or data-subject-rights flow: Beyond erasure, there is no rectification or general data-subject-rights mechanism. +- No specified revocation flow; an optional status surface: The specifications define no revocation flow, but the implementation includes an optional, off-by-default credential-status surface (a public `GET /v1/credentials/{id}/status` and an admin `POST /admin/v1/credentials/{id}/status`) that an operator can enable to mark a credential `revoked`. Key rotation exists (a rotated-out key may remain published so existing results stay verifiable), but that is not a way to revoke an already-issued credential. Treat status-based revocation as an operator-enabled capability, not an always-on one. +- No broad cross-authority exchange beyond static peering: Federation between authorities is static-peer only; dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of scope, so the design supports a narrower cross-authority data-exchange surface than the word "federation" might suggest. +- No privacy-budgeted analytics: Aggregate routes produce statistical outputs, not a longitudinal privacy budget. A data-protection impact assessment should not describe an aggregate route as privacy-budgeted unless a separate, deployed control actually provides that. +- No compliance claim: Conformance to these specifications does not imply conformance to any external standard or any data-protection regulation, and the specifications themselves are draft. ## Operator responsibilities: what the design leaves to you The minimization and purpose-limitation posture described above is the design default, but the operator owns the configuration. Several of the protections are conditional, and a reviewer should test the deployment, not the design, for each one. -The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding skips the binding-level gating — no purpose gating, no relationship gating, and no input minimization — and falls back to identifier-only resolution. Claim-level purpose constraints still apply, though: the claim's own permitted purposes and the deployment's allowed-purposes list are enforced before the source is read, so a claim-purpose mismatch is refused with no source reads. Purpose limitation is therefore not entirely absent without a matching policy; what is missing is the binding-level gating. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a binding-level hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. +The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding skips the binding-level gating (no purpose gating, no relationship gating, and no input minimization) and falls back to identifier-only resolution. Claim-level purpose constraints still apply, though: the claim's own permitted purposes and the deployment's allowed-purposes list are enforced before the source is read, so a claim-purpose mismatch is refused with no source reads. Purpose limitation is therefore not entirely absent without a matching policy; what is missing is the binding-level gating. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a binding-level hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. -The existence-oracle protection is also defeasible. The matching surface collapses failures to a single public reason by default, but a deployment may disable that collapse and surface not-found, ambiguous, or rejected outcomes — an over-disclosure risk the operator controls. +The existence-oracle protection is also defeasible. The matching surface collapses failures to a single public reason by default, but a deployment may disable that collapse and surface not-found, ambiguous, or rejected outcomes, an over-disclosure risk the operator controls. -More broadly, the architecture defines primitives and leaves a large set of data-protection-relevant controls to the operator. Secret and key provisioning, audit retention and storage, tenant isolation, transport security, edge rate limiting, deployment configuration, and incident response are not defined by the design; they are responsibilities you provision and verify in a deployment. The clear-eyed view, then, is this: Registry Stack offers minimization, purpose limitation, and accountability as enforceable design primitives, but whether a given deployment realizes them — and whether it meets any legal obligation — depends on how the operator configures and runs it. +More broadly, the architecture defines primitives and leaves a large set of data-protection-relevant controls to the operator. Secret and key provisioning, audit retention and storage, tenant isolation, transport security, edge rate limiting, deployment configuration, and incident response are not defined by the design; they are responsibilities you provision and verify in a deployment. The clear-eyed view, then, is this: Registry Stack offers minimization, purpose limitation, and accountability as enforceable design primitives, but whether a given deployment realizes them (and whether it meets any legal obligation) depends on how the operator configures and runs it. ## Related -- [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) — how disclosure modes are selected and policy-bound -- [Threat model](../threat-model/) — the boundaries, assets, and threats behind this posture -- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/) — the high-level security summary -- [Known limitations and non-guarantees](../known-limitations/) — the full inventory of edges -- [Records stay home](../records-stay-home/) — what stays inside the institution's boundary and what crosses it +- [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/): how disclosure modes are selected and policy-bound +- [Threat model](../threat-model/): the boundaries, assets, and threats behind this posture +- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/): the high-level security summary +- [Known limitations and non-guarantees](../known-limitations/): the full inventory of edges +- [Records stay home](../records-stay-home/): what stays inside the institution's boundary and what crosses it - The security and protocol specifications: [RS-SEC-G](../../spec/rs-sec-g/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index fa788014..b7b9d6fd 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -1,6 +1,6 @@ --- title: Disclosure modes and computed answers -description: Why Registry Notary can answer a question about a subject by returning a computed result — value, predicate, or redacted — instead of handing back the source record. +description: Why Registry Notary can answer a question about a subject by returning a computed result (value, predicate, or redacted) instead of handing back the source record. status: draft owner: registry-docs source_repos: @@ -11,19 +11,21 @@ locale: en standards_referenced: [] --- -You are weighing whether Registry Stack actually keeps a sensitive registry record private while still answering questions about the subjects in it. The short answer is that a caller asks a question and receives a *computed answer*, not the record. This page explains how that works, what each kind of answer does and does not reveal, and where the privacy claim has edges. - -You will see one product term used throughout: a **claim**, which is a single pre-modelled question — one decision or one extracted value, such as "is this person registered?" or "what is this person's registration date?". A claim is deliberately narrow: a claim that tried to return a whole record would over-collect and be hard to authorize, which is exactly the outcome this design avoids. +You are weighing whether Registry Stack actually keeps a sensitive registry record private while still answering questions about the subjects in it. +The short answer is that a caller asks a question and receives a *computed answer*, not the record. +This page explains how that works, what each kind of answer does and does not reveal, and where the privacy claim has edges. +You will see one product term used throughout: a **claim**, which is a single pre-modelled question (one decision or one extracted value), such as "is this person registered?" or "what is this person's registration date?". +A claim is deliberately narrow: a claim that tried to return a whole record would over-collect and be hard to authorize, which is exactly the outcome this design avoids. ## How Registry Notary controls what leaves the service -Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. There are exactly three — there is no fourth mode. `value` is not always all-or-nothing, though: when a claim returns an object, `value` mode can have specific fields redacted (configured through `redaction_fields`), and the result carries the object minus those fields plus a list of what was withheld. +Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. There are exactly three; there is no fourth mode. `value` is not always all-or-nothing, though: when a claim returns an object, `value` mode can have specific fields redacted (configured through `redaction_fields`), and the result carries the object minus those fields plus a list of what was withheld. Per-claim disclosure control is enforced at runtime by the service. It is not left to a caller's good behaviour. ## The evaluation pipeline: computed answers, not record handoffs -The reason an answer can stand in for a record is that the caller never supplies the value being asked about. To evaluate a claim, the caller sends a **subject identifier** and a **claim id** over a REST call authenticated the way the rest of the service is — an API key or bearer token in static-credential mode, or an OIDC token, whichever your deployment is configured for. Registry Notary then performs the evaluation against its own configured sources. The caller does not supply the evaluated value, and cannot inject one. +The reason an answer can stand in for a record is that the caller never supplies the value being asked about. To evaluate a claim, the caller sends a **subject identifier** and a **claim id** over a REST call authenticated the way the rest of the service is: an API key or bearer token in static-credential mode, or an OIDC token, whichever your deployment is configured for. Registry Notary then performs the evaluation against its own configured sources. The caller does not supply the evaluated value, and cannot inject one. The evaluation runs as a pipeline: a source connector resolves facts about the subject, a rule computes the configured condition, a disclosure mode shapes what leaves the service, and a response format carries the result. Because Notary computes the answer itself, it can return that computed answer rather than handing back the source record it read. @@ -39,19 +41,19 @@ The disclosure mode fixes how much of the computed answer the caller receives: | `predicate` | only the true/false satisfaction of the rule | | `redacted` | no value and no satisfaction outcome | -`value` returns the full value, so this mode does hand over the computed result in full — it is the least private of the three and is appropriate only where the use case calls for the actual value. `predicate` returns only the boolean satisfaction. `redacted` returns neither the underlying value nor the yes/no outcome. +`value` returns the full value, so this mode does hand over the computed result in full; it is the least private of the three and is appropriate only where the use case calls for the actual value. `predicate` returns only the boolean satisfaction. `redacted` returns neither the underlying value nor the yes/no outcome. Every evaluation result records which disclosure mode was applied, so a downstream system can tell how much was revealed. ## Why predicate and redacted avoid sharing the underlying record -This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` when a matching record is found, while a subject with no matching record returns `evidence.not_available` (collapsed to a single public reason by default) rather than `false` — absence is surfaced as no-evidence, not as a negative result. Either way the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. +This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` when a matching record is found, while a subject with no matching record returns `evidence.not_available` (collapsed to a single public reason by default) rather than `false`; absence is surfaced as no-evidence, not as a negative result. Either way the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. One current renderer limitation to note: this no-outcome guarantee holds for the standard result body, but the CCCEV JSON-LD renderer currently emits `cccev:isConformantTo: false` for a redacted result instead of omitting it. A deployment that allows both redacted disclosure and CCCEV output should be aware that the outcome is not fully withheld in that render. -The difference between the two matters. `predicate` is not "zero disclosure" — it still tells the caller a true-or-false fact about the subject. `redacted` goes one step further: it does not leak even a yes/no. They are not interchangeable. +The difference between the two matters. `predicate` is not "zero disclosure"; it still tells the caller a true-or-false fact about the subject. `redacted` goes one step further: it does not leak even a yes/no. They are not interchangeable. ## How disclosure policy is configured per claim @@ -59,11 +61,11 @@ The caller does not freely pick from all three modes. Each claim configures a ** ## What a redacted result still reveals (and to whom) -"Reveals nothing to the caller" is not the same as "leaves no trace." A `redacted` evaluation can still be pointed back at: it carries an `evaluation_id`, and the audit record carries a `verification_id` and a `claim_hash`. That lets you reference the evaluation that happened — it is not a cryptographic signature on the answer. Redaction shapes what the caller sees; it does not erase the fact that an evaluation happened. +"Reveals nothing to the caller" is not the same as "leaves no trace." A `redacted` evaluation can still be pointed back at: it carries an `evaluation_id`, and the audit record carries a `verification_id` and a `claim_hash`. That lets you reference the evaluation that happened; it is not a cryptographic signature on the answer. Redaction shapes what the caller sees; it does not erase the fact that an evaluation happened. -That trace is by design. Every evaluation — redacted or not — is audited, and a deployment can run audit fail-closed, so that a request whose audit record cannot be written does not succeed. So redaction does not mean an unlogged request; it means an unrevealed answer. +That trace is by design. Every evaluation (redacted or not) is audited, and a deployment can run audit fail-closed, so that a request whose audit record cannot be written does not succeed. So redaction does not mean an unlogged request; it means an unrevealed answer. -There is a related protection on the matching surface. By default, a matching failure collapses to a single public reason, `evidence.not_available`, so the lookup cannot be used as an existence oracle — a "not found" answer does not tell the caller whether a record exists. This complements `redacted` disclosure by not leaking record existence. A deployment can turn this off, but only in a controlled environment; it is not always on. +There is a related protection on the matching surface. By default, a matching failure collapses to a single public reason, `evidence.not_available`, so the lookup cannot be used as an existence oracle; a "not found" answer does not tell the caller whether a record exists. This complements `redacted` disclosure by not leaking record existence. A deployment can turn this off, but only in a controlled environment; it is not always on. ## Disclosure control is one safeguard among several @@ -73,17 +75,17 @@ Disclosure modes are one runtime safeguard within a broader posture that also in A few boundaries matter when you evaluate this design: -- **Minimization is conditional, not absolute.** A minimized answer can be narrower than the full source record only where the configured use case supports that pattern. Notary does not always return less than the record — `value` mode returns the full value, and a claim reveals only what its policy was configured to reveal. -- **Governance depends on configured policy.** A source binding with no matching policy falls back to unrestricted, identifier-only resolution, with no purpose, relationship, or input-minimization gating. Minimization is not automatic; it follows the configuration. -- **The caller cannot inject a value, but matching strictness is configured.** The "computed answer avoids sharing the record" guarantee rests on Notary computing the answer from a subject id and claim id. Identity matching is only as strict as its configured policy. -- **A sidecar token is not a per-subject boundary.** A source adapter sidecar's internal static bearer token is not by itself an end-user, tenant, or subject authorization boundary; the sidecar hop does not enforce per-subject access. -- **Disclosure modes are not credential selective disclosure.** A holder's later choice of which credential fields to present is a separate mechanism from Notary's three evaluation modes. Do not read `value`/`predicate`/`redacted` as the same thing as a wallet holder selectively disclosing credential fields. -- **Aligning with a standard is not conforming to it.** This page describes Notary's own disclosure behaviour. It is not an endorsement of, or conformance to, any outside specification. +- Minimization is conditional, not absolute: A minimized answer can be narrower than the full source record only where the configured use case supports that pattern. Notary does not always return less than the record; `value` mode returns the full value, and a claim reveals only what its policy was configured to reveal. +- Governance depends on configured policy: A source binding with no matching policy falls back to unrestricted, identifier-only resolution, with no purpose, relationship, or input-minimization gating. Minimization is not automatic; it follows the configuration. +- The caller cannot inject a value, but matching strictness is configured: The "computed answer avoids sharing the record" guarantee rests on Notary computing the answer from a subject id and claim id. Identity matching is only as strict as its configured policy. +- A sidecar token is not a per-subject boundary: A source adapter sidecar's internal static bearer token is not by itself an end-user, tenant, or subject authorization boundary; the sidecar hop does not enforce per-subject access. +- Disclosure modes are not credential selective disclosure: A holder's later choice of which credential fields to present is a separate mechanism from Notary's three evaluation modes. Do not read `value`/`predicate`/`redacted` as the same thing as a wallet holder selectively disclosing credential fields. +- Aligning with a standard is not conforming to it: This page describes Notary's own disclosure behaviour. It is not an endorsement of, or conformance to, any outside specification. ## Related -- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/) — the full posture this fits into -- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) — the privacy view -- [Known limitations and non-guarantees](../known-limitations/) — where the edges are -- [Records stay home](../records-stay-home/) — what stays inside the institution and what crosses out +- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/): the full posture this fits into +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/): the privacy view +- [Known limitations and non-guarantees](../known-limitations/): where the edges are +- [Records stay home](../records-stay-home/): what stays inside the institution and what crosses out - How a claim and its disclosure policy are defined: [RS-DM-CLAIM](../../spec/rs-dm-claim/); disclosure modes, credentials, and federation: [RS-PR-NOTARY](../../spec/rs-pr-notary/) diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index e4645eb0..eeaf643f 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -16,7 +16,7 @@ standards_referenced: - odrl --- -You already know what Registry Stack is for: answer a question about a record without sharing the underlying record. This page answers a different question — *what does Registry Stack not yet do or guarantee?* — by giving you, in one place, the honest list of where the boundaries are. +You already know what Registry Stack is for: answer a question about a record without sharing the underlying record. This page answers a different question (*what does Registry Stack not yet do or guarantee?*) by giving you, in one place, the honest list of where the boundaries are. ## What this page is and how to use it @@ -26,15 +26,15 @@ The limitations are not invented for this page. Each protocol and data-model spe Three things are out of scope here, and each is a link instead: -- **Why a given limit exists** — read the relevant specification. -- **Whether it will change** — see the project roadmap (`ROADMAP.md` in the repository root). This page describes current behavior only. -- **How to work around a limit** — mitigations are not covered here. Follow the linked specification for each boundary, and see `release/VERIFY.md` in the repository for release verification. +- Why a given limit exists: read the relevant specification. +- Whether it will change: see the project roadmap (`ROADMAP.md` in the repository root). This page describes current behavior only. +- How to work around a limit: mitigations are not covered here. Follow the linked specification for each boundary, and see `release/VERIFY.md` in the repository for release verification. -Use this page as a checklist before you commit. If a limit below is a dealbreaker for your deployment, better to find that out before integrating than after. +Use this page as a checklist before you commit. If one of these limits is a dealbreaker for your deployment, better to find that out before integrating than after. ## Everything here is draft -Every governing specification — architecture, security, protocol, and data-model — is currently at lifecycle status `draft`: under development or review, not `current` (in force). The contracts these limitations are stated against are themselves not yet finalized. +Every governing specification (architecture, security, protocol, and data-model) is currently at lifecycle status `draft`: under development or review, not `current` (in force). The contracts these limitations are stated against are themselves not yet finalized. Registry Stack is a pre-1.0 technical release for evaluation, integration pilots, and public review. It is not a production support commitment and carries no hosted service-level agreement. @@ -44,76 +44,76 @@ Two things follow, and both matter as you read the rest of the page. First, the These boundaries cut across components. Read them first. -- **No source mutation, no event stream.** Registry Relay does not write back to or otherwise mutate source registry data, and it has no event-stream backend; sources are read as batch snapshots or table scans only. Registry Notary likewise has no file-based or database connector — a source connector reaches its target over HTTP, so the connector-to-target hop is HTTP-only. -- **Aggregate routes are not a privacy budget.** Relay aggregate routes return the configured statistical observations, but provide no built-in longitudinal privacy budget and do not track cumulative disclosure across repeated or overlapping queries. Do not describe an aggregate route as privacy-budgeted unless a separate deployed control provides that protection. -- **A published manifest grants nothing.** Publishing a dataset, policy, evidence offering, or federation relationship in a discovery artifact grants no access, enforces nothing, and asserts no fact about a live record. Federation discovery metadata is not a trust anchor: a published manifest does not bind a consumer to trust a JWKS endpoint, federation API host, or peer identity. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. -- **Registry Lab and its hosted instance are demonstrations, not infrastructure.** The local Registry Lab is a compose-based runnable demonstration that uses fixture credentials and demo-grade configuration and ships no production deployment guidance. Any hosted instance of the lab is likewise a demonstration, not production infrastructure: it carries no uptime or data-retention commitment. +- No source mutation, no event stream: Registry Relay does not write back to or otherwise mutate source registry data, and it has no event-stream backend; sources are read as batch snapshots or table scans only. Registry Notary likewise has no file-based or database connector; a source connector reaches its target over HTTP, so the connector-to-target hop is HTTP-only. +- Aggregate routes are not a privacy budget: Relay aggregate routes return the configured statistical observations, but provide no built-in longitudinal privacy budget and do not track cumulative disclosure across repeated or overlapping queries. Do not describe an aggregate route as privacy-budgeted unless a separate deployed control provides that protection. +- A published manifest grants nothing: Publishing a dataset, policy, evidence offering, or federation relationship in a discovery artifact grants no access, enforces nothing, and asserts no fact about a live record. Federation discovery metadata is not a trust anchor: a published manifest does not bind a consumer to trust a JWKS endpoint, federation API host, or peer identity. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- Registry Lab and its hosted instance are demonstrations, not infrastructure: The local Registry Lab is a compose-based runnable demonstration that uses fixture credentials and demo-grade configuration and ships no production deployment guidance. Any hosted instance of the lab is likewise a demonstration, not production infrastructure: it carries no uptime or data-retention commitment. -For the rationale behind each of these, follow the linked specifications in the sections below. +For the rationale behind each of these, follow the linked specifications in each section. ## Credential issuance and lifecycle limits (Notary) Registry Notary issues credentials, but the issuance surface is deliberately narrow. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/). -- **One credential format, one binding method when binding is enabled.** Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. -- **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. The capability-discovery document (`/.well-known/evidence-service`) declares `openid4vci.support: not_full_issuer` — announcing it is not a full issuer; this flag lives there rather than in the OID4VCI credential-issuer metadata. -- **Delegated-attestation issuance is limited to the direct path.** The OID4VCI transaction-token path for delegated attestation is rejected at the credential endpoint. Direct credential issuance via `/v1/credentials` is supported when the stored evaluation's access mode is delegated-attestation and the relationship allow-lists the credential profile. -- **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, the public status endpoint reads a credential's status, and only the admin status endpoint — which requires the `registry:notary:admin` scope — can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. -- **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. -- **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. -- **Standalone admin reload is not implemented.** The standalone Notary `/admin/v1/reload` route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload, and non-swappable changes require a service restart. Governed config apply and the file-watch signer do hot-apply some changes without one — signing-key rotation reports `restart_required: false`, client and OpenAPI auth-policy changes are swapped in, and a same-public-key signer replacement reloads — so not every key or configuration change needs a restart. +- One credential format, one binding method when binding is enabled: Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. +- A profiled issuance subset, not a full issuer: The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile (a profiled subset of Draft 13 using the `dc+sd-jwt` format), not a full OID4VCI issuer and not a claim of general external-wallet interoperability. The capability-discovery document (`/.well-known/evidence-service`) declares `openid4vci.support: not_full_issuer` (announcing it is not a full issuer); this flag lives there rather than in the OID4VCI credential-issuer metadata. +- Delegated-attestation issuance is limited to the direct path: The OID4VCI transaction-token path for delegated attestation is rejected at the credential endpoint. Direct credential issuance via `/v1/credentials` is supported when the stored evaluation's access mode is delegated-attestation and the relationship allow-lists the credential profile. +- No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow: The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, the public status endpoint reads a credential's status, and only the admin status endpoint (which requires the `registry:notary:admin` scope) can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- CCCEV output is a profiled shape, not conformant: [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. +- Two signed artifacts, not one: Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default; ES256/P-256 is also supported per credential profile; with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts: don't describe one as the other. +- Standalone admin reload is not implemented: The standalone Notary `/admin/v1/reload` route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload, and non-swappable changes require a service restart. Governed config apply and the file-watch signer do hot-apply some changes without one (signing-key rotation reports `restart_required: false`, client and OpenAPI auth-policy changes are swapped in, and a same-public-key signer replacement reloads), so not every key or configuration change needs a restart. ## Federation limits Federation across institutions is static-peer only. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/) and the [security model](../../spec/rs-sec-g/). -- **Static peers only.** Replay protection defaults to in-process, in-memory state, but a Redis-backed shared replay store is available for multi-instance deployments (`replay.storage = redis`, with `federation.replay.storage = redis` when the top-level store is Redis), and federation uses it for request replay and `jti` checks. There is still no dynamic trust-chain discovery and no federated credential issuance; both are out of scope for this version. -- **Delegated evaluation returns a result, never a credential.** Across a federation boundary, what crosses is a scoped evaluation result — never an issued credential. +- Static peers only: Replay protection defaults to in-process, in-memory state, but a Redis-backed shared replay store is available for multi-instance deployments (`replay.storage = redis`, with `federation.replay.storage = redis` when the top-level store is Redis), and federation uses it for request replay and `jti` checks. There is still no dynamic trust-chain discovery and no federated credential issuance; both are out of scope for this version. +- Delegated evaluation returns a result, never a credential: Across a federation boundary, what crosses is a scoped evaluation result, never an issued credential. ## Registry Relay limits Registry Relay exposes protected, read-only registry routes. Its boundaries are covered in depth in the [Registry Relay protocol](../../spec/rs-pr-relay/). -- **Read-only, batch or scan.** Relay does not mutate source data in v1 and has no event-stream backend; it reads sources as batch snapshots or table scans only. -- **Feature-gated surfaces.** Several Relay surfaces — the OGC API Features, Records, and EDR adapters and the SP DCI sync adapter — are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. (Signed response credentials are not Cargo-gated; they are config-gated through `provenance`.) -- **Aggregates are not privacy-budgeted.** As noted stack-wide, Relay aggregate routes provide no longitudinal privacy-budget or cumulative-differencing protection. -- **Relay does not evaluate or own the manifest.** Relay does not evaluate claims or issue Notary's SD-JWT VC credentials — that is Notary's role — though with `provenance` enabled and a caller requesting `Accept: application/vc+jwt` it can attach its own VC-JWT signed response credential. It also does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. -- **Governed enforcement is one PDP profile, not full interoperability.** Governed runtime policy enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1` — the single PDP profile this build recognizes). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of [ODRL](../../reference/standards/) terms outside the supported set. +- Read-only, batch or scan: Relay does not mutate source data in v1 and has no event-stream backend; it reads sources as batch snapshots or table scans only. +- Feature-gated surfaces: Several Relay surfaces (the OGC API Features, Records, and EDR adapters and the SP DCI sync adapter) are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. (Signed response credentials are not Cargo-gated; they are config-gated through `provenance`.) +- Aggregates are not privacy-budgeted: As noted stack-wide, Relay aggregate routes provide no longitudinal privacy-budget or cumulative-differencing protection. +- Relay does not evaluate or own the manifest: Relay does not evaluate claims or issue Notary's SD-JWT VC credentials (that is Notary's role), though with `provenance` enabled and a caller requesting `Accept: application/vc+jwt` it can attach its own VC-JWT signed response credential. It also does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. +- Governed enforcement is one PDP profile, not full interoperability: Governed runtime policy enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`, the single PDP profile this build recognizes). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of [ODRL](../../reference/standards/) terms outside the supported set. ## Data-model limits (Claim and Manifest) These limits concern what the claim and manifest configuration loaders do and do not enforce. Read them in full in the [Claim data model](../../spec/rs-dm-claim/) and [Manifest data model](../../spec/rs-dm-manifest/). -- **The `plugin` rule type is unimplemented.** The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. -- **Load-time invariants are not all enforced.** The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. A disclosure default outside the allowed set and a dangling rule source surface as request or evaluation errors at runtime, not as startup failures. Duplicate claim ids are different: they are never reported, because the runtime lookup returns the first match by id, so a later duplicate is silently shadowed. -- **Identifier-only matching has no further gating.** A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. -- **Manifests describe; they do not enforce.** The portable metadata layer only describes — publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. -- **Rendered standards artifacts are well-formed, not certified.** Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. -- **Unknown manifest keys are ignored, not rejected.** Manifest runtime-binding exclusion is an enumerated list, not a deny-unknown-fields schema: an unrecognized key outside the runtime-only list is ignored rather than rejected. -- **Vocabulary influence is not vocabulary emission.** PROV-O is a design influence only: provenance-shaped concepts appear in audit fields and the claim provenance struct, but no PROV-O vocabulary terms are emitted as JSON-LD. No standalone SKOS artifact is published yet — only embedded SKOS-shaped nodes — and CPSV-AP has no Relay runtime metadata route. +- The `plugin` rule type is unimplemented: The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. +- Load-time invariants are not all enforced: The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. A disclosure default outside the allowed set and a dangling rule source surface as request or evaluation errors at runtime, not as startup failures. Duplicate claim ids are different: they are never reported, because the runtime lookup returns the first match by id, so a later duplicate is silently shadowed. +- Identifier-only matching has no further gating: A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. +- Manifests describe; they do not enforce: The portable metadata layer only describes: publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- Rendered standards artifacts are well-formed, not certified: Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. +- Unknown manifest keys are ignored, not rejected: Manifest runtime-binding exclusion is an enumerated list, not a deny-unknown-fields schema: an unrecognized key outside the runtime-only list is ignored rather than rejected. +- Vocabulary influence is not vocabulary emission: PROV-O is a design influence only: provenance-shaped concepts appear in audit fields and the claim provenance struct, but no PROV-O vocabulary terms are emitted as JSON-LD. No standalone SKOS artifact is published yet (only embedded SKOS-shaped nodes), and CPSV-AP has no Relay runtime metadata route. ## Security-model operator boundary A healthy, reachable, internally consistent deployment is not the same as a production-secure one. Read the full security boundary in the [security model](../../spec/rs-sec-g/). -- **Health checks do not certify key custody.** Readiness, liveness, and protocol-conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and key-provider approval remain operator responsibilities. -- **Many guarantees are the operator's to provide.** Secret and key provisioning, key custody and rotation schedule, audit retention and storage, tenant isolation, transport termination, edge rate limiting, deployment configuration, and incident response are operator responsibilities — not behaviors the stack guarantees. -- **Audit fail-closed is a capability, not a guarantee about every build.** A deployment can run audit fail-closed, so that a request whose audit record cannot be written does not return success. That is a switch a deployment is able to turn on — not a promise that every route in a given build has been individually audited, nor that every build ships with it on. -- **The Notary audit record omits the scopes field.** The security model says an audit record captures at least the caller principal, the scopes exercised, a request identifier, and the purpose. Notary's audit record body records the principal, request id, and purpose, but does not include the caller's exercised scopes — so a downstream review of Notary audit alone cannot reconstruct which scopes authorized a request. (Relay's audit record does record scopes.) -- **The source-adapter sidecar is an internal connector, not a public API.** The source adapter sidecar is an internal connector surface, not a public subject-facing API. Its static bearer token is not by itself a tenant or subject authorization boundary, and it relies on deployment-network egress controls for outbound source traffic. Direct public access, broad reuse of one sidecar token across unrelated sources, or shared cache or backoff state across authorization contexts is outside the security model. -- **Demo and template code is not a production profile.** Demo helper code and generated workflow snippets are integration examples, not a production freshness or replay-protection profile, and must not be relied on for freshness, expiry, or replay protection. +- Health checks do not certify key custody: Readiness, liveness, and protocol-conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and key-provider approval remain operator responsibilities. +- Many guarantees are the operator's to provide: Secret and key provisioning, key custody and rotation schedule, audit retention and storage, tenant isolation, transport termination, edge rate limiting, deployment configuration, and incident response are operator responsibilities, not behaviors the stack guarantees. +- Audit fail-closed is a capability, not a guarantee about every build: A deployment can run audit fail-closed, so that a request whose audit record cannot be written does not return success. That is a switch a deployment is able to turn on, not a promise that every route in a given build has been individually audited, nor that every build ships with it on. +- The Notary audit record omits the scopes field: The security model says an audit record captures at least the caller principal, the scopes exercised, a request identifier, and the purpose. Notary's audit record body records the principal, request id, and purpose, but does not include the caller's exercised scopes, so a downstream review of Notary audit alone cannot reconstruct which scopes authorized a request. (Relay's audit record does record scopes.) +- The source-adapter sidecar is an internal connector, not a public API: The source adapter sidecar is an internal connector surface, not a public subject-facing API. Its static bearer token is not by itself a tenant or subject authorization boundary, and it relies on deployment-network egress controls for outbound source traffic. Direct public access, broad reuse of one sidecar token across unrelated sources, or shared cache or backoff state across authorization contexts is outside the security model. +- Demo and template code is not a production profile: Demo helper code and generated workflow snippets are integration examples, not a production freshness or replay-protection profile, and must not be relied on for freshness, expiry, or replay protection. ## Supply-chain and release limits These concern how releases are signed and what that signing currently covers. -- **Partial release signing.** GitHub Release assets are signed with keyless cosign and, when tag-triggered, carry SLSA provenance. However, OCI image signatures are not yet published, and Git version tags are not yet cryptographically signed (GPG, SSH, or Sigstore). The `v0.8.0` prerelease was published before release-asset signing and does not currently include cosign signatures. +- Partial release signing: GitHub Release assets are signed with keyless cosign and, when tag-triggered, carry SLSA provenance. However, OCI image signatures are not yet published, and Git version tags are not yet cryptographically signed (GPG, SSH, or Sigstore). The `v0.8.0` prerelease was published before release-asset signing and does not currently include cosign signatures. For how to verify what *is* signed, see the release verification guidance in `release/VERIFY.md` in the repository. ## Where to go next -- **Why a given limit exists** — the rationale and trade-offs live in each specification's own limitations section: [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), and [RS-DM-MANIFEST](../../spec/rs-dm-manifest/). For the security and architecture boundaries, read [RS-SEC-G](../../spec/rs-sec-g/) and [RS-ARC-G](../../spec/rs-arc-g/). -- **How each external standard is actually adopted** — the [standards register](../../reference/standards/) records the adoption mode for every standard named across the stack. -- **What might change** — the project roadmap (`ROADMAP.md` in the repository root). This page does not track it. -- **How to verify what is signed** — release verification lives in `release/VERIFY.md` in the repository. +- Why a given limit exists: the rationale and trade-offs live in each specification's own limitations section: [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), and [RS-DM-MANIFEST](../../spec/rs-dm-manifest/). For the security and architecture boundaries, read [RS-SEC-G](../../spec/rs-sec-g/) and [RS-ARC-G](../../spec/rs-arc-g/). +- How each external standard is actually adopted: the [standards register](../../reference/standards/) records the adoption mode for every standard named across the stack. +- What might change: the project roadmap (`ROADMAP.md` in the repository root). This page does not track it. +- How to verify what is signed: release verification lives in `release/VERIFY.md` in the repository. diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index b6db6e6f..5507d50d 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -1,6 +1,6 @@ --- title: Records stay home -description: How an institution proves facts from registries it already holds — without the records leaving. +description: How an institution proves facts from registries it already holds, without the records leaving. status: draft owner: registry-docs source_repos: @@ -13,13 +13,13 @@ standards_referenced: --- An institution that runs a civil registry, a social-protection database, or a health -registry already holds the records it needs. Registry Stack lets it **answer questions -about those records** — *is this person alive? is this household eligible?* — and return -a result another system can trust, while the records themselves are **read where they -already live, never written back, and never handed over**. - +registry already holds the records it needs. +Registry Stack lets it **answer questions about those records** (*is this person alive? is +this household eligible?*) and return a result another system can trust, while the records +themselves are **read where they already live, never written back, and never handed +over**. This page explains what that means in practice: what stays inside the institution's -boundary, what crosses it, and — equally important — what the design does and does not +boundary, what crosses it, and (equally important) what the design does and does not guarantee. ## A question goes in, an answer comes out @@ -28,8 +28,8 @@ The mental model is one sentence: **a scoped question crosses into the instituti record is read in place, and only a computed answer crosses back out.** A caller never sends the value it is asking about and never receives the underlying -record. It sends a subject identifier and the id of a *claim* — a single, pre-modelled -question — and receives one of a few narrow shapes of answer: a yes/no, a single value, a +record. It sends a subject identifier and the id of a *claim* (a single, pre-modelled +question) and receives one of a few narrow shapes of answer: a yes/no, a single value, a machine-readable evaluation result, or a credential the subject can carry in a wallet. The source row that the answer was computed from stays behind. @@ -37,7 +37,7 @@ answer was computed from stays behind. ```mermaid flowchart LR - subgraph inst["Institution — data stays here"] + subgraph inst["Institution: data stays here"] src[("Source registry\nCSV · XLSX · Parquet · PostgreSQL")] relay["Registry Relay\nprotected read API"] notary["Registry Notary\nevaluate · disclose · issue"] @@ -68,39 +68,39 @@ disclosure policy, and issues credentials. ## What stays home -- **Source data is read in place.** Relay reads sources as batch snapshots or table scans; +- Source data is read in place: Relay reads sources as batch snapshots or table scans; there is no write-back to the source registry, and runtime services expose no data-mutation routes. The source keeps running as it always has. "No write-back to the - source and no external handoff" is not the same as "no copy exists": in snapshot mode — - the default — Relay materializes a projected copy into its local cache (`cache_dir`), which + source and no external handoff" is not the same as "no copy exists": in snapshot mode + (the default), Relay materializes a projected copy into its local cache (`cache_dir`), which carries its own retention and access considerations. -- **Storage internals stay private.** The paths, table names, and backend credentials that +- Storage internals stay private: The paths, table names, and backend credentials that point at the source live in the service's runtime configuration, decided at startup. They are never part of the public API surface, and never part of a portable metadata file that gets distributed. -- **The institution keeps custody.** The design premise is *distributed custody*: each +- The institution keeps custody: The design premise is *distributed custody*: each authority retains control of its own registry data, and the stack does not aggregate records into a central system. It provides the exchange surface, not a data lake. -- **Private signing keys never leave the issuer.** The institution publishes the *public* +- Private signing keys never leave the issuer: The institution publishes the *public* half of its signing key so anyone can verify a signed credential or signed result; the private half stays inside. ## What crosses the boundary -Only a computed answer crosses out — never the source row. The answer takes one of a few +Only a computed answer crosses out: never the source row. The answer takes one of a few shapes: -- **A yes/no** — only the true/false satisfaction of the modelled rule. -- **A single value** — the evaluated value itself, when the claim's disclosure mode is +- A yes/no: only the true/false satisfaction of the modelled rule. +- A single value: the evaluated value itself, when the claim's disclosure mode is `value` (this returns the full value). -- **A machine-readable evaluation result** — a claim-result document carrying *provenance +- A machine-readable evaluation result: a claim-result document carrying *provenance metadata*: which evaluation produced it, under which policy, across how many sources. This provenance lets a receiving system trace the result; it is not a cryptographic signature. -- **A holder-bound credential** — an SD-JWT VC the subject can store in a wallet and present +- A holder-bound credential: an SD-JWT VC the subject can store in a wallet and present later. Unlike the plain result, the credential is cryptographically verifiable against the issuer's published keys. -Across a federation boundary — one institution's Notary asking another's — what crosses is +Across a federation boundary (one institution's Notary asking another's), what crosses is a scoped, signed evaluation result, never a credential. ## How much an answer reveals: the three disclosure modes @@ -112,7 +112,7 @@ receives. There are exactly three: |------|-----------|-----------| | `value` | the full evaluated value | nothing about the value | | `predicate` | only the true/false satisfaction | the underlying value | -| `redacted` | neither — the result carries no value **and** no yes/no | the value *and* the outcome | +| `redacted` | neither: the result carries no value **and** no yes/no | the value *and* the outcome | The policy bounds which modes are allowed, and the caller chooses among them: a claim defines an `allowed` set and a `default`, the service honours a requested mode when it is in @@ -131,8 +131,8 @@ stays home. A credential is not a copy of the record. It is an **SD-JWT VC**: the signed body carries a SHA-256 *digest* of each selectively disclosable field rather than the field value, so a -field the holder does not present stays hidden. It **can be** holder-bound — tied to the -holder's key so it is not presentable without the matching private key — when the issuing +field the holder does not present stays hidden. It **can be** holder-bound (tied to the +holder's key so it is not presentable without the matching private key) when the issuing profile enables binding (the wallet issuance path does); the default profile issues an unbound credential. Either way, the holder chooses which fields to reveal to which verifier. Anyone can verify it against the issuer's published public keys, served without authentication so a verifier needs no credential of @@ -143,57 +143,57 @@ its own. The issued credential carries no full record payload. The "stays home" property rests on a few enforced rules, covered in depth in the Trust & Security material: -- **Scope-before-source, deny-by-default.** A service checks the caller's scope *before* it +- Scope-before-source, deny-by-default: A service checks the caller's scope *before* it reads any source or evaluates any claim, and does not widen a caller's reach at request time beyond what its configuration grants. Anything that touches a record or a claim requires authentication; the routes reachable without it are the operational, discovery, and protocol-bootstrap surfaces: liveness and readiness probes, the public verification keys, credential-issuance discovery metadata, the OID4VCI wallet-flow endpoints, the docs, public credential-status reads, and credential type metadata. -- **A permit, or a closed door.** On a governed read, the policy decision point must return +- A permit, or a closed door: On a governed read, the policy decision point must return a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. -- **Every person-level request is audited.** An audit record captures at least the caller, +- Every person-level request is audited: An audit record captures at least the caller, a request id, and the declared purpose where one was supplied; Relay additionally records the scopes exercised (Notary's record does not). A deployment can run audit fail-closed, so a request whose audit record cannot be written does not return success. -## What this guarantees — and what it does not +## What this guarantees, and what it does not "Records stay home" is a precise, narrow promise. Reading it as more than it is would be a mistake, so the limits are stated plainly here. -- **It is not "data never moves" and not "air-gapped".** The promise is *read-in-place, no +- It is not "data never moves" and not "air-gapped": The promise is *read-in-place, no write-back, retained custody*. Authorized, minimized answers do leave the boundary by - design — that is the point of the system. -- **Minimization is modelled, not automatic.** `value` mode discloses the full value. A + design: that is the point of the system. +- Minimization is modelled, not automatic: `value` mode discloses the full value. A claim reveals only what its author configured it to reveal; least disclosure is a design choice the claim makes, not a property the stack imposes on every answer. -- **Correctness depends on the source.** Notary reports what the configured source says; it +- Correctness depends on the source: Notary reports what the configured source says; it does not independently vouch for whether the source is correct or current. -- **A plain result is provenance-tagged, not signed.** The everyday evaluation response +- A plain result is provenance-tagged, not signed: The everyday evaluation response carries provenance metadata, not a cryptographic signature. Cryptographic verifiability - comes from the SD-JWT VC credential and the signed federation result — a receiving system + comes from the SD-JWT VC credential and the signed federation result: a receiving system that must verify an answer cryptographically uses the credential, not the default response. -- **Matching is only as strict as it is configured.** Notary resolves a subject through its +- Matching is only as strict as it is configured: Notary resolves a subject through its configured matching policy and does not independently verify identity beyond that. By default a matching failure collapses to a single public reason, so the lookup surface cannot be used as an existence oracle. -- **This is not zero-knowledge.** A `predicate` answer is a policy-enforced boolean computed +- This is not zero-knowledge: A `predicate` answer is a policy-enforced boolean computed inside the service; SD-JWT selective disclosure is digest omission. Neither is a zero-knowledge proof, and the documentation should not imply one. -- **No revocation flow is specified, and no erasure flow.** This version specifies issuance, +- No revocation flow is specified, and no erasure flow: This version specifies issuance, disclosure, presentation, and verification, but no credential-revocation flow and no data-subject erasure. An optional, off-by-default credential-status surface can be enabled to mark a credential `revoked`, but that is an operator-enabled capability rather than a specified flow. A key rotated out may remain published so existing results stay - verifiable — that is not a revocation mechanism. -- **Several guarantees are the operator's to provide.** Network egress limits, key custody, + verifiable: that is not a revocation mechanism. +- Several guarantees are the operator's to provide: Network egress limits, key custody, tenant isolation, audit retention, and transport security are supplied by the deployment, not guaranteed by the stack. -- **The model is specified in draft.** The behaviour above is defined in the `RS-*` - specifications, which are still drafts and may change. Alignment with an external standard - is not a claim of conformance to it or of legal compliance. +- The model is specified in draft: The behaviour described in this section is defined in + the `RS-*` specifications, which are still drafts and may change. Alignment with an + external standard is not a claim of conformance to it or of legal compliance. ## Related diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index 3947d7f9..9021a686 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -1,6 +1,6 @@ --- title: Threat model -description: The trust boundaries, assets, and threats the Registry Stack design considers — what it mitigates, and the residual risks it leaves to the operator or treats as out of scope. +description: The trust boundaries, assets, and threats the Registry Stack design considers (what it mitigates, and the residual risks it leaves to the operator or treats as out of scope). status: draft owner: registry-docs source_repos: @@ -17,15 +17,16 @@ standards_referenced: This page is for a reviewer or auditor who already reasons in terms of assets, trust boundaries, and adversaries, but does not yet know where the Registry Stack's boundaries -actually sit. It answers one question: **why does this design produce the security -properties it claims — which threats does the architecture consider, what does it actually +actually sit. +It answers one question: **why does this design produce the security +properties it claims: which threats does the architecture consider, what does it actually mitigate, and where do the residual risks lie?** - -It is deliberately a map of boundaries and limits, not a runbook. Hardening procedures -belong to the Operate section, the high-level posture summary to the Trust & Security -overview, and privacy obligations to [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/). Here the goal is a -defensible threat model: the boundaries the design draws, the threats it places in and out -of scope, and the residual risks stated honestly. +It is deliberately a map of boundaries and limits, not a runbook. +Hardening procedures belong to the Operate section, the high-level posture summary to the +Trust & Security overview, and privacy obligations to [Data minimization and purpose +limitation](../data-minimization-and-purpose-limitation/). +Here the goal is a defensible threat model: the boundaries the design draws, the threats it +places in and out of scope, and the residual risks stated honestly. ## The two-layer architecture and its primary trust boundary @@ -47,14 +48,14 @@ out of the portable artifact entirely. Five components sit on or beside the trust boundaries: -- **Registry Manifest** — the offline metadata producer. No production data, no auth, no +- Registry Manifest: the offline metadata producer. No production data, no auth, no secrets. It lives entirely on the describe side of the primary boundary. -- **Registry Relay** — a read-only consultation gateway over sources. -- **Registry Notary** — claim evaluation, credential issuance, and federation. This is the +- Registry Relay: a read-only consultation gateway over sources. +- Registry Notary: claim evaluation, credential issuance, and federation. This is the component that evaluates a modelled question, applies disclosure policy, and issues credentials. -- **Registry Platform** — the shared security primitives that the runtime services build on. -- **Registry Lab** — demonstration only, running on fixture credentials. Treat it as out of +- Registry Platform: the shared security primitives that the runtime services build on. +- Registry Lab: demonstration only, running on fixture credentials. Treat it as out of scope for production trust: its demo and template integrations are integration examples, not a production freshness or replay-protection profile, and a team copying them into production must add request freshness, expiry, or nonce checks itself. @@ -67,8 +68,8 @@ auditing separately. ## Trust boundaries in detail **The service edge (authentication).** Authentication is the trust boundary at the service -edge. Each service runs exactly one authentication mode — either static-credential -fingerprint or OIDC — and authenticates every claim- or record-bearing route before +edge. Each service runs exactly one authentication mode (either static-credential +fingerprint or OIDC) and authenticates every claim- or record-bearing route before responding. The unauthenticated surfaces are the operational, discovery, and protocol-bootstrap routes: liveness and readiness probes, the public verification keys (JWKS / `did:web`), credential-issuer discovery, the OID4VCI wallet-flow endpoints, the @@ -78,7 +79,7 @@ peer-signed JWS, so it is not anonymous. **Notary to its sources.** A runtime boundary sits between Registry Notary and the sources it reads. Notary calls Relay over HTTP as a data source; it does not link Relay code. The -only connectors are HTTP (`registry_data_api`, `dci`, and the source adapter sidecar) — there +only connectors are HTTP (`registry_data_api`, `dci`, and the source adapter sidecar); there is no file or database connector. **The source adapter sidecar.** The sidecar is an *internal* trust boundary, not a @@ -86,8 +87,8 @@ subject-authorization boundary, and this distinction matters for an auditor. Its token authenticates only the Notary-to-sidecar hop; it is not an end-user, tenant, or subject authorization boundary. It is an internal, private deployment surface behind Notary, not a public subject-facing API. Keeping the token off the public network and scoping its -credentials, cache, and backoff state is outside the security model — it is an operator -responsibility — and shared cache or backoff state across authorization contexts is not +credentials, cache, and backoff state is outside the security model (it is an operator +responsibility), and shared cache or backoff state across authorization contexts is not something the model reasons about. **Federation between two Notary services.** Federation is a deliberate trust boundary @@ -95,22 +96,22 @@ between two Notary services, and it is **static-peer only**: only configured sta admitted. It is not a dynamic trust mesh. **Delegated self-attestation.** Delegated self-attestation is a distinct trust context from -peer federation — don't confuse the two. It does not delegate trust to a peer +peer federation; don't confuse the two. It does not delegate trust to a peer Notary; it stays inside the citizen / OIDC trust context. ## Assets protected by the design The assets the design sets out to protect are: -- **Private signing keys.** The private signing key never leaves the issuer; only the public - half is published — through JWKS, and through `did:web` in Relay gateway mode. A verifier +- Private signing keys: The private signing key never leaves the issuer; only the public + half is published (through JWKS, and through `did:web` in Relay gateway mode). A verifier never needs a credential to verify, which keeps verification off the protected side entirely. -- **Person-level source data.** The source rows behind a claim are read in place and never +- Person-level source data: The source rows behind a claim are read in place and never cross the boundary; only a computed, disclosure-shaped answer does. -- **Audit integrity.** Audit is treated as a security control rather than best-effort - logging (see below). -- **Secret material.** Bearer tokens, raw credentials, and key material are kept out of both +- Audit integrity: Audit is treated as a security control rather than best-effort + logging (see Audit as a control). +- Secret material: Bearer tokens, raw credentials, and key material are kept out of both the portable layer and client-facing surfaces. The project itself treats a specific set of threat classes as security-relevant, which is a @@ -120,138 +121,138 @@ leakage, and privacy regressions that expose raw subject identifiers. ## Threats the design considers and mitigates -- **Secret disclosure via stored credentials or timing.** Static credentials are stored only - as SHA-256 fingerprints — never the raw secret — and compared in constant time. -- **Token forgery / acceptance of untrusted tokens (OIDC mode).** A token is trusted only +- Secret disclosure via stored credentials or timing: Static credentials are stored only + as SHA-256 fingerprints (never the raw secret) and compared in constant time. +- Token forgery / acceptance of untrusted tokens (OIDC mode): A token is trusted only after signature verification against the configured issuer JWKS, plus issuer, audience, and algorithm checks. The service still owns its route scopes regardless of the token. -- **Privilege escalation / over-reach.** Authorization is scope-based, deny-by-default, and +- Privilege escalation / over-reach: Authorization is scope-based, deny-by-default, and scope-before-source: a caller is refused before any source read or claim evaluation if it lacks the required scope, and reach is never widened at request time. -- **An existence / matching oracle.** By default a matching failure collapses to a single +- An existence / matching oracle: By default a matching failure collapses to a single public reason (`evidence.not_available`), with the granular reason kept only in audit, so the matching surface cannot be used to probe whether a record exists. A deployment may disable this collapse only in a controlled environment; disabling it is a residual confidentiality risk. -- **Information leakage in error and cache surfaces.** `problem+json` error bodies carry +- Information leakage in error and cache surfaces: `problem+json` error bodies carry stable codes only. Bearer tokens, private keys, source values, filesystem paths, and internal error chains stay in protected operator logs, never in responses; principal-scoped responses are not public cache entries. -- **Over-collection and value injection in claim evaluation.** The caller supplies only a +- Over-collection and value injection in claim evaluation: The caller supplies only a subject id and a claim id and must not supply the evaluated value; bindings read only the fields they need and reject input paths outside a declared allow-list. -- **Disclosure leakage.** A redacted result carries neither the value nor the predicate +- Disclosure leakage: A redacted result carries neither the value nor the predicate outcome. Selective-disclosure credentials carry SHA-256 digests of unselected fields, so a holder cannot present an undisclosed field. Holder binding is configurable per credential - profile and defaults to off; where a profile enables it (using `did:jwk`) — and on the - self-attestation issuance path, which requires it — the holder is bound by a fresh + profile and defaults to off; where a profile enables it (using `did:jwk`), and on the + self-attestation issuance path, which requires it, the holder is bound by a fresh audience-bound proof-of-possession. Binding applies only to an issued credential; the plain evaluation result is not a credential and is never holder-bound. -- **Self-attested data crossing a trust gate.** Self-attestation and delegated +- Self-attested data crossing a trust gate: Self-attestation and delegated self-attestation derive subject, requester, and relationship from the authenticated principal and scoped authorization details; caller-supplied `requester`, `relationship`, and `on_behalf_of` fields are rejected before any source read, and self-asserted target fields do not satisfy trusted-principal gates. -- **Spoofed trust context and ungoverned reads (Relay side).** Relay ignores trust-context +- Spoofed trust context and ungoverned reads (Relay side): Relay ignores trust-context headers unless the principal is scoped to assert that exact value, and a governed read must receive a PDP permit or fail closed with a stable `pdp.*` code rather than falling back to an ungoverned read. -- **SSRF / uncontrolled egress.** Outbound source fetches are meant to be constrained by Registry +- SSRF / uncontrolled egress: Outbound source fetches are meant to be constrained by Registry Platform's outbound-HTTP-policy. Where it is applied, it limits the destinations a source - connector may reach. This is recommended posture rather than a hard guarantee — confirm it + connector may reach. This is recommended posture rather than a hard guarantee: confirm it is enabled rather than assuming it. One detail an auditor should note: the guard exposes an `allow_insecure_private_network` opt-in that, per source connection, re-enables plain-HTTP private-network and link-local destinations the strict policy would otherwise refuse. Even with that opt-in, cloud-metadata addresses (such as `169.254.169.254` and its IPv6 equivalent) stay denied. Treat the opt-in as a deployment decision to review, not a default. -- **Untrusted federation peers, replay, and wrong-purpose requests.** Only configured static +- Untrusted federation peers, replay, and wrong-purpose requests: Only configured static peers are admitted, and before any source read the server verifies peer identity, request signature, freshness, single use, purpose, profile, and audience. The single-use and freshness checks are backed by a real replay-store primitive that tracks one-time JWT `jti` and nonce values (federation request `jti`, OID4VCI `c_nonce`, and holder-proof `jti`). -**Audit as a control.** Every request touching person-level data is recorded — principal, -request id, and `Data-Purpose` — and a deployment can run audit fail-closed, so an +**Audit as a control.** Every request touching person-level data is recorded (principal, +request id, and `Data-Purpose`) and a deployment can run audit fail-closed, so an unrecordable request does not return success. Two caveats for the auditor. First, this is a capability a deployment can turn on, not a guarantee that every route in a given build has been individually audited; treat per-route audit coverage as something to verify in your deployment. Second, the security model expects the audit record to capture the scopes -exercised as well, but Notary's audit record does not yet include that field — Relay's does — +exercised as well, but Notary's audit record does not yet include that field (Relay's does), so Notary audit alone cannot reconstruct which scopes authorized a request. ## Residual risks and what is left to the operator -The canonical inventory of every current limit is [Known limitations and non-guarantees](../known-limitations/); the residual risks below are the subset a threat model must weigh. +The canonical inventory of every current limit is [Known limitations and non-guarantees](../known-limitations/); the residual risks in this section are the subset a threat model must weigh. These are the risks the design does *not* close and states honestly: -- **Key custody is not certified by health checks.** Readiness, liveness, and conformance +- Key custody is not certified by health checks: Readiness, liveness, and conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and provider approval remain operator responsibilities. -- **The operator boundary is where coverage ends.** Secret and key provisioning, key custody +- The operator boundary is where coverage ends: Secret and key provisioning, key custody and rotation, audit retention and storage, tenant isolation, TLS termination and certificates, edge rate limiting, deployment configuration, and incident response are operator responsibilities, not behavior the model defines. -- **The sidecar depends on deployment-network controls.** The source adapter sidecar relies +- The sidecar depends on deployment-network controls: The source adapter sidecar relies on deployment-network egress controls; the model does not enforce them. -- **Aggregate routes are not a privacy budget.** Relay aggregate routes do not constitute a +- Aggregate routes are not a privacy budget: Relay aggregate routes do not constitute a longitudinal privacy budget; v1 does not track cumulative disclosure across repeated or overlapping aggregate queries. Do not describe a Relay aggregate as privacy-budgeted unless a separate deployed control provides it. This is a residual disclosure risk left to the operator. -- **Some claim-definition invariants surface only at runtime.** A disclosure default outside +- Some claim-definition invariants surface only at runtime: A disclosure default outside the allowed set, a rule source naming an undeclared binding, and the unimplemented plugin - rule are not enforced at configuration load — they surface only at evaluation or request + rule are not enforced at configuration load; they surface only at evaluation or request time. Duplicate claim ids are different and worse: they are not rejected at load and are never reported, because the runtime lookup returns the first match by id, so a later duplicate is silently shadowed. A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. These are residual misconfiguration risks left to the operator. -- **Known product gaps.** No revocation flow is specified — though an optional, - off-by-default credential-status surface can be enabled to mark a credential `revoked` — +- Known product gaps: No revocation flow is specified (though an optional, + off-by-default credential-status surface can be enabled to mark a credential `revoked`), there is no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure workflow in this version. -- **Admin reload is non-functional standalone.** Notary's admin reload route returns HTTP 501 +- Admin reload is non-functional standalone: Notary's admin reload route returns HTTP 501 (`registry.admin.capability.not_supported`) in the standalone router and performs no reload, and non-swappable changes require a service restart. Some changes do hot-apply - without a restart, though — signing-key rotation and auth-policy swaps among them. + without a restart, though: signing-key rotation and auth-policy swaps among them. ## What is explicitly out of scope These are non-goals for this version. None of them should be read into a Registry Stack conformance claim: -- **Dynamic federation.** Dynamic trust-chain discovery, audit checkpoint exchange, and - federated credential issuance are out of scope. Federation is static-peer only — not a +- Dynamic federation: Dynamic trust-chain discovery, audit checkpoint exchange, and + federated credential issuance are out of scope. Federation is static-peer only: not a dynamic trust mesh. Replay protection defaults to in-memory, with a Redis-backed shared store available. -- **Full Evidence Gateway / external-policy interoperability.** Governed Relay PDP enforcement +- Full Evidence Gateway / external-policy interoperability: Governed Relay PDP enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of ODRL terms outside the supported profile. Unsupported ODRL terms or invalid policy identity fail closed. -- **A full credential issuer.** The OID4VCI surface is a scoped self-attestation issuance +- A full credential issuer: The OID4VCI surface is a scoped self-attestation issuance subset of OID4VCI Draft 13, not a full issuer or general external-wallet interoperability. Notary advertises `openid4vci.support: not_full_issuer`, and delegated-attestation transaction tokens are rejected at the credential endpoint. -- **Certified standards compliance.** Aligning with a standard is not conforming to it: +- Certified standards compliance: Aligning with a standard is not conforming to it: speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, W3C DID, or the rest does not certify conformance to any of them, and CCCEV-shaped output is not conformant to CCCEV 2.00. -- **Feature-gated surfaces of other builds.** Several runtime surfaces — the OGC API - Features/Records/EDR adapters and SP DCI sync — are feature-gated and mount only when the +- Feature-gated surfaces of other builds: Several runtime surfaces (the OGC API + Features/Records/EDR adapters and SP DCI sync) are feature-gated and mount only when the build is configured with the matching Cargo feature. Do not infer their presence from the route catalog of a different build. Relay signed response credentials are not Cargo-gated; they are config-gated through `provenance`. Admin routes run on a separate optional listener and are documented in the generated OpenAPI, but standalone mode - does not implement runtime config reload — it returns HTTP 501. + does not implement runtime config reload; it returns HTTP 501. ## Related -- [RS-SEC-G](../../spec/rs-sec-g/) — the security model -- [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) — protocol contracts -- [RS-ARC-G](../../spec/rs-arc-g/) — the two-layer architecture -- [RS-DM-CLAIM](../../spec/rs-dm-claim/) — claim definitions and disclosure +- [RS-SEC-G](../../spec/rs-sec-g/): the security model +- [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/): protocol contracts +- [RS-ARC-G](../../spec/rs-arc-g/): the two-layer architecture +- [RS-DM-CLAIM](../../spec/rs-dm-claim/): claim definitions and disclosure - Hardening procedures *(Operate)* · Trust posture summary *(Trust & Security overview)* · [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx index 9472fb5a..9287ba60 100644 --- a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx +++ b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx @@ -19,8 +19,8 @@ standards_referenced: Before you adopt Registry Stack you want a straight answer to three questions: what does it guarantee about security and privacy, what does it leave for you to get right, and what does it deliberately *not* do? This page answers all three in plain terms. It is not a substitute -for the specifications — at the end you'll find links to the exact documents where each -property is defined — but it is the honest summary you can read first. +for the specifications (at the end you'll find links to the exact documents where each +property is defined) but it is the honest summary you can read first. ## At a glance @@ -33,7 +33,7 @@ property is defined — but it is the honest summary you can read first. | Three disclosure modes, with `redacted` revealing neither value nor answer | **Built in** | | Selective-disclosure credentials, with holder binding configurable per profile (off by default) | **Built in** | | Federation limited to peers you configure | **Built in** | -| Hardened HTTP headers and outbound-traffic limits | **Recommended** — confirm your build applies them | +| Hardened HTTP headers and outbound-traffic limits | **Recommended**: confirm your build applies them | | Key custody, retention, isolation, TLS, rate limiting | **Your deployment's job** | | Compliance with any external standard it cites | **Not claimed** | @@ -41,24 +41,24 @@ property is defined — but it is the honest summary you can read first. **Nothing answers without authenticating first.** Every route that returns a record or a claim result checks the caller's credential before it does anything else. A service runs one -authentication mode at a time — either API-key fingerprints (it stores a hash, never the raw +authentication mode at a time: either API-key fingerprints (it stores a hash, never the raw key, and compares in constant time) or OIDC tokens (it checks the signature, issuer, audience, and algorithm). The routes open to an anonymous caller are the operational and discovery -surfaces — health and readiness probes, the public verification keys, credential-issuer +surfaces (health and readiness probes, the public verification keys, credential-issuer discovery and the OID4VCI wallet-flow endpoints, the docs, public credential-status lookups, -and credential type metadata — while everything that returns a record or claim result still +and credential type metadata) while everything that returns a record or claim result still requires a credential. **Authorization is deny-by-default, and it happens before any data is touched.** A caller has to hold the scope a route requires, and that check runs *before* Relay reads a source or -Notary evaluates a claim — so a request can never quietly widen its own reach at the last +Notary evaluates a claim, so a request can never quietly widen its own reach at the last moment. **Access leaves a trail.** Every request that returns person-level data is recorded with who asked, a request id, and the declared purpose. (Relay's audit record also captures the scopes -exercised; Notary's audit record does not yet include that field — see the limitations hub.) You +exercised; Notary's audit record does not yet include that field. See the limitations hub.) You can run audit fail-closed, so that if the audit record can't be written the request doesn't -succeed — that's a switch you turn on for a deployment that needs it, not something that's +succeed: that's a switch you turn on for a deployment that needs it, not something that's always on by default. **An answer is shaped, not a copy of the record.** Every claim is answered in one of three @@ -70,29 +70,29 @@ covers this in depth. **Credentials are selective, and can be holder-bound.** When Notary issues a credential it's an SD-JWT VC: the signed body carries hashes of each disclosable field rather than the values, so the holder reveals only what they choose. Holder binding is configurable per credential profile -and defaults to off — when a profile enables it (using `did:jwk`), the credential is tied to the +and defaults to off: when a profile enables it (using `did:jwk`), the credential is tied to the holder's own key and can't be presented by anyone else. The self-attestation (wallet) issuance path requires binding with proof-of-possession, so credentials issued there are always bound. Anyone can verify a credential against the issuer's published keys without needing a credential of their own. The plain claim or evaluation result is not a credential, so it is never -holder-bound — binding applies only to an issued SD-JWT VC. Credentials are signed with +holder-bound: binding applies only to an issued SD-JWT VC. Credentials are signed with EdDSA/Ed25519 by default, with ES256/P-256 also available per profile. **Federation only talks to peers you configured.** There's no automatic trust discovery. Peers are loaded from configuration at startup, anything else is rejected, and a federated request is checked for identity, signature, freshness, purpose, and audience before any source is read. -What crosses a federation boundary is a scoped answer — never a credential. +What crosses a federation boundary is a scoped answer, never a credential. -## What's recommended — and worth confirming +## What's recommended, and worth confirming Two things are strong recommendations rather than hard guarantees, so they're worth a moment in a review: -- **Shared security primitives.** Authentication, audit, cryptography, and the credential +- Shared security primitives: Authentication, audit, cryptography, and the credential helpers are meant to come from one shared component (Registry Platform) so they behave the same everywhere and can be reviewed in one place. The specs recommend this rather than force - it — so confirm a given service actually does it rather than reimplementing its own. -- **HTTP hardening.** Security headers and limits on outbound traffic are recommended posture. + it, so confirm a given service actually does it rather than reimplementing its own. +- HTTP hardening: Security headers and limits on outbound traffic are recommended posture. Check that your build applies them rather than assuming it. ## Where the guarantees stop @@ -101,15 +101,15 @@ A trustworthy posture is as honest about its edges as its strengths. The full in [Known limitations and non-guarantees](../known-limitations/); the three that matter most for a trust review are: -- **Your deployment owns the operational security.** Key custody and rotation, audit retention, +- Your deployment owns the operational security: Key custody and rotation, audit retention, tenant isolation, TLS, rate limiting, and incident response are yours, not the stack's. A - deployment can pass every health and conformance check and still be running on demo keys — so + deployment can pass every health and conformance check and still be running on demo keys, so passing checks is not the same as production-grade key custody. -- **Aligning with a standard is not conforming to it.** Registry Stack speaks several standards' - shapes — SD-JWT VC, OID4VCI, CCCEV, W3C DID, ODRL — but conformance to a Registry Stack spec +- Aligning with a standard is not conforming to it: Registry Stack speaks several standards' + shapes (SD-JWT VC, OID4VCI, CCCEV, W3C DID, ODRL) but conformance to a Registry Stack spec does not imply certified conformance to any of them. The OID4VCI and CCCEV surfaces in particular are deliberately partial subsets. -- **A published catalog grants nothing.** Listing a dataset or policy in a discovery artifact +- A published catalog grants nothing: Listing a dataset or policy in a discovery artifact describes intent; it doesn't grant access or prove a record exists. Only an authorized runtime read decides that. @@ -120,19 +120,19 @@ Sigstore cosign, and tag-built releases also publish SLSA provenance. You can ve `cosign verify-blob` and `slsa-verifier`; the steps are in `SECURITY.md` and `release/VERIFY.md` in the repository. (Git tags themselves and container images aren't signed yet.) -Everything above is defined normatively in the specifications, which are the place to go when +Everything on this page is defined normatively in the specifications, which are the place to go when you want to check a claim rather than take it on trust: -- [RS-SEC-G](../../spec/rs-sec-g/) — the security model (authentication, authorization, audit, +- [RS-SEC-G](../../spec/rs-sec-g/): the security model (authentication, authorization, audit, key boundary, the operator boundary) -- [RS-PR-NOTARY](../../spec/rs-pr-notary/) — disclosure modes, credentials, and federation -- [RS-PR-RELAY](../../spec/rs-pr-relay/) — the protected read API -- [RS-DM-CLAIM](../../spec/rs-dm-claim/) — how a claim and its disclosure policy are defined -- [RS-ARC-G](../../spec/rs-arc-g/) — the component boundaries these rest on +- [RS-PR-NOTARY](../../spec/rs-pr-notary/): disclosure modes, credentials, and federation +- [RS-PR-RELAY](../../spec/rs-pr-relay/): the protected read API +- [RS-DM-CLAIM](../../spec/rs-dm-claim/): how a claim and its disclosure policy are defined +- [RS-ARC-G](../../spec/rs-arc-g/): the component boundaries these rest on ## Related -- [Records stay home](../records-stay-home/) — what stays inside the institution and what crosses out -- [Threat model](../threat-model/) — the boundaries, assets, and threats behind this posture -- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) — the privacy view -- [Known limitations and non-guarantees](../known-limitations/) — the full list of edges +- [Records stay home](../records-stay-home/): what stays inside the institution and what crosses out +- [Threat model](../threat-model/): the boundaries, assets, and threats behind this posture +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/): the privacy view +- [Known limitations and non-guarantees](../known-limitations/): the full list of edges From 3d2a1b18a80ad8c71a279a092df0ad3bf2068582 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 11:33:06 +0700 Subject: [PATCH 12/25] docs: link Notary audit scopes gap to tracking issue #178 Signed-off-by: Jeremi Joslin --- docs/site/src/content/docs/explanation/known-limitations.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index eeaf643f..365b80b8 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -99,7 +99,7 @@ A healthy, reachable, internally consistent deployment is not the same as a prod - Health checks do not certify key custody: Readiness, liveness, and protocol-conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and key-provider approval remain operator responsibilities. - Many guarantees are the operator's to provide: Secret and key provisioning, key custody and rotation schedule, audit retention and storage, tenant isolation, transport termination, edge rate limiting, deployment configuration, and incident response are operator responsibilities, not behaviors the stack guarantees. - Audit fail-closed is a capability, not a guarantee about every build: A deployment can run audit fail-closed, so that a request whose audit record cannot be written does not return success. That is a switch a deployment is able to turn on, not a promise that every route in a given build has been individually audited, nor that every build ships with it on. -- The Notary audit record omits the scopes field: The security model says an audit record captures at least the caller principal, the scopes exercised, a request identifier, and the purpose. Notary's audit record body records the principal, request id, and purpose, but does not include the caller's exercised scopes, so a downstream review of Notary audit alone cannot reconstruct which scopes authorized a request. (Relay's audit record does record scopes.) +- The Notary audit record omits the scopes field: The security model says an audit record captures at least the caller principal, the scopes exercised, a request identifier, and the purpose. Notary's audit record body records the principal, request id, and purpose, but does not include the caller's exercised scopes, so a downstream review of Notary audit alone cannot reconstruct which scopes authorized a request. (Relay's audit record does record scopes.) Tracked in [issue #178](https://github.com/registrystack/registry-stack/issues/178). - The source-adapter sidecar is an internal connector, not a public API: The source adapter sidecar is an internal connector surface, not a public subject-facing API. Its static bearer token is not by itself a tenant or subject authorization boundary, and it relies on deployment-network egress controls for outbound source traffic. Direct public access, broad reuse of one sidecar token across unrelated sources, or shared cache or backoff state across authorization contexts is outside the security model. - Demo and template code is not a production profile: Demo helper code and generated workflow snippets are integration examples, not a production freshness or replay-protection profile, and must not be relied on for freshness, expiry, or replay protection. From eee526cc0be73438ac4a34e0c901866107467a53 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 11:50:50 +0700 Subject: [PATCH 13/25] docs: record that Notary audit now captures exercised scopes The implementation gap tracked in #178 is fixed on main (EvidenceAuditEvent.scopes_used), so the known-limitations entry and the two per-page caveats come out. Signed-off-by: Jeremi Joslin --- .../data-minimization-and-purpose-limitation.mdx | 2 +- .../site/src/content/docs/explanation/known-limitations.mdx | 1 - .../site/src/content/docs/explanation/records-stay-home.mdx | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index fff614a9..04eb0268 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -58,7 +58,7 @@ One related boundary is positive rather than cautionary. The portable metadata t ## Audit as an accountability primitive -Accountability is supported through audit, which is treated as a security control rather than an optional log. Every request that returns person-level records or claim results must be recorded with at least the caller principal, a request identifier, and the purpose value where one was supplied, and a deployment can run audit fail-closed so that a request whose audit record cannot be written does not succeed. The security model expects the scopes exercised to be recorded too; in practice Relay's audit record captures them, but Notary's audit record does not yet include that field, a gap noted in the limitations inventory. +Accountability is supported through audit, which is treated as a security control rather than an optional log. Every request that returns person-level records or claim results must be recorded with at least the caller principal, the scopes exercised, a request identifier, and the purpose value where one was supplied, and a deployment can run audit fail-closed so that a request whose audit record cannot be written does not succeed. One precise reading matters here. Audit fail-closed is a capability a deployment can turn on, not a guarantee that every route in any given build has been individually audited against it. Whether a particular deployment meets it is something a reviewer verifies in that deployment, not something the design asserts on its behalf. diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 365b80b8..eeca3373 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -99,7 +99,6 @@ A healthy, reachable, internally consistent deployment is not the same as a prod - Health checks do not certify key custody: Readiness, liveness, and protocol-conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and key-provider approval remain operator responsibilities. - Many guarantees are the operator's to provide: Secret and key provisioning, key custody and rotation schedule, audit retention and storage, tenant isolation, transport termination, edge rate limiting, deployment configuration, and incident response are operator responsibilities, not behaviors the stack guarantees. - Audit fail-closed is a capability, not a guarantee about every build: A deployment can run audit fail-closed, so that a request whose audit record cannot be written does not return success. That is a switch a deployment is able to turn on, not a promise that every route in a given build has been individually audited, nor that every build ships with it on. -- The Notary audit record omits the scopes field: The security model says an audit record captures at least the caller principal, the scopes exercised, a request identifier, and the purpose. Notary's audit record body records the principal, request id, and purpose, but does not include the caller's exercised scopes, so a downstream review of Notary audit alone cannot reconstruct which scopes authorized a request. (Relay's audit record does record scopes.) Tracked in [issue #178](https://github.com/registrystack/registry-stack/issues/178). - The source-adapter sidecar is an internal connector, not a public API: The source adapter sidecar is an internal connector surface, not a public subject-facing API. Its static bearer token is not by itself a tenant or subject authorization boundary, and it relies on deployment-network egress controls for outbound source traffic. Direct public access, broad reuse of one sidecar token across unrelated sources, or shared cache or backoff state across authorization contexts is outside the security model. - Demo and template code is not a production profile: Demo helper code and generated workflow snippets are integration examples, not a production freshness or replay-protection profile, and must not be relied on for freshness, expiry, or replay protection. diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 5507d50d..2e18a93a 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -154,9 +154,9 @@ Security material: a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. - Every person-level request is audited: An audit record captures at least the caller, - a request id, and the declared purpose where one was supplied; Relay additionally records - the scopes exercised (Notary's record does not). A deployment can run audit fail-closed, so - a request whose audit record cannot be written does not return success. + the scopes exercised, a request id, and the declared purpose where one was supplied. + A deployment can run audit fail-closed, so a request whose audit record cannot be + written does not return success. ## What this guarantees, and what it does not From 8d900aa20960a8ad0a2c3708c4aa2e1b4597dfb0 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 12:04:12 +0700 Subject: [PATCH 14/25] docs: build the Security section Security overview absorbs the trust-posture page (its at-a-glance table and framing over the REQ-anchored draft), new hardening checklist and vulnerability-reporting how-tos, and the self-assessment and OpenSSF evidence pages move under security/. All new pages carry status: draft pending Tier-C maintainer sign-off. Signed-off-by: Jeremi Joslin --- .../trust-posture-and-security-guarantees.mdx | 138 -------------- .../docs/security/hardening-checklist.mdx | 170 ++++++++++++++++++ docs/site/src/content/docs/security/index.mdx | 150 ++++++++++++++++ .../openssf-evidence.mdx | 0 .../docs/security/report-a-vulnerability.mdx | 87 +++++++++ .../self-assessment.mdx} | 8 +- 6 files changed, 411 insertions(+), 142 deletions(-) delete mode 100644 docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx create mode 100644 docs/site/src/content/docs/security/hardening-checklist.mdx create mode 100644 docs/site/src/content/docs/security/index.mdx rename docs/site/src/content/docs/{reference => security}/openssf-evidence.mdx (100%) create mode 100644 docs/site/src/content/docs/security/report-a-vulnerability.mdx rename docs/site/src/content/docs/{reference/security-self-assessment.mdx => security/self-assessment.mdx} (94%) diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx deleted file mode 100644 index 9287ba60..00000000 --- a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Trust posture and security guarantees -description: A plain account of what Registry Stack protects for you, what it recommends you confirm, and what it deliberately leaves to your deployment. -status: draft -owner: registry-docs -source_repos: - - registry-stack -last_reviewed: "2026-06-28" -doc_type: explanation -locale: en -standards_referenced: - - sd-jwt-vc - - oid4vci - - cccev - - w3c-did - - odrl ---- - -Before you adopt Registry Stack you want a straight answer to three questions: what does it -guarantee about security and privacy, what does it leave for you to get right, and what does -it deliberately *not* do? This page answers all three in plain terms. It is not a substitute -for the specifications (at the end you'll find links to the exact documents where each -property is defined) but it is the honest summary you can read first. - -## At a glance - -| What | Where it stands | -|------|-----------------| -| Authentication on every route that touches a record or a claim | **Built in** | -| Deny-by-default authorization, checked before any data is read | **Built in** | -| Only the public half of a signing key is ever published | **Built in** | -| Person-level access is written to an audit log | **Built in** (fail-closed is yours to switch on) | -| Three disclosure modes, with `redacted` revealing neither value nor answer | **Built in** | -| Selective-disclosure credentials, with holder binding configurable per profile (off by default) | **Built in** | -| Federation limited to peers you configure | **Built in** | -| Hardened HTTP headers and outbound-traffic limits | **Recommended**: confirm your build applies them | -| Key custody, retention, isolation, TLS, rate limiting | **Your deployment's job** | -| Compliance with any external standard it cites | **Not claimed** | - -## What's built in - -**Nothing answers without authenticating first.** Every route that returns a record or a -claim result checks the caller's credential before it does anything else. A service runs one -authentication mode at a time: either API-key fingerprints (it stores a hash, never the raw -key, and compares in constant time) or OIDC tokens (it checks the signature, issuer, audience, -and algorithm). The routes open to an anonymous caller are the operational and discovery -surfaces (health and readiness probes, the public verification keys, credential-issuer -discovery and the OID4VCI wallet-flow endpoints, the docs, public credential-status lookups, -and credential type metadata) while everything that returns a record or claim result still -requires a credential. - -**Authorization is deny-by-default, and it happens before any data is touched.** A caller has -to hold the scope a route requires, and that check runs *before* Relay reads a source or -Notary evaluates a claim, so a request can never quietly widen its own reach at the last -moment. - -**Access leaves a trail.** Every request that returns person-level data is recorded with who -asked, a request id, and the declared purpose. (Relay's audit record also captures the scopes -exercised; Notary's audit record does not yet include that field. See the limitations hub.) You -can run audit fail-closed, so that if the audit record can't be written the request doesn't -succeed: that's a switch you turn on for a deployment that needs it, not something that's -always on by default. - -**An answer is shaped, not a copy of the record.** Every claim is answered in one of three -modes: `value` hands back the computed value, `predicate` returns only a yes/no, and `redacted` -returns neither the value nor the yes/no. The claim's policy fixes which modes are allowed; -within that set the caller can request one, and otherwise the default applies. [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) -covers this in depth. - -**Credentials are selective, and can be holder-bound.** When Notary issues a credential it's an -SD-JWT VC: the signed body carries hashes of each disclosable field rather than the values, so -the holder reveals only what they choose. Holder binding is configurable per credential profile -and defaults to off: when a profile enables it (using `did:jwk`), the credential is tied to the -holder's own key and can't be presented by anyone else. The self-attestation (wallet) issuance -path requires binding with proof-of-possession, so credentials issued there are always bound. -Anyone can verify a credential against the issuer's published keys without needing a credential -of their own. The plain claim or evaluation result is not a credential, so it is never -holder-bound: binding applies only to an issued SD-JWT VC. Credentials are signed with -EdDSA/Ed25519 by default, with ES256/P-256 also available per profile. - -**Federation only talks to peers you configured.** There's no automatic trust discovery. Peers -are loaded from configuration at startup, anything else is rejected, and a federated request is -checked for identity, signature, freshness, purpose, and audience before any source is read. -What crosses a federation boundary is a scoped answer, never a credential. - -## What's recommended, and worth confirming - -Two things are strong recommendations rather than hard guarantees, so they're worth a moment in -a review: - -- Shared security primitives: Authentication, audit, cryptography, and the credential - helpers are meant to come from one shared component (Registry Platform) so they behave the - same everywhere and can be reviewed in one place. The specs recommend this rather than force - it, so confirm a given service actually does it rather than reimplementing its own. -- HTTP hardening: Security headers and limits on outbound traffic are recommended posture. - Check that your build applies them rather than assuming it. - -## Where the guarantees stop - -A trustworthy posture is as honest about its edges as its strengths. The full inventory lives in -[Known limitations and non-guarantees](../known-limitations/); the three that matter most for a -trust review are: - -- Your deployment owns the operational security: Key custody and rotation, audit retention, - tenant isolation, TLS, rate limiting, and incident response are yours, not the stack's. A - deployment can pass every health and conformance check and still be running on demo keys, so - passing checks is not the same as production-grade key custody. -- Aligning with a standard is not conforming to it: Registry Stack speaks several standards' - shapes (SD-JWT VC, OID4VCI, CCCEV, W3C DID, ODRL) but conformance to a Registry Stack spec - does not imply certified conformance to any of them. The OID4VCI and CCCEV surfaces in - particular are deliberately partial subsets. -- A published catalog grants nothing: Listing a dataset or policy in a discovery artifact - describes intent; it doesn't grant access or prove a record exists. Only an authorized runtime - read decides that. - -## Verifying a release, and reading the source - -For checking what you're actually running, GitHub release assets are signed with keyless -Sigstore cosign, and tag-built releases also publish SLSA provenance. You can verify both with -`cosign verify-blob` and `slsa-verifier`; the steps are in `SECURITY.md` and `release/VERIFY.md` -in the repository. (Git tags themselves and container images aren't signed yet.) - -Everything on this page is defined normatively in the specifications, which are the place to go when -you want to check a claim rather than take it on trust: - -- [RS-SEC-G](../../spec/rs-sec-g/): the security model (authentication, authorization, audit, - key boundary, the operator boundary) -- [RS-PR-NOTARY](../../spec/rs-pr-notary/): disclosure modes, credentials, and federation -- [RS-PR-RELAY](../../spec/rs-pr-relay/): the protected read API -- [RS-DM-CLAIM](../../spec/rs-dm-claim/): how a claim and its disclosure policy are defined -- [RS-ARC-G](../../spec/rs-arc-g/): the component boundaries these rest on - -## Related - -- [Records stay home](../records-stay-home/): what stays inside the institution and what crosses out -- [Threat model](../threat-model/): the boundaries, assets, and threats behind this posture -- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/): the privacy view -- [Known limitations and non-guarantees](../known-limitations/): the full list of edges diff --git a/docs/site/src/content/docs/security/hardening-checklist.mdx b/docs/site/src/content/docs/security/hardening-checklist.mdx new file mode 100644 index 00000000..3af2e759 --- /dev/null +++ b/docs/site/src/content/docs/security/hardening-checklist.mdx @@ -0,0 +1,170 @@ +--- +title: Harden a production deployment +description: An actionable checklist for the operator responsibilities RS-SEC-G Section 9 leaves to your deployment, grouped by key custody, audit, transport, deployment profile, and incident response. +status: draft +owner: registry-docs +source_repos: + - registry-relay + - registry-notary + - registry-platform +last_reviewed: "2026-07-02" +doc_type: how-to +locale: en +standards_referenced: [] +--- + +Registry Relay and Registry Notary enforce authentication, authorization, key publication, and +audit as software controls (see [Security overview](../)). +[RS-SEC-G](../../spec/rs-sec-g/) Section 9 draws a line at the deployment: secret and key +provisioning, key custody and rotation, audit retention and storage, tenant isolation, transport +termination and certificate management, edge rate limiting, deployment configuration, and incident +response stay with the operator. +This page turns that boundary into a checklist you work through before you call a deployment +production-ready. + +## When to use this + +Use this checklist before a `production` or `evidence_grade` deployment goes live, and again after +any change to signing keys, audit sinks, network placement, or the declared deployment profile. It +assumes you have already completed the [Registry Relay configuration reference](../../products/registry-relay/configuration/) +or the [Registry Notary operator configuration reference](../../products/registry-notary/operator-config-reference/) +for your service. + +## Before you start + +- Decide the deployment profile (`local`, `hosted_lab`, `production`, or `evidence_grade`) you are + claiming; see the deployment profile section of the + [Registry Relay configuration reference](../../products/registry-relay/configuration/). The + profile is never inferred from hostname or network position; you declare it. +- Have `registry-notary doctor` (Notary) or your Relay startup logs available to check gate + findings as you work through each item. + +## Keys and custody + +- Configure signing keys through the provider abstraction (`local_jwk_env`, `file_watch`, or + `pkcs11`) under `evidence.signing_keys`, not as inline YAML values. Startup fails closed if an + active signing provider cannot be constructed. See the + [signing key provider reference](../../products/registry-notary/signing-key-provider/). +- Provision secrets (API-key fingerprints, source tokens, private JWKs, the audit hash secret) only + through the environment variables named in config. For local development the binary accepts + `--env-file`; for shared environments use the platform secret store and do not check dotenv files + into the repository. +- Rotate a signing key by adding a new `kid`, moving credential profiles to it, then moving the old + key to `publish_only` until its configured publication window ends or verifiers no longer need + it. Do not delete a key verifiers may still need. +- Keep RS256 scoped to the eSignet RP client assertion key. Credential profile, access token, and + federation response signing use Ed25519 EdDSA or ES256/P-256; RS256 is rejected there. +- If a signing key uses `provider: pkcs11`, confirm the deployed binary was built with the + `pkcs11` capability (`registry-notary build-info`) and validate the module, token, and key lookup + with `registry-notary doctor --config `. +- Relay: confirm every env-backed `fingerprint.name` referenced in config exists in the runtime + environment, and that no raw key, fingerprint, private JWK, or full environment dump reaches a + log line; see the production checklist in the + [Registry Relay configuration reference](../../products/registry-relay/configuration/). + +## Audit sink and retention + +- Configure a durable audit sink (`file` or `syslog`, not bare `stdout`) before you declare + `production` or `evidence_grade`. A missing durable sink trips `relay.audit.sink_missing` / + `notary.audit.sink_missing`, which fails startup at `evidence_grade` (`startup_fail` for both + services, and already `startup_fail` at `production` for Notary). +- Set `audit.hash_secret_env` to an environment variable holding at least 32 bytes of + deployment-specific random secret material. Relay startup fails closed if it is missing, empty, + unset, or weak. +- Keep `audit.write_policy: fail_closed` (the default) unless an explicit availability exception + accepts best-effort audit durability under `availability_first`. `fail_closed` means a request + whose audit record cannot be written returns `503 audit.write_failed` instead of a success + (REQ-SEC-G-009). +- If you rotate the audit hash secret, retain the old secret under your audit retention controls + for any period during which older records must remain comparable to new ones, or accept that new + records will not match old audit handles. +- Hash-chained envelopes (`prev_hash`, `record_hash`) detect ordering gaps and accidental + corruption, but they do not protect against an actor who can rewrite the audit sink itself. Use + an append-only external sink or independent tail-hash anchoring when you need stronger integrity + than the envelope alone provides. + +## Transport and edge + +- Terminate TLS and manage certificates at your reverse proxy or load balancer; RS-SEC-G Section 9 + leaves transport termination and certificate management to the operator. +- Keep the admin listener private. A publicly exposed admin surface trips + `relay.admin.public_exposure` or `notary.admin.shared_exposure`, both `startup_fail` at + `evidence_grade`. +- Set explicit `cors.allowed_origins`. The default CORS policy is deny by omission; add only the + origins your integration actually needs. +- Enable `server.trust_proxy` with an explicit `trusted_proxies` list only when a reverse proxy + sits in front of Relay. Both `enabled` and `trusted_proxies` default to off/empty. +- Enforce ingress rate limiting at your gateway or edge, then either declare + `deployment.evidence.ingress_rate_limit: true` or accept the `relay.ingress.rate_limit_missing` + finding it otherwise raises. The flag defaults to `false`. +- Leave `allow_insecure_localhost` and `allow_insecure_private_network` false on Notary source + connections in production unless a deployment review explicitly accepts the private-network + source; a plain `http://` source with neither allowance trips `notary.source.insecure_url`, and + enabling the escape hatch trips `notary.source.private_network_escape`. +- For Postgres sources, require `sslmode=require` on the connection string and use read-only + database credentials; for live materialization, scope that role to `SELECT` only the configured + table or view. +- Apply the Registry Platform HTTP-security response headers and outbound-HTTP-policy primitive on + source-connector egress; both are RS-SEC-G recommendations (REQ-SEC-G-012), not an enforced + default, so confirm your build turns them on. + +## Deployment profile and posture endpoint + +- Declare `deployment.profile` explicitly. An omitted profile binds no gates and only raises a + `deployment.profile_undeclared` warning, so a silent gap stays silent until you declare a + profile. +- Treat `evidence_grade` as requiring a signed, governed config bundle: running it from a plain + local YAML file trips `relay.config.unsigned` / `notary.config.unsigned` and the process refuses + to start. +- Review `GET /admin/v1/posture` regularly. It reports the declared profile, active findings, and + active waivers, so the deployment's actual state is inspectable rather than asserted. +- Give every waiver a non-empty, non-secret reason and a mandatory expiry date. `startup_fail` + gates are never waivable; a waiver naming one is rejected. Waiver reasons appear only in the + restricted posture tier. +- Use `deployment.evidence.*` flags (for example `ingress_rate_limit`, `api_key_rotation`) only to + assert controls that live outside the process and cannot be observed by it. Each flag defaults to + `false`. +- If a Notary instance runs active-active with peers, declare `multi_instance` and use Redis-backed + replay storage. In-memory replay under a multi-instance or federated declaration trips + `notary.replay.in_memory_high_risk`. + +## Incident response + +- Know your private disclosure channel before you need it: see + [Report a vulnerability](../report-a-vulnerability/). +- If credential-status is enabled, use the admin status endpoint (requires the + `registry:notary:admin` scope) to mark a compromised credential `revoked`. Registry Notary + defines no other revocation flow; a credential issued with credential-status disabled cannot be + revoked through the API. See [Known limitations and non-guarantees](../../explanation/known-limitations/). +- Rotate a compromised signing key using the documented rotation procedure (new `kid`, old key to + `publish_only`) rather than deleting it outright, so previously issued, still-valid credentials + remain verifiable through the rotation. +- Preserve the audit trail for post-incident review: hash-chained, fail-closed audit records are + the record a deployment reconstructs a request timeline from (REQ-SEC-G-008, REQ-SEC-G-009). + +## Verify + +- Start the service against your production config and confirm it starts (or fails closed as + expected for a deliberately unmet `startup_fail` gate). +- Query `GET /admin/v1/posture` and read the `deployment` object: confirm the profile matches what + you declared, and that no unwaived finding at or above your profile's threshold is outstanding. +- Send a request that should be audited and confirm a corresponding record lands in your configured + sink, not only `stdout`. +- Attempt an admin-listener request from outside the private network it is meant to be bound to, + and confirm it is refused. + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| Process refuses to start after declaring `production` or `evidence_grade` | A `startup_fail` gate tripped (for example missing audit sink or unsigned config) | Check the posture or startup log for the finding id, then fix the underlying condition; `startup_fail` gates cannot be waived | +| Posture reports `deployment.profile_undeclared` | No `deployment.profile` set | Declare a profile explicitly; an omitted profile is not the same as `local` | +| Posture reports `deployment.waiver_expired` | A waiver's `expires` date passed | Re-review the finding and either fix it or issue a new waiver with a new expiry | + +## Next + +- [Security overview](../) +- [Report a vulnerability](../report-a-vulnerability/) +- [Registry Relay configuration reference](../../products/registry-relay/configuration/) +- [Registry Notary operator configuration reference](../../products/registry-notary/operator-config-reference/) +- [Known limitations and non-guarantees](../../explanation/known-limitations/) diff --git a/docs/site/src/content/docs/security/index.mdx b/docs/site/src/content/docs/security/index.mdx new file mode 100644 index 00000000..d1858ed0 --- /dev/null +++ b/docs/site/src/content/docs/security/index.mdx @@ -0,0 +1,150 @@ +--- +title: Security overview +description: "The enforced security model of Registry Stack, the responsibilities that stay with the operator, and the public evidence a security reviewer can check." +status: draft +owner: registry-docs +source_repos: + - registry-relay + - registry-notary + - registry-platform +last_reviewed: "2026-07-02" +doc_type: explanation +locale: en +standards_referenced: [] +--- + +Use this page to assess the security posture of Registry Stack before you deploy it or integrate +with it. +It summarizes the model the software enforces, names the responsibilities that stay with the +operator, and links the public evidence a reviewer can verify. +The normative source for everything on this page is the security model specification +[RS-SEC-G](../spec/rs-sec-g/); this page is its narrative summary. + +## At a glance + +| What | Where it stands | +|------|-----------------| +| Authentication on every route that touches a record or a claim | Built in | +| Deny-by-default authorization, checked before any data is read | Built in | +| Only the public half of a signing key is ever published | Built in | +| Person-level access is written to an audit log | Built in (fail-closed is yours to switch on) | +| Three disclosure modes, with `redacted` revealing neither value nor answer | Built in | +| Selective-disclosure credentials, with holder binding configurable per profile (off by default) | Built in | +| Federation limited to peers you configure | Built in | +| Hardened HTTP headers and outbound-traffic limits | Recommended, confirm your build applies them | +| Key custody, retention, isolation, TLS, rate limiting | Your deployment's job | +| Compliance with any external standard it cites | Not claimed | + +## The model in one pass + +Every request that returns person-level records or claim results moves through the same pipeline: +authenticate, authorize, serve, audit. + +- Authenticate: a runtime service runs exactly one authentication mode, fixed by + configuration: static credentials or OIDC (REQ-SEC-G-002). Static credentials are configured as + SHA-256 fingerprints, never raw secrets, and compared in constant time (REQ-SEC-G-003). OIDC + tokens are trusted only after signature, issuer, audience, and algorithm checks (REQ-SEC-G-004). +- Authorize: authorization is scope-based and deny-by-default. The required scope is enforced + before any source is read, and a caller's reach is never widened at request time + (REQ-SEC-G-005, REQ-SEC-G-006). +- Serve: the service returns the configured surface: a protected read, a claim result, a + selected value, a signed credential, or a denial. Every claim is answered in one of three + disclosure modes: `value` hands back the computed value, `predicate` returns only a yes/no, and + `redacted` returns neither the value nor the yes/no; the claim's policy fixes which modes are + allowed. [Disclosure modes and computed answers](../explanation/disclosure-modes-and-computed-answers/) + covers this in depth. +- Audit: the request is recorded in a hash-chained audit envelope capturing the caller + principal, the scopes exercised, a request identifier, and the declared purpose + (REQ-SEC-G-008). A deployment can run + audit fail-closed, so a request whose audit record cannot be written does not succeed + (REQ-SEC-G-009). + +Only liveness and readiness probes and public verification-key discovery are served without +authentication. +Issuers sign with asymmetric keys and publish only the public half, so any verifier can check an +issued credential without holding a credential of its own (REQ-SEC-G-007). + +When Notary issues a credential it is an SD-JWT VC: the signed body carries hashes of each +disclosable field rather than the values, so the holder reveals only what they choose. +Holder binding is configurable per credential profile and defaults to off; the self-attestation +(wallet) issuance path requires it. +See [Evidence issuance, end to end](../explanation/evidence-issuance/) for the full credential +lifecycle. + +Delegated evaluation between services is admitted only from statically configured peers, and the +peer's identity, signature, freshness, and purpose are verified before any source read +(REQ-SEC-G-010). + +## What's recommended, not enforced + +Two behaviors are recommended rather than required, so confirm them rather than assume them. + +- Registry Platform sourcing: security-critical primitives (authentication, OIDC verification, + audit envelopes, HTTP security, outbound HTTP policy, cryptography, and SD-JWT VC helpers) are + meant to come from Registry Platform rather than be reimplemented per service, so they behave + the same everywhere and can be reviewed in one place. RS-SEC-G recommends this rather than + forcing it (REQ-SEC-G-001), so confirm a given service actually does it rather than + reimplementing its own. +- HTTP hardening: security response headers and limits on outbound source-fetch traffic are + recommended posture (REQ-SEC-G-012). Check that your build applies them rather than assuming + it. + +## Declared deployment assurance + +A deployment states the assurance level it claims; the software then enforces gates that match the +claim. +Both runtime services accept a `deployment` block with a profile of `local`, `hosted_lab`, +`production`, or `evidence_grade`. +Each profile binds a findings catalog at escalating severities: a `production` deployment with no +durable audit sink refuses to start, and an `evidence_grade` deployment refuses to start from an +unsigned local config file. +Findings, waivers (each with a mandatory reason and expiry), and the declared profile are reported +by the operations posture endpoint, so the deployment's actual state is inspectable rather than +asserted. +The gate catalogs are documented in the +[Registry Relay configuration reference](../products/registry-relay/configuration/) and the +[Registry Notary operator configuration reference](../products/registry-notary/operator-config-reference/). + +## What stays with the operator + +The security model ends where the deployment begins. +Secret and key provisioning, key custody and rotation schedules, audit retention and storage, +tenant isolation, transport termination and certificate management, edge rate limiting, deployment +configuration, and incident response are operator responsibilities (RS-SEC-G Section 9). +The software keeps secrets out of distributable artifacts (REQ-SEC-G-013) and provides the +primitives; the operator provisions, configures, and operates them. +[Harden a production deployment](hardening-checklist/) turns this boundary into an actionable +checklist. + +## Evidence for reviewers + +Public, checkable evidence, from most to least formal: + +- [RS-SEC-G](../spec/rs-sec-g/) is the normative security model; [RS-PR-RELAY](../spec/rs-pr-relay/) + and [RS-PR-NOTARY](../spec/rs-pr-notary/) carry its wire-level form per service. +- [Security self-assessment](self-assessment/) is a CNCF TAG Security-style maintainer + self-assessment, including known gaps. +- [OpenSSF and release trust](openssf-evidence/) records which release-integrity checks are + verifiable today (signatures, provenance, Scorecard) and which are still incomplete. +- The [standards register](../reference/standards/) records the adoption mode and evidence for each + cited standard. +- [DPI safeguards alignment](../explanation/dpi-safeguards-alignment/) maps the stack to safeguards + language for review programs. + +These documents state known gaps alongside implemented controls. +None of them is a certification, and the hosted lab is a synthetic-data demo, not a production +assurance claim. + +## Report a vulnerability + +Suspected vulnerabilities go through the private disclosure process, never public issues or pull +requests. +See [Report a vulnerability](report-a-vulnerability/). + +## Next + +- [Harden a production deployment](hardening-checklist/) +- [Threat model](../explanation/threat-model/): the boundaries, assets, and threats behind this posture +- [Known limitations and non-guarantees](../explanation/known-limitations/): the full list of edges +- [RS-SEC-G: Registry family security model](../spec/rs-sec-g/) +- [Security self-assessment](self-assessment/) diff --git a/docs/site/src/content/docs/reference/openssf-evidence.mdx b/docs/site/src/content/docs/security/openssf-evidence.mdx similarity index 100% rename from docs/site/src/content/docs/reference/openssf-evidence.mdx rename to docs/site/src/content/docs/security/openssf-evidence.mdx diff --git a/docs/site/src/content/docs/security/report-a-vulnerability.mdx b/docs/site/src/content/docs/security/report-a-vulnerability.mdx new file mode 100644 index 00000000..3ab43c6f --- /dev/null +++ b/docs/site/src/content/docs/security/report-a-vulnerability.mdx @@ -0,0 +1,87 @@ +--- +title: Report a vulnerability +description: How to report a suspected Registry Stack vulnerability privately, what is in scope, and how to verify a release you downloaded. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-07-02" +doc_type: how-to +locale: en +standards_referenced: [] +--- + +Report a suspected Registry Stack vulnerability privately. Never open a public issue or pull +request for a suspected credential disclosure, auth bypass, audit redaction failure, connector +data leakage, or signing-key handling bug. + +## When to use this + +Use the private disclosure channel for anything in scope below. For a known, documented pilot +limitation that does not create an exploitable issue beyond what is already documented, file a +regular product-gap issue instead; see [Known limitations and non-guarantees](../../explanation/known-limitations/). + +## Before you start + +Gather what the report needs before you open the channel: + +- The affected commit or release tag. +- The config shape involved (redact secret values before you paste anything). +- Reproduction steps. +- The impact you observed or expect. + +Do not include live credentials, bearer tokens, API keys, private keys, or raw registry records in +the report. + +## Steps + +1. Report privately through GitHub Security Advisories at + `https://github.com/registrystack/registry-stack/security/advisories/new`. +2. If GitHub Security Advisories is unavailable, contact the maintainer through an existing + private project channel instead of opening a public issue or pull request. +3. Include the affected commit, config shape, reproduction steps, and impact from the checklist + above. + +Registry Stack aims to acknowledge private reports within 5 business days. + +## What is in scope + +Authentication bypass, credential disclosure, audit redaction failure, audit integrity failure, +signing-key handling bugs, source connector data leakage, and privacy regressions that expose raw +subject identifiers. + +Known pilot limitations, such as no revocation service, no `/.well-known/jwt-vc-issuer` endpoint, +and no built-in data-subject erasure workflow, are product gaps, not vulnerabilities, unless they +create an exploitable security or privacy issue beyond the documented limitation. The source +adapter sidecar also relies on deployment-network egress controls for outbound source traffic; see +the sidecar's README at `crates/registry-notary-source-adapter-sidecar/README.md` in the +repository. + +## Verify a release signature + +For checking what you actually downloaded: Registry Stack release assets are signed by the release +workflow with keyless Sigstore cosign, and tag-triggered releases also publish SLSA provenance. +For each signed release asset, download the asset, its `.sig` signature, and its `.pem` +certificate from the GitHub Release, then verify: + +```bash +asset=registryctl-v0.8.3-linux-amd64 + +cosign verify-blob \ + --certificate "${asset}.pem" \ + --signature "${asset}.sig" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github.com/registrystack/registry-stack/.github/workflows/release.yml@refs/tags/v[0-9]+[.][0-9]+[.][0-9]+.*$' \ + "${asset}" +``` + +If a release asset has no matching `.sig` and `.pem` files, treat it as unsigned; the `v0.8.0` +prerelease predates release-asset signing. Git version tags are not yet cryptographically signed. +The full verification steps, including SLSA provenance, are in `SECURITY.md` and +`release/VERIFY.md` in the repository. + +## Next + +- [Security overview](../) +- [Harden a production deployment](../hardening-checklist/) +- [OpenSSF and release trust](../openssf-evidence/) diff --git a/docs/site/src/content/docs/reference/security-self-assessment.mdx b/docs/site/src/content/docs/security/self-assessment.mdx similarity index 94% rename from docs/site/src/content/docs/reference/security-self-assessment.mdx rename to docs/site/src/content/docs/security/self-assessment.mdx index 7cba3737..f333827e 100644 --- a/docs/site/src/content/docs/reference/security-self-assessment.mdx +++ b/docs/site/src/content/docs/security/self-assessment.mdx @@ -76,7 +76,7 @@ Operator responsibilities include: ## Project compliance -Registry Stack documents standards alignment in [Standards](../standards/) and +Registry Stack documents standards alignment in [Standards](../../reference/standards/) and the public spec register in [Specifications](../../spec/). These pages describe implementation evidence and known gaps; they do not claim legal compliance by themselves. @@ -122,10 +122,10 @@ No public review ledger yet. Primary public evidence: - [RS-SEC-G](../../spec/rs-sec-g/) -- [Contracts](../contracts/) +- [Contracts](../../reference/contracts/) - [OpenSSF evidence](../openssf-evidence/) -- [Registry Notary API reference](../apis/registry-notary/) -- [Registry Relay API reference](../apis/registry-relay/) +- [Registry Notary API reference](../../reference/apis/registry-notary/) +- [Registry Relay API reference](../../reference/apis/registry-relay/) - GitHub Release assets, with generated release capsules after the first capsule-producing release run - [Release verification](https://github.com/registrystack/registry-stack/blob/main/release/VERIFY.md) From 11b29dd6411db1d752e1e18136f5ac495e3944c8 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 12:04:12 +0700 Subject: [PATCH 15/25] docs: split see-it-live into Quickstart and credential tour The zero-install Relay reads become the five-minute Quickstart; the Notary issuance flow becomes its own credential tour, the first place a new reader sees a privacy-preserving credential. Signed-off-by: Jeremi Joslin --- .../content/docs/start/credential-tour.mdx | 114 ++++++++++ .../src/content/docs/start/quickstart.mdx | 150 +++++++++++++ .../src/content/docs/start/see-it-live.mdx | 212 ------------------ 3 files changed, 264 insertions(+), 212 deletions(-) create mode 100644 docs/site/src/content/docs/start/credential-tour.mdx create mode 100644 docs/site/src/content/docs/start/quickstart.mdx delete mode 100644 docs/site/src/content/docs/start/see-it-live.mdx diff --git a/docs/site/src/content/docs/start/credential-tour.mdx b/docs/site/src/content/docs/start/credential-tour.mdx new file mode 100644 index 00000000..b728e4f5 --- /dev/null +++ b/docs/site/src/content/docs/start/credential-tour.mdx @@ -0,0 +1,114 @@ +--- +title: Get a credential from the hosted lab +description: In the hosted lab, watch Registry Notary issue a signed, privacy-preserving credential to a hosted demo wallet, with zero install. +status: current +owner: registry-docs +source_repos: + - registry-lab + - registry-notary +last_reviewed: "2026-06-28" +doc_type: tutorial +locale: en +standards_referenced: + - oid4vci + - sd-jwt-vc +--- + +import QuickstartMeta from '../../../components/QuickstartMeta.astro'; + +In the hosted lab, **Registry Notary** issues a signed credential to a hosted demo wallet that +answers a question without exposing the underlying record. This is a guided browser flow, not a +curl sequence: a real [OID4VCI](../../reference/glossary/#oid4vci) issuance needs a holder key and +a sign-in, which is wallet territory. You do not need to install a wallet: the lab hosts a demo +wallet at [wallet.lab.registrystack.org](https://wallet.lab.registrystack.org), so you can receive +the credential end to end in the browser. Everything runs in the hosted lab at +[lab.registrystack.org](https://lab.registrystack.org), so this page has no setup on your machine. + + + +This lab uses synthetic data and public demo-only credentials by design. +Do not reuse anything you copy here outside the lab. + +## What is running + +The lab runs a citizen **Notary** you will use in this tutorial: an issuer that hands out a +privacy-preserving credential instead of the raw record, at +[citizen-notary.lab.registrystack.org](https://citizen-notary.lab.registrystack.org). The lab also +runs a health-program Notary in front of a demo DHIS2 health information system, used only by the +DHIS2 integration tutorial, at +[dhis2-notary.lab.registrystack.org](https://dhis2-notary.lab.registrystack.org). For how these +connect to the rest of the stack, see the +[architecture overview](../../explanation/architecture/). For the full set of live demo +credentials, identities, and ready-made requests, open the lab homepage at +[lab.registrystack.org](https://lab.registrystack.org). + +If the hosted lab or one of its service URLs is unavailable, run the same multi-service topology +locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/). + +## Get a signed credential + +The citizen Notary issues a Selective Disclosure JWT Verifiable Credential +([SD-JWT VC](../../reference/glossary/#sd-jwt-vc)) of type `person_is_alive_sd_jwt`, media type +`application/dc+sd-jwt`. The credential discloses a single predicate, whether +the person is alive, as a true or false claim. It never hands over the underlying civil record. +The [quickstart](../quickstart/) Relay row-read example uses `NID-1001` / Miguel Santos, while this +wallet flow uses the lab wallet identity `NID-2001` / Maria Santos. + +Start in the **Wallet test** section of [lab.registrystack.org](https://lab.registrystack.org), +which links the credential flow end to end: start the citizen Notary flow, sign in, then paste the +generated credential offer into the hosted wallet. The Notary requires you to sign in before it +issues. The sign-in runs through eSignet, the lab's hosted demo identity provider; you do not need +an account, and the values below are synthetic demo identities: + +- National ID: `NID-2001` +- Name: Maria Santos +- Login and OTP code: `111111` +- PIN: `545411` + +After sign-in, the hosted wallet receives the signed `person_is_alive_sd_jwt` credential, with the +alive predicate set to true. If you prefer your own OID4VCI-compatible wallet, you can open the +credential offer directly instead: + +```text +https://citizen-notary.lab.registrystack.org/oid4vci/credential-offer?credential_configuration_id=person_is_alive_sd_jwt +``` + +### The negative control + +Try this in the same browser flow. Self-attestation binds the credential subject to the identity you +signed in as. If you sign in as `NID-2001` but ask for a credential bound to `NID-1001`, the Notary +refuses: it will not issue a credential for a subject you did not authenticate as. The issuer cannot +be talked into vouching for someone else. For more on this service, see +[Registry Notary](../../products/registry-notary/). + +### DHIS2 integration path + +The lab also runs a DHIS2-backed Notary for API credential and claim-evaluation work. +Because that path is about integrating an existing source system, it lives in its own tutorial: +[Configure DHIS2 claim checks](../../tutorials/configure-dhis2-claim-checks/). + +## What you built + +You watched Registry Notary issue a signed, privacy-preserving Selective Disclosure JWT Verifiable +Credential to a hosted demo wallet: the credential discloses a single true or false predicate, +whether the person is alive, without exposing the underlying civil record. Self-attestation held: signing +in as `NID-2001` and asking for a credential bound to `NID-1001` was refused, because the issuer will +not vouch for a subject you did not authenticate as. + +## Next + +- [Evaluate a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): run a Notary + against the Relay you published and evaluate a claim. +- [Run a protected registry API locally](../../tutorials/publish-spreadsheet-secured-registry-api/): + stand up your own protected Relay from a sample workbook. + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| The hosted lab or a service URL does not respond | n/a | Run the same multi-service topology locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/) | diff --git a/docs/site/src/content/docs/start/quickstart.mdx b/docs/site/src/content/docs/start/quickstart.mdx new file mode 100644 index 00000000..c38c4e92 --- /dev/null +++ b/docs/site/src/content/docs/start/quickstart.mdx @@ -0,0 +1,150 @@ +--- +title: Quickstart +description: With zero install, read a protected registry API from your terminal against the public hosted lab and watch Registry Relay enforce scope. +status: current +owner: registry-docs +source_repos: + - registry-lab + - registry-relay +last_reviewed: "2026-06-28" +doc_type: tutorial +locale: en +standards_referenced: [] +--- + +import QuickstartMeta from '../../../components/QuickstartMeta.astro'; + +In about five minutes you can read a protected registry API from your terminal, with zero install, +against a public hosted lab. You will call **Registry Relay**, which returns records to authorized +callers, and watch it enforce scope: the same API answers some requests and refuses others, +depending on what the caller's token allows. Everything runs in the hosted lab at +[lab.registrystack.org](https://lab.registrystack.org), so this page has no setup on your machine. + + + +This lab uses synthetic data and public demo-only credentials by design. +Do not reuse anything you copy here outside the lab. + +## What is running + +The lab runs a civil-registry **Relay** you will call directly in this tutorial: a protected, +scoped, read-only HTTP API over a civil registry source, at +[civil-relay.lab.registrystack.org](https://civil-relay.lab.registrystack.org). The lab also runs +**Registry Notary** services for the next step in this journey; see +[Get a credential from the hosted lab](../credential-tour/). For how these connect to the rest of +the stack, see the [architecture overview](../../explanation/architecture/). For the full set of +live demo credentials, identities, and ready-made requests, open the lab homepage at +[lab.registrystack.org](https://lab.registrystack.org). + +:::note +Lab tokens are public demo-only values that rotate, so this page shows placeholders, never live +tokens. Get the current tokens and identities from the **For developers** section near the bottom +of the lab homepage, which shows every demo credential with a copy button and a prebuilt curl +example. The same data is available +unauthenticated as JSON at +[lab.registrystack.org/api/lab.json](https://lab.registrystack.org/api/lab.json). +::: + +## Get your bearer tokens + +The civil Relay authenticates callers with `Authorization: Bearer `. Each token carries a +scope, so the same API answers some requests and refuses others. + +Open [lab.registrystack.org](https://lab.registrystack.org), scroll to the **For developers** +section near the bottom, and copy two bearer tokens from the demo credential cards: a +metadata-scope token and a row-reader token. Set them in your shell. The placeholders below stand +in for the live tokens. + +```sh +export CIVIL_METADATA_TOKEN="" +export CIVIL_ROW_READER_TOKEN="" +``` + +## List datasets (metadata scope) + +The metadata-scope token can list the datasets the Relay publishes: + +```sh +curl -sS \ + -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ + https://civil-relay.lab.registrystack.org/v1/datasets +``` + +The Relay returns `200 OK` with the catalog. Each dataset entry carries more fields than shown; +they are omitted here for brevity: + +```json +{ "data": [ { "dataset_id": "civil_registry" } ] } +``` + +## Read rows (row scope plus a purpose) + +Reading actual records needs a token with the row scope and a `Data-Purpose` header that declares why +you are reading: + +```sh +curl -sS \ + -H "Authorization: Bearer $CIVIL_ROW_READER_TOKEN" \ + -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ + "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" +``` + +The Relay returns `200 OK` with one record and pagination metadata (synthetic demo data): + +```json +{ + "data": [ + { + "national_id": "NID-1001", + "given_name": "Miguel", + "surname": "Santos", + "birth_date": "2016-01-15", + "life_stage": "child", + "deceased": false, + "district": "north" + } + ], + "pagination": { "has_more": true, "next_cursor": "..." } +} +``` + +## See access control hold + +The point of the gateway is that it never widens reach at request time. Send the metadata-scope token +to the rows endpoint and the Relay refuses it: + +```sh +curl -sS -o /dev/null -w "%{http_code}\n" \ + -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ + -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ + "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" +``` + +The Relay returns `403 Forbidden` with `{"code":"auth.scope_denied"}`. Same API, same network call, +different scope, different answer. For more on this service, see +[Registry Relay](../../products/registry-relay/). + +## What you built + +You read a protected registry API from your terminal and watched Registry Relay enforce scope: a +metadata-scope token listed the dataset catalog (`200`), a row-scope token with a declared purpose +read one record (`200`), and the same metadata-scope token was refused at the rows endpoint (`403`, +`auth.scope_denied`). Same API, same network call, different scope, different answer. + +## Next + +- [Get a credential from the hosted lab](../credential-tour/): continue in the same hosted lab and + see Registry Notary issue a privacy-preserving credential. +- [Run a protected registry API locally](../../tutorials/publish-spreadsheet-secured-registry-api/): + stand up your own protected Relay from a sample workbook. + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| The hosted lab or a service URL does not respond | n/a | Run the same multi-service topology locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/) | diff --git a/docs/site/src/content/docs/start/see-it-live.mdx b/docs/site/src/content/docs/start/see-it-live.mdx deleted file mode 100644 index 0a7819c3..00000000 --- a/docs/site/src/content/docs/start/see-it-live.mdx +++ /dev/null @@ -1,212 +0,0 @@ ---- -title: See it live -description: With zero install, read a protected registry API from your terminal and receive a privacy-preserving credential in a hosted demo wallet, all against the public hosted lab. -status: current -owner: registry-docs -source_repos: - - registry-lab - - registry-registryctl -last_reviewed: "2026-06-28" -doc_type: tutorial -locale: en -standards_referenced: - - oid4vci - - sd-jwt-vc ---- - -import QuickstartMeta from '../../../components/QuickstartMeta.astro'; - -In about ten minutes you can see the core of Registry Stack working against a public hosted lab. You -will read a protected registry API from your terminal (**Registry Relay**, which returns records to -authorized callers), then have **Registry Notary** issue a signed credential, delivered to a hosted -demo wallet, that answers a question without exposing the record. The Relay reads are pure `curl` -with zero install; the credential step is a guided browser flow using the lab's hosted wallet, so it -needs no install either. Everything runs in the hosted lab at -[lab.registrystack.org](https://lab.registrystack.org), so the main flow has no setup on your -machine. - - - -This lab uses synthetic data and public demo-only credentials by design. -Do not reuse anything you copy here outside the lab. - -## What is running - -The lab runs three services that you will touch directly: - -- A civil-registry **Relay**: a protected, scoped, read-only HTTP API over a civil registry source, - at [civil-relay.lab.registrystack.org](https://civil-relay.lab.registrystack.org). -- A citizen **Notary**: an issuer that hands out a privacy-preserving credential instead of the raw - record, at [citizen-notary.lab.registrystack.org](https://citizen-notary.lab.registrystack.org). -- A health-program **Notary** in front of a demo DHIS2 health information system, used only by the - DHIS2 integration tutorial, at - [dhis2-notary.lab.registrystack.org](https://dhis2-notary.lab.registrystack.org). - -For how these connect to the rest of the stack, see the -[architecture overview](../../explanation/architecture/). For the full set of live demo -credentials, identities, and ready-made requests, open the lab homepage at -[lab.registrystack.org](https://lab.registrystack.org). - -:::note -Lab tokens are public demo-only values that rotate, so this page shows placeholders, never live -tokens. Get the current tokens and identities from the **For developers** section near the bottom -of the lab homepage, which shows every demo credential with a copy button and a prebuilt curl -example. The same data is available -unauthenticated as JSON at -[lab.registrystack.org/api/lab.json](https://lab.registrystack.org/api/lab.json). -::: - -If the hosted lab or one of its service URLs is unavailable, run the same multi-service topology -locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/). - -## Read a protected registry API - -This is the part you can run right now from a terminal. The civil Relay authenticates callers with -`Authorization: Bearer `. Each token carries a scope, so the same API answers some requests -and refuses others. - -Open [lab.registrystack.org](https://lab.registrystack.org), scroll to the **For developers** -section near the bottom, and copy two bearer tokens from the demo credential cards: a -metadata-scope token and a row-reader token. Set them in your shell. The placeholders below stand -in for the live tokens. - -```sh -export CIVIL_METADATA_TOKEN="" -export CIVIL_ROW_READER_TOKEN="" -``` - -### List datasets (metadata scope) - -The metadata-scope token can list the datasets the Relay publishes: - -```sh -curl -sS \ - -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ - https://civil-relay.lab.registrystack.org/v1/datasets -``` - -The Relay returns `200 OK` with the catalog. Each dataset entry carries more fields than shown; -they are omitted here for brevity: - -```json -{ "data": [ { "dataset_id": "civil_registry" } ] } -``` - -### Read rows (row scope plus a purpose) - -Reading actual records needs a token with the row scope and a `Data-Purpose` header that declares why -you are reading: - -```sh -curl -sS \ - -H "Authorization: Bearer $CIVIL_ROW_READER_TOKEN" \ - -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ - "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" -``` - -The Relay returns `200 OK` with one record and pagination metadata (synthetic demo data): - -```json -{ - "data": [ - { - "national_id": "NID-1001", - "given_name": "Miguel", - "surname": "Santos", - "birth_date": "2016-01-15", - "life_stage": "child", - "deceased": false, - "district": "north" - } - ], - "pagination": { "has_more": true, "next_cursor": "..." } -} -``` - -### See access control hold - -The point of the gateway is that it never widens reach at request time. Send the metadata-scope token -to the rows endpoint and the Relay refuses it: - -```sh -curl -sS -o /dev/null -w "%{http_code}\n" \ - -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ - -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ - "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" -``` - -The Relay returns `403 Forbidden` with `{"code":"auth.scope_denied"}`. Same API, same network call, -different scope, different answer. For more on this service, see -[Registry Relay](../../products/registry-relay/). - -## Get a signed credential - -Now see the issuance side. The citizen Notary issues a Selective Disclosure JWT Verifiable -Credential ([SD-JWT VC](../../reference/glossary/#sd-jwt-vc)) of type `person_is_alive_sd_jwt`, -media type `application/dc+sd-jwt`. The credential discloses a single predicate, whether -the person is alive, as a true or false claim. It never hands over the underlying civil record. -The Relay row-read example uses `NID-1001` / Miguel Santos, while this wallet flow uses the lab -wallet identity `NID-2001` / Maria Santos. - -Unlike the Relay reads above, this part is a guided browser flow, not a curl sequence: a real -[OID4VCI](../../reference/glossary/#oid4vci) issuance needs a holder key and a sign-in, which is wallet -territory. You do not need to install a wallet: the lab hosts a demo wallet at -[wallet.lab.registrystack.org](https://wallet.lab.registrystack.org), so you can receive the -credential end to end in the browser. - -Start in the **Wallet test** section of [lab.registrystack.org](https://lab.registrystack.org), -which links the credential flow end to end: start the citizen Notary flow, sign in, then paste the -generated credential offer into the hosted wallet. The Notary requires you to sign in before it -issues. The sign-in runs through eSignet, the lab's hosted demo identity provider; you do not need -an account, and the values below are synthetic demo identities: - -- National ID: `NID-2001` -- Name: Maria Santos -- Login and OTP code: `111111` -- PIN: `545411` - -After sign-in, the hosted wallet receives the signed `person_is_alive_sd_jwt` credential, with the -alive predicate set to true. If you prefer your own OID4VCI-compatible wallet, you can open the -credential offer directly instead: - -```text -https://citizen-notary.lab.registrystack.org/oid4vci/credential-offer?credential_configuration_id=person_is_alive_sd_jwt -``` - -### The negative control - -Try this in the same browser flow. Self-attestation binds the credential subject to the identity you -signed in as. If you sign in as `NID-2001` but ask for a credential bound to `NID-1001`, the Notary -refuses: it will not issue a credential for a subject you did not authenticate as. The issuer cannot -be talked into vouching for someone else. For more on this service, see -[Registry Notary](../../products/registry-notary/). - -### DHIS2 integration path - -The lab also runs a DHIS2-backed Notary for API credential and claim-evaluation work. -Because that path is about integrating an existing source system, it lives in its own tutorial: -[Configure DHIS2 claim checks](../../tutorials/configure-dhis2-claim-checks/). - -## Now run your own - -You have seen the two payoffs against the hosted lab. To run the same shapes on your own machine, -start with the local single-node tutorials: - -- [Run a protected registry API locally](../../tutorials/publish-spreadsheet-secured-registry-api/): - stand up your own protected Relay from a sample workbook. -- [Evaluate a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): run a Notary - against the Relay you published and evaluate a claim. -- [Connect Notary to a Registry Data API source](../../tutorials/run-notary-standalone-for-api/): - run Notary in a separate project against a Registry Data API-shaped source. -- [Configure DHIS2 claim checks](../../tutorials/configure-dhis2-claim-checks/): connect Registry - Notary to a DHIS2 source adapter and inspect the fields to change for your own DHIS2 deployment. - -Only assessing fit? You do not need to install anything: -[When to use Registry Stack](../when-to-use/) covers fit and non-goals, and -[DPI safeguards alignment](../../explanation/dpi-safeguards-alignment/) maps the stack to -safeguards language for review programs. From 074b7f8ea23aed5938259bb4a79067ad3f5f621d Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 12:04:12 +0700 Subject: [PATCH 16/25] docs: rework homepage into fast path, two products, security strip Signed-off-by: Jeremi Joslin --- docs/site/src/content/docs/index.mdx | 49 ++++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/site/src/content/docs/index.mdx b/docs/site/src/content/docs/index.mdx index 0e95fc1c..5c445e29 100644 --- a/docs/site/src/content/docs/index.mdx +++ b/docs/site/src/content/docs/index.mdx @@ -12,27 +12,40 @@ locale: en standards_referenced: [] --- -import HomeLanding from '../../components/HomeLanding.astro'; +Registry Stack adds registry-facing services, protected reads, governed evidence, and audit +records, over data an institution already holds. +The data never leaves the source system. -Registry Stack is for building registry-facing services over data an institution already holds. It -adds controlled read access, governed evidence responses, and audit records without turning the -registry into a shared database. +Start with the [quickstart](start/quickstart/): no install required, about five minutes. +Then take the [credential tour](start/credential-tour/). -Registry Stack has two runtime patterns: +## Two products -- **Protected Registry APIs** are scoped, read-only HTTP APIs over existing files, extracts, - databases, or legacy registry systems. Registry Relay implements this surface. -- **Evidence Gateway** is the governed evidence path. A runtime service checks request context, - source context, supported policy terms, freshness, redaction, and audit provenance before it - returns a claim result, credential, redacted response, protected read, or denial. Registry Notary - implements claim evaluation and credential issuance; governed Registry Relay routes use the same - Policy Decision Point (PDP) pattern for protected reads. +Registry Relay protects what an institution already holds. Registry Notary certifies answers drawn +from that data. -Use these docs to build and operate both patterns. The product pages are pulled from each repository -at build time, so they match the code. For audience, problem, and ecosystem context, see -[registrystack.org](https://registrystack.org/). +### Registry Relay -Start with [See it live](start/see-it-live/): a hosted lab, zero install, one protected read and one -signed credential in under two minutes. +Registry Relay exposes protected, scoped, read-only APIs over existing sources: files, extracts, +databases, and legacy registry systems. - +[Your first Relay](tutorials/publish-spreadsheet-secured-registry-api/): run a protected registry +API locally over a sample spreadsheet. + +### Registry Notary + +Registry Notary certifies evidence: claim evaluation, credential issuance, disclosure policy, and +audit provenance. It is the flagship of Registry Stack. + +[Your first Notary](tutorials/verify-claim-registry-api/): evaluate a claim and compare the result +with a protected row read. + +## Security + +Registry Stack authorization is scope-based and deny-by-default. +Every request writes to a hash-chained audit envelope. +These docs are evidence-anchored: every claim about product behavior points to code, a fixture, a +test, or a specification requirement ID. + +Read the [security overview](security/) for the threat model and the public evidence a reviewer can +check. From 56eef75c989e598b27f527b4588525b97d75ce4f Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 12:09:01 +0700 Subject: [PATCH 17/25] docs: restructure navigation to the target information architecture Get started becomes the first-run journey in reading order; the generated Relay, Notary, and Manifest groups lift to top-level product sections; Integrations and Concepts replace the old group names; the Security rail gathers the overview, threat model, limitations hub, hardening, reporting, and evidence pages. Redirects cover the moved and split pages, the stale quickstart-to-home redirect is gone, and remaining cross-links point at the new slugs. The orphaned HomeLanding component is removed. Signed-off-by: Jeremi Joslin --- docs/site/astro.config.mjs | 91 ++++++---- docs/site/src/components/HomeLanding.astro | 162 ------------------ ...ta-minimization-and-purpose-limitation.mdx | 2 +- .../disclosure-modes-and-computed-answers.mdx | 2 +- .../docs/explanation/evidence-issuance.mdx | 2 +- .../src/content/docs/start/when-to-use.mdx | 2 +- .../tutorials/first-run-with-registry-lab.mdx | 2 +- ...blish-spreadsheet-secured-registry-api.mdx | 3 +- .../tutorials/verify-claim-registry-api.mdx | 2 +- 9 files changed, 64 insertions(+), 204 deletions(-) delete mode 100644 docs/site/src/components/HomeLanding.astro diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index 2eef7b4b..091062b7 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -41,6 +41,16 @@ const base = process.env.DOCS_BASE || undefined; const basePath = base?.replace(/\/$/, ''); const isArchivedBuild = Boolean(basePath); const productSidebar = loadProductSidebar(); + +// Lift a generated per-product group to the top level of the sidebar. +// Fails the build loudly if the generator's labels change, so the nav can +// never silently lose a product section. +/** @param {string} label */ +function generatedProduct(label) { + const group = productSidebar.find((/** @type {{ label: string }} */ entry) => entry.label === label); + if (!group) throw new Error(`generated sidebar group "${label}" not found`); + return group; +} const disabledSitemap = { name: '@astrojs/sitemap', hooks: {}, @@ -64,8 +74,11 @@ export default defineConfig({ // to their new homes so old links and search results keep resolving. redirects: { '/start/': internalRedirect('/'), + '/start/see-it-live/': internalRedirect('/start/quickstart/'), + '/explanation/trust-posture-and-security-guarantees/': internalRedirect('/security/'), + '/reference/security-self-assessment/': internalRedirect('/security/self-assessment/'), + '/reference/openssf-evidence/': internalRedirect('/security/openssf-evidence/'), // quickstart's "Choose by question" router merged into the homepage (2026-06). - '/start/quickstart/': internalRedirect('/'), '/start/your-first-call/': internalRedirect('/tutorials/first-run-with-registry-lab/'), // verify-claim-own-api moved into the Apply to your stack path (2026-06). '/tutorials/verify-claim-own-api/': internalRedirect('/tutorials/run-notary-standalone-for-api/'), @@ -109,7 +122,7 @@ export default defineConfig({ '/products/registry-notary/opencrvs-dci-onboarding/': 'https://docs.registrystack.org/products/registry-notary/opencrvs-onboarding/', // registry-manifest, registry-atlas, registry-platform, registry-lab projects/* // redirects removed: targets are deferred from the MVP docs cut. - '/projects/registry-lab/demo-flow/': internalRedirect('/start/see-it-live/'), + '/projects/registry-lab/demo-flow/': internalRedirect('/start/quickstart/'), // The API reference moved from static Redoc HTML to native, theme-aware, // searchable pages. Keep the old shareable links working. '/api/registry-relay.html': internalRedirect('/reference/apis/relay/'), @@ -193,20 +206,19 @@ export default defineConfig({ }, ], // Diataxis IA: Get started, Tutorials, Products, Explanation, Reference. - // The per-product groups inside Products are generated from - // src/data/repo-docs.yaml by scripts/generate-sidebar.mjs (the - // productSidebar array), so the menu follows each product's - // doc_type/nav_order and never drifts from the manifest. Within a - // product, pages are sub-grouped by Diataxis type once the product grows - // past a threshold; smaller products stay flat. Product labels drop the - // shared "Registry" prefix (Relay, Notary, ...) since the site title and - // the Products group already supply that context. + // The per-product groups are generated from src/data/repo-docs.yaml by + // scripts/generate-sidebar.mjs (the productSidebar array), so each + // product menu follows its doc_type/nav_order and never drifts from the + // manifest. Within a product, pages are sub-grouped by Diataxis type + // once the product grows past a threshold; smaller products stay flat. + // generatedProduct() lifts each group into its own top-level product + // section; hand-authored operator tutorials append after the generated + // items. // - // "Get started" is orientation only: Overview (which carries the - // "Choose by question" router), the zero-install demo, and the - // evaluation page. The core Tutorials path stays on one generated local - // project; operator paths live under Apply to your stack, and named - // source-system paths live under Integrate existing systems. + // "Get started" is the first-run journey in reading order: the hosted + // quickstart and credential tour, then the two local first-run + // tutorials, then the evaluation and lab pages. Named source-system + // paths live under Integrations. sidebar: [ { label: 'Get started', @@ -214,27 +226,37 @@ export default defineConfig({ // Short nav labels to avoid wrapping in the narrow sidebar; page // titles keep the full wording. { label: 'Overview', link: '/' }, - { label: 'See it live', slug: 'start/see-it-live' }, + { label: 'Quickstart', slug: 'start/quickstart' }, + { label: 'Credential tour', slug: 'start/credential-tour' }, + { label: 'Your first Relay', slug: 'tutorials/publish-spreadsheet-secured-registry-api' }, + { label: 'Your first Notary', slug: 'tutorials/verify-claim-registry-api' }, { label: 'When to use', slug: 'start/when-to-use' }, + { label: 'Run the lab', slug: 'tutorials/first-run-with-registry-lab' }, ], }, { - label: 'Tutorials', + label: 'Registry Relay', + collapsed: true, items: [ - { label: 'Run a protected API', slug: 'tutorials/publish-spreadsheet-secured-registry-api' }, - { label: 'Evaluate a claim', slug: 'tutorials/verify-claim-registry-api' }, + ...generatedProduct('Relay').items, + { label: 'Deploy with own data', slug: 'tutorials/deploy-standalone-with-own-data' }, ], }, { - label: 'Apply to your stack', + label: 'Registry Notary', + collapsed: true, items: [ + ...generatedProduct('Notary').items, { label: 'Notary for a Registry Data API', slug: 'tutorials/run-notary-standalone-for-api' }, - { label: 'Deploy with own data', slug: 'tutorials/deploy-standalone-with-own-data' }, - { label: 'Run the lab', slug: 'tutorials/first-run-with-registry-lab' }, ], }, { - label: 'Integrate existing systems', + label: 'Registry Manifest', + collapsed: true, + items: generatedProduct('Manifest').items, + }, + { + label: 'Integrations', items: [ { label: 'OpenCRVS claims', slug: 'tutorials/verify-opencrvs-claims' }, { label: 'DHIS2 claim checks', slug: 'tutorials/configure-dhis2-claim-checks' }, @@ -242,11 +264,7 @@ export default defineConfig({ ], }, { - label: 'Products', - items: productSidebar, - }, - { - label: 'Explanation', + label: 'Concepts', collapsed: true, items: [ { label: 'Architecture', slug: 'explanation/architecture' }, @@ -255,21 +273,26 @@ export default defineConfig({ { label: 'Consultation flow', slug: 'explanation/consultation-flow' }, { label: 'Evidence issuance', slug: 'explanation/evidence-issuance' }, { label: 'Disclosure modes', slug: 'explanation/disclosure-modes-and-computed-answers' }, + { label: 'Data minimization', slug: 'explanation/data-minimization-and-purpose-limitation' }, { label: 'Trusted context', slug: 'explanation/trusted-context-constraints' }, { label: 'Integration patterns', slug: 'explanation/integration-patterns' }, { label: 'DPI safeguards', slug: 'explanation/dpi-safeguards-alignment' }, ], }, { - // Trust & Security rail (roadmap §6): the reviewer/auditor-facing - // posture, threat model, privacy story, and the canonical limits hub. - label: 'Trust & security', + // The reviewer/auditor-facing rail: the enforced model, the threat + // model, the canonical limits hub, and the public evidence a + // security reviewer can check. + label: 'Security', collapsed: true, items: [ - { label: 'Trust posture', slug: 'explanation/trust-posture-and-security-guarantees' }, + { label: 'Overview', slug: 'security' }, { label: 'Threat model', slug: 'explanation/threat-model' }, - { label: 'Data minimization', slug: 'explanation/data-minimization-and-purpose-limitation' }, { label: 'Known limitations', slug: 'explanation/known-limitations' }, + { label: 'Harden a deployment', slug: 'security/hardening-checklist' }, + { label: 'Report a vulnerability', slug: 'security/report-a-vulnerability' }, + { label: 'Self-assessment', slug: 'security/self-assessment' }, + { label: 'OpenSSF evidence', slug: 'security/openssf-evidence' }, ], }, { @@ -292,8 +315,6 @@ export default defineConfig({ { label: 'Environment variables', slug: 'reference/environment-variables' }, { label: 'Contracts', slug: 'reference/contracts' }, { label: 'Standards', slug: 'reference/standards' }, - { label: 'OpenSSF evidence', slug: 'reference/openssf-evidence' }, - { label: 'Security self-assessment', slug: 'reference/security-self-assessment' }, { label: 'ITB and SEMIC evidence', slug: 'reference/itb-semic-evidence' }, { label: 'Glossary', slug: 'reference/glossary' }, ], diff --git a/docs/site/src/components/HomeLanding.astro b/docs/site/src/components/HomeLanding.astro deleted file mode 100644 index 3719c488..00000000 --- a/docs/site/src/components/HomeLanding.astro +++ /dev/null @@ -1,162 +0,0 @@ ---- -// The homepage section index. Renders the "Choose by question" router as a -// stack of register cards plus a primary-action row, reusing the shared card -// classes (.card-header, .card-meta, .card-footer) so it inherits the -// civic-print look and dark mode automatically. Editorial routing data lives -// here (like DocsList.astro's descriptions map), so index.mdx stays prose. -const base = import.meta.env.BASE_URL === '/' ? '' : import.meta.env.BASE_URL.replace(/\/$/, ''); - -function withBase(path: string) { - return path.startsWith('/') ? `${base}${path}` : path; -} - -interface Cta { - label: string; - href: string; -} - -interface RouteCard { - id: string; - question: string; - start: Cta; - then: Cta[]; -} - -const primary: Cta = { label: 'See it live', href: '/start/see-it-live/' }; -const secondary: Cta[] = [ - { label: 'When to use', href: '/start/when-to-use/' }, - { label: 'API reference', href: '/reference/apis/' }, -]; - -const cards: RouteCard[] = [ - { - id: 'see-it-work', - question: 'I want to see it work right now.', - start: { label: 'See it live', href: '/start/see-it-live/' }, - then: [{ label: 'When to use Registry Stack', href: '/start/when-to-use/' }], - }, - { - id: 'what-and-when', - question: 'What is Registry Stack, and when should I use it?', - start: { label: 'When to use Registry Stack', href: '/start/when-to-use/' }, - then: [{ label: 'Architecture overview', href: '/explanation/architecture/' }], - }, - { - id: 'why-and-who', - question: 'Why does this matter, and who benefits?', - start: { label: 'Why (registrystack.org)', href: 'https://registrystack.org/why/' }, - then: [{ label: 'Use cases (registrystack.org)', href: 'https://registrystack.org/use-cases/' }], - }, - { - id: 'try-locally', - question: 'How do I try it on my own machine?', - start: { - label: 'Run a protected registry API locally', - href: '/tutorials/publish-spreadsheet-secured-registry-api/', - }, - then: [{ label: 'Evaluate a claim with Registry Notary', href: '/tutorials/verify-claim-registry-api/' }], - }, - { - id: 'full-demo', - question: 'How do I run the full multi-service demo locally?', - start: { label: 'First run with Registry Lab', href: '/tutorials/first-run-with-registry-lab/' }, - then: [{ label: 'Architecture overview', href: '/explanation/architecture/' }], - }, - { - id: 'fhir', - question: 'How do I integrate FHIR-shaped health data?', - start: { label: 'Getting started with FHIR evidence', href: '/tutorials/getting-started-fhir-evidence/' }, - then: [{ label: 'Integration patterns', href: '/explanation/integration-patterns/' }], - }, - { - id: 'dhis2', - question: 'How do I configure claim checks from DHIS2?', - start: { label: 'Configure DHIS2 claim checks', href: '/tutorials/configure-dhis2-claim-checks/' }, - then: [{ label: 'Integration patterns', href: '/explanation/integration-patterns/' }], - }, - { - id: 'own-data', - question: 'How do I deploy Relay against my own data, not a sample?', - start: { - label: 'Deploy Relay and Notary standalone with your own data', - href: '/tutorials/deploy-standalone-with-own-data/', - }, - then: [{ label: 'Relay configuration', href: '/products/registry-relay/configuration/' }], - }, - { - id: 'existing-api', - question: 'I already have a Registry Data API source. How do I evaluate claims from it?', - start: { - label: 'Connect Notary to a Registry Data API source', - href: '/tutorials/run-notary-standalone-for-api/', - }, - then: [{ label: 'Registry Notary', href: '/products/registry-notary/' }], - }, - { - id: 'opencrvs', - question: 'How do I integrate OpenCRVS claim checks?', - start: { label: 'Verify OpenCRVS claims with registryctl', href: '/tutorials/verify-opencrvs-claims/' }, - then: [ - { - label: 'OpenCRVS onboarding model', - href: 'https://docs.registrystack.org/products/registry-notary/opencrvs-onboarding/', - }, - ], - }, - { - id: 'make-usable', - question: 'How do existing files, extracts, databases, or legacy systems become usable?', - start: { label: 'Protected Registry APIs with Registry Relay', href: '/products/registry-relay/' }, - then: [{ label: 'Relay API reference', href: '/reference/apis/registry-relay/' }], - }, - { - id: 'verify-without-record', - question: 'How can a service verify a status or claim without receiving the full record?', - start: { label: 'Evidence Gateway with Registry Notary', href: '/products/registry-notary/' }, - then: [{ label: 'Evidence issuance', href: '/explanation/evidence-issuance/' }], - }, - { - id: 'how-it-fits', - question: 'How do the pieces fit together?', - start: { label: 'Architecture overview', href: '/explanation/architecture/' }, - then: [ - { label: 'Registry Relay', href: '/products/registry-relay/' }, - { label: 'Registry Notary', href: '/products/registry-notary/' }, - ], - }, -]; ---- - -
- {primary.label} - - {secondary.map((action) => ( - {action.label} - ))} - -
- -

Choose by question

- -
- {cards.map((card) => ( -
-
-

{card.question}

-
-
-
Start with
-
{card.start.label}
-
Then read
-
- {card.then.map((link, index) => ( - <> - {link.label} - {index < card.then.length - 1 ? : null} - - ))} -
-
-
- ))} -
diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 04eb0268..23d42b3c 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -87,7 +87,7 @@ More broadly, the architecture defines primitives and leaves a large set of data - [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/): how disclosure modes are selected and policy-bound - [Threat model](../threat-model/): the boundaries, assets, and threats behind this posture -- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/): the high-level security summary +- [Security overview](../../security/): the high-level security summary - [Known limitations and non-guarantees](../known-limitations/): the full inventory of edges - [Records stay home](../records-stay-home/): what stays inside the institution's boundary and what crosses it - The security and protocol specifications: [RS-SEC-G](../../spec/rs-sec-g/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index b7b9d6fd..4c00b700 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -84,7 +84,7 @@ A few boundaries matter when you evaluate this design: ## Related -- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/): the full posture this fits into +- [Security overview](../../security/): the full posture this fits into - [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/): the privacy view - [Known limitations and non-guarantees](../known-limitations/): where the edges are - [Records stay home](../records-stay-home/): what stays inside the institution and what crosses out diff --git a/docs/site/src/content/docs/explanation/evidence-issuance.mdx b/docs/site/src/content/docs/explanation/evidence-issuance.mdx index d0ecfe74..004a2c24 100644 --- a/docs/site/src/content/docs/explanation/evidence-issuance.mdx +++ b/docs/site/src/content/docs/explanation/evidence-issuance.mdx @@ -97,7 +97,7 @@ evaluation, or receipt. policy, replay state, purpose, profile, and audience before source reads, then returns a compact signed JWT response. This path returns a scoped evaluation result, not a credential. -The [hosted lab walkthrough](../../start/see-it-live/) citizen self-attestation scenario uses path 2: a +The [credential tour](../../start/credential-tour/) citizen self-attestation scenario uses path 2: a host-side Notary runs against the lab's civil registry, eSignet provides the access token, and a wallet completes the OID4VCI flow. diff --git a/docs/site/src/content/docs/start/when-to-use.mdx b/docs/site/src/content/docs/start/when-to-use.mdx index 521e4427..b5bd4da8 100644 --- a/docs/site/src/content/docs/start/when-to-use.mdx +++ b/docs/site/src/content/docs/start/when-to-use.mdx @@ -79,6 +79,6 @@ Registry Stack deliberately leaves these to other systems: ## Next -- [See it live](../see-it-live/) +- [Quickstart](../quickstart/) - [Choose by question (homepage router)](../../) - [Architecture overview](../../explanation/architecture/) diff --git a/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx b/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx index 643cea40..fa6df922 100644 --- a/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx +++ b/docs/site/src/content/docs/tutorials/first-run-with-registry-lab.mdx @@ -29,7 +29,7 @@ clients into a single topology that runs on a laptop. This tutorial brings the topology up with the `just` recipes the lab provides, verifies it with the smoke suite, runs the narrated demo client, and explains what each of the three scenarios exercises across the Relay and Notary services. If you only want the shortest hosted path before running the -stack locally, start with [See it live](../../start/see-it-live/) instead. This page is the full +stack locally, start with the [Quickstart](../../start/quickstart/) instead. This page is the full local tour. For normal local adopter projects, start with the `registryctl` tutorials instead: diff --git a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx index 6f2b0cd8..5b6f3af3 100644 --- a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx +++ b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx @@ -234,7 +234,8 @@ stayed where it started, and only an authorized, scoped request got an answer ou project and answer a narrow question without exposing the source row. - [Connect Notary to a Registry Data API source](../run-notary-standalone-for-api/): run Notary separately from a Registry Data API-shaped source. -- [See it live](../../start/see-it-live/): explore the hosted demo to see the full stack in action. +- [Credential tour](../../start/credential-tour/): watch the hosted lab issue a privacy-preserving + credential, no install needed. ## Troubleshooting diff --git a/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx b/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx index e4caec71..6ac8e7e3 100644 --- a/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx +++ b/docs/site/src/content/docs/tutorials/verify-claim-registry-api.mdx @@ -289,7 +289,7 @@ It does not delete your workbook, generated configs, local keys, or smoke result inspection are the next local rung. - [Connect Notary to a Registry Data API source](../run-notary-standalone-for-api/): run Notary in a separate project against a Registry Data API-shaped source. -- [See it live](../../start/see-it-live/): explore the hosted demo to see credential issuance and +- [Credential tour](../../start/credential-tour/): explore the hosted demo to see credential issuance and cross-authority flows. ## Troubleshooting From 64402c74472a3fa24896248ac0502c1dffa105c4 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 12:17:56 +0700 Subject: [PATCH 18/25] docs: address reader-persona review findings Glossary link and journey ordering on the homepage, drop the unsupported flagship line, show the 403 body the quickstart prose interprets, replace the credential tour's cross-page anchors with page links and drop its duplicated caveat, gloss holder binding and delegated evaluation on the security overview and eSignet on the hardening checklist, and mark the release-verification example asset as a placeholder with its expected output. Signed-off-by: Jeremi Joslin --- docs/site/src/content/docs/index.mdx | 6 ++++-- .../site/src/content/docs/security/hardening-checklist.mdx | 3 ++- docs/site/src/content/docs/security/index.mdx | 6 ++++-- .../src/content/docs/security/report-a-vulnerability.mdx | 4 +++- docs/site/src/content/docs/start/credential-tour.mdx | 7 ++----- docs/site/src/content/docs/start/quickstart.mdx | 6 +++--- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/site/src/content/docs/index.mdx b/docs/site/src/content/docs/index.mdx index 5c445e29..39b030a1 100644 --- a/docs/site/src/content/docs/index.mdx +++ b/docs/site/src/content/docs/index.mdx @@ -12,7 +12,8 @@ locale: en standards_referenced: [] --- -Registry Stack adds registry-facing services, protected reads, governed evidence, and audit +Registry Stack adds registry-facing services, protected reads, +[governed evidence](reference/glossary/), and audit records, over data an institution already holds. The data never leaves the source system. @@ -23,6 +24,7 @@ Then take the [credential tour](start/credential-tour/). Registry Relay protects what an institution already holds. Registry Notary certifies answers drawn from that data. +Take the quickstart first; the tutorials here are the follow-up, on your own machine. ### Registry Relay @@ -35,7 +37,7 @@ API locally over a sample spreadsheet. ### Registry Notary Registry Notary certifies evidence: claim evaluation, credential issuance, disclosure policy, and -audit provenance. It is the flagship of Registry Stack. +audit provenance. [Your first Notary](tutorials/verify-claim-registry-api/): evaluate a claim and compare the result with a protected row read. diff --git a/docs/site/src/content/docs/security/hardening-checklist.mdx b/docs/site/src/content/docs/security/hardening-checklist.mdx index 3af2e759..1a1ab860 100644 --- a/docs/site/src/content/docs/security/hardening-checklist.mdx +++ b/docs/site/src/content/docs/security/hardening-checklist.mdx @@ -52,7 +52,8 @@ for your service. - Rotate a signing key by adding a new `kid`, moving credential profiles to it, then moving the old key to `publish_only` until its configured publication window ends or verifiers no longer need it. Do not delete a key verifiers may still need. -- Keep RS256 scoped to the eSignet RP client assertion key. Credential profile, access token, and +- Keep RS256 scoped to the eSignet RP client assertion key (eSignet, the MOSIP-project identity + provider, is the one integration that needs RS256). Credential profile, access token, and federation response signing use Ed25519 EdDSA or ES256/P-256; RS256 is rejected there. - If a signing key uses `provider: pkcs11`, confirm the deployed binary was built with the `pkcs11` capability (`registry-notary build-info`) and validate the module, token, and key lookup diff --git a/docs/site/src/content/docs/security/index.mdx b/docs/site/src/content/docs/security/index.mdx index d1858ed0..d0636e6f 100644 --- a/docs/site/src/content/docs/security/index.mdx +++ b/docs/site/src/content/docs/security/index.mdx @@ -66,12 +66,14 @@ issued credential without holding a credential of its own (REQ-SEC-G-007). When Notary issues a credential it is an SD-JWT VC: the signed body carries hashes of each disclosable field rather than the values, so the holder reveals only what they choose. -Holder binding is configurable per credential profile and defaults to off; the self-attestation +Holder binding, which cryptographically ties a credential to a key the holder controls, is +configurable per credential profile and defaults to off; the self-attestation (wallet) issuance path requires it. See [Evidence issuance, end to end](../explanation/evidence-issuance/) for the full credential lifecycle. -Delegated evaluation between services is admitted only from statically configured peers, and the +Delegated evaluation, where one Notary asks a trusted peer to evaluate a claim against a source +only that peer can read, is admitted only from statically configured peers, and the peer's identity, signature, freshness, and purpose are verified before any source read (REQ-SEC-G-010). diff --git a/docs/site/src/content/docs/security/report-a-vulnerability.mdx b/docs/site/src/content/docs/security/report-a-vulnerability.mdx index 3ab43c6f..30c57f96 100644 --- a/docs/site/src/content/docs/security/report-a-vulnerability.mdx +++ b/docs/site/src/content/docs/security/report-a-vulnerability.mdx @@ -65,7 +65,7 @@ For each signed release asset, download the asset, its `.sig` signature, and its certificate from the GitHub Release, then verify: ```bash -asset=registryctl-v0.8.3-linux-amd64 +asset=registryctl-v0.8.3-linux-amd64 # replace with the asset you downloaded cosign verify-blob \ --certificate "${asset}.pem" \ @@ -75,6 +75,8 @@ cosign verify-blob \ "${asset}" ``` +A successful check prints `Verified OK`. + If a release asset has no matching `.sig` and `.pem` files, treat it as unsigned; the `v0.8.0` prerelease predates release-asset signing. Git version tags are not yet cryptographically signed. The full verification steps, including SLSA provenance, are in `SECURITY.md` and diff --git a/docs/site/src/content/docs/start/credential-tour.mdx b/docs/site/src/content/docs/start/credential-tour.mdx index b728e4f5..e23a5f10 100644 --- a/docs/site/src/content/docs/start/credential-tour.mdx +++ b/docs/site/src/content/docs/start/credential-tour.mdx @@ -18,7 +18,7 @@ import QuickstartMeta from '../../../components/QuickstartMeta.astro'; In the hosted lab, **Registry Notary** issues a signed credential to a hosted demo wallet that answers a question without exposing the underlying record. This is a guided browser flow, not a -curl sequence: a real [OID4VCI](../../reference/glossary/#oid4vci) issuance needs a holder key and +curl sequence: a real [OID4VCI](../../reference/glossary/) issuance needs a holder key and a sign-in, which is wallet territory. You do not need to install a wallet: the lab hosts a demo wallet at [wallet.lab.registrystack.org](https://wallet.lab.registrystack.org), so you can receive the credential end to end in the browser. Everything runs in the hosted lab at @@ -47,13 +47,10 @@ connect to the rest of the stack, see the credentials, identities, and ready-made requests, open the lab homepage at [lab.registrystack.org](https://lab.registrystack.org). -If the hosted lab or one of its service URLs is unavailable, run the same multi-service topology -locally with [First run with Registry Lab](../../tutorials/first-run-with-registry-lab/). - ## Get a signed credential The citizen Notary issues a Selective Disclosure JWT Verifiable Credential -([SD-JWT VC](../../reference/glossary/#sd-jwt-vc)) of type `person_is_alive_sd_jwt`, media type +([SD-JWT VC](../../reference/glossary/)) of type `person_is_alive_sd_jwt`, media type `application/dc+sd-jwt`. The credential discloses a single predicate, whether the person is alive, as a true or false claim. It never hands over the underlying civil record. The [quickstart](../quickstart/) Relay row-read example uses `NID-1001` / Miguel Santos, while this diff --git a/docs/site/src/content/docs/start/quickstart.mdx b/docs/site/src/content/docs/start/quickstart.mdx index c38c4e92..98137412 100644 --- a/docs/site/src/content/docs/start/quickstart.mdx +++ b/docs/site/src/content/docs/start/quickstart.mdx @@ -119,14 +119,14 @@ The point of the gateway is that it never widens reach at request time. Send the to the rows endpoint and the Relay refuses it: ```sh -curl -sS -o /dev/null -w "%{http_code}\n" \ +curl -sS -i \ -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1" ``` -The Relay returns `403 Forbidden` with `{"code":"auth.scope_denied"}`. Same API, same network call, -different scope, different answer. For more on this service, see +The Relay refuses with `403 Forbidden`, and the `problem+json` body carries the stable code +`auth.scope_denied`. For more on this service, see [Registry Relay](../../products/registry-relay/). ## What you built From 410370219bffe347939f574512e211457e1ee232 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 13:25:28 +0700 Subject: [PATCH 19/25] docs(relay): replace frozen pre-monorepo doc links with relative paths configuration.md pointed at jeremi/registry-relay at a frozen SHA for metadata, development, and provenance docs; relative links track the monorepo and let the docs-site sync rewrite them to site routes. Signed-off-by: Jeremi Joslin --- crates/registry-relay/docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/registry-relay/docs/configuration.md b/crates/registry-relay/docs/configuration.md index 7757bc58..9e14eb91 100644 --- a/crates/registry-relay/docs/configuration.md +++ b/crates/registry-relay/docs/configuration.md @@ -270,7 +270,7 @@ Keep standard-facing meaning in the manifest: catalog, datasets, entities, fields, constraints, vocabularies, codelists, profiles, conformance claims, and descriptive ODRL policy metadata. -See [metadata.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/metadata.md) for the manifest schema, static publication, and +See [metadata.md](metadata.md) for the manifest schema, static publication, and the `metadata.manifest.*` / `runtime.binding.*` startup error codes. ODRL policy belongs in the portable metadata manifest, not in runtime dataset @@ -499,7 +499,7 @@ Token verification failures map to specific `auth.*` codes so audit pipelines ca | `auth.invalid_credential` | 401 | JWT decode failure not covered by a more specific variant | | `auth.jwks_unavailable` | 503 | JWKS fetch failed; Registry Relay cannot verify any token | -For a worked example of running Registry Relay against a local OIDC provider (using the project's dev Zitadel stack), see [development.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/development.md). +For a worked example of running Registry Relay against a local OIDC provider (using the project's dev Zitadel stack), see [development.md](development.md). ## Audit @@ -977,7 +977,7 @@ publicschema: | `schema_url` | `https://publicschema.org/schemas/{target}.schema.json` | `credentialSchema.id` in the issued VC | | `credential_type` | `{target}` | `type[1]` value in the issued VC | -See [provenance.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/provenance.md) for CEL context variables, issuance behavior, audit records, and the build and test commands for this feature. +See [provenance.md](provenance.md) for CEL context variables, issuance behavior, audit records, and the build and test commands for this feature. ## Aggregates @@ -1058,7 +1058,7 @@ The `provenance` block is optional. When absent or `enabled: false`, the gateway The key is named `provenance` for compatibility; it governs the response-credential issuer (DID, signing key, claim validity, and accepted media types). These credentials are W3C VCDM 2.0 VC-JWT with a Registry Relay JSON-LD context; they are not W3C PROV-O. -See [provenance.md](https://github.com/jeremi/registry-relay/blob/3938fdf3930e134d6dca97360baf64ac3a16bed2/docs/provenance.md) for the full signer, DID, schema, context, and rotation contract. +See [provenance.md](provenance.md) for the full signer, DID, schema, context, and rotation contract. ## Production checklist From 20757d05c42564bd42280cca14873b5984827bf0 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 13:25:28 +0700 Subject: [PATCH 20/25] docs(notary,manifest): point doc links and SDK coordinates at the monorepo Notary README and manifest docs linked jeremi/registry-notary and jeremi/registry-manifest at frozen SHAs; relative links track the monorepo. The client SDK guide's Cargo, pip, and pnpm coordinates now name registrystack/registry-stack with the products/notary/bindings paths. The sidecar guide's platform link follows the doc's move to products/platform/docs. Signed-off-by: Jeremi Joslin --- products/manifest/docs/overview.md | 8 ++++---- products/manifest/docs/profile-fixtures.md | 2 +- products/manifest/docs/reference.md | 14 +++++++------- products/manifest/docs/validate-and-render.md | 2 +- products/notary/docs/README.md | 14 +++++++------- products/notary/docs/client-sdk-guide.md | 16 ++++++++-------- .../notary/docs/sidecar-trust-and-secrets.md | 6 +++--- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/products/manifest/docs/overview.md b/products/manifest/docs/overview.md index c5abbda4..5d2bb11b 100644 --- a/products/manifest/docs/overview.md +++ b/products/manifest/docs/overview.md @@ -189,11 +189,11 @@ These are examples until reviewed against official OpenCRVS, OpenSPP, OpenIMIS, or maintainer-provided artifacts. Four additional subdirectories exist in `profiles/` for -[`opencrvs`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/opencrvs/), -[`openimis`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/openimis/), -[`openspp`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/openspp/), +[`opencrvs`](../profiles/opencrvs/), +[`openimis`](../profiles/openimis/), +[`openspp`](../profiles/openspp/), and -[`spdci`](https://github.com/jeremi/registry-manifest/tree/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/spdci/). +[`spdci`](../profiles/spdci/). Each contains only a `README.md` that marks it as a placeholder pending official review. ## v0 caveats diff --git a/products/manifest/docs/profile-fixtures.md b/products/manifest/docs/profile-fixtures.md index 01c1cc2e..b1adfa29 100644 --- a/products/manifest/docs/profile-fixtures.md +++ b/products/manifest/docs/profile-fixtures.md @@ -132,7 +132,7 @@ program area or official standard), follow these steps to create and validate it 6. Run `validate-profiles profiles` to confirm the fixture passes alongside all existing profiles. See -[`profiles/example-civil-registration/profile.yaml`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/example-civil-registration/profile.yaml) +[`profiles/example-civil-registration/profile.yaml`](../profiles/example-civil-registration/profile.yaml) for a complete example. ## Verification diff --git a/products/manifest/docs/reference.md b/products/manifest/docs/reference.md index 882b2cb6..6278db31 100644 --- a/products/manifest/docs/reference.md +++ b/products/manifest/docs/reference.md @@ -5,7 +5,7 @@ Look up CLI subcommand flags, manifest key definitions, federation metadata, pub ## CLI subcommands Source: -[`crates/registry-manifest-cli/src/main.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-cli/src/main.rs) +[`crates/registry-manifest-cli/src/main.rs`](../../../crates/registry-manifest-cli/src/main.rs) ### `validate` @@ -70,7 +70,7 @@ Use `--format cpsv-ap` for the service catalogue rather than `--format dcat --pr Schema version enforced: `registry-manifest/v1`. Source: -[`crates/registry-manifest-core/src/lib.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/src/lib.rs) +[`crates/registry-manifest-core/src/lib.rs`](../../../crates/registry-manifest-core/src/lib.rs) (`MetadataManifest` struct). | Key | Type | Required | Description | @@ -101,7 +101,7 @@ Source: | `application_profiles` | list of `ApplicationProfile` | No | Profile IDs and versions the catalog declares support for (for example, `[{id: "bregdcat-ap", version: "3.0.0"}]`). | | `standards` | `StandardsManifest` | No | Declares DCAT, SHACL, and JSON Schema versions in use. | -### DatasetManifest keys (common keys; see [source](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/src/lib.rs) for the full type definition) +### DatasetManifest keys (common keys; see [source](../../../crates/registry-manifest-core/src/lib.rs) for the full type definition) | Key | Description | | --- | --- | @@ -183,7 +183,7 @@ They belong in Registry Relay or Registry Notary runtime configuration, not in a `source`, `source_id`, `table`, `token_url`, `url`, `url_env`, `visibility` Source: -[`crates/registry-manifest-core/src/lib.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/src/lib.rs) +[`crates/registry-manifest-core/src/lib.rs`](../../../crates/registry-manifest-core/src/lib.rs) (`RUNTIME_ONLY_KEYS`). ## Schema versions @@ -244,7 +244,7 @@ for validation bugs, security fixes, or explicitly forbidden runtime-only keys. ## Publish output artifacts Source: -[`crates/registry-manifest-cli/src/main.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-cli/src/main.rs) +[`crates/registry-manifest-cli/src/main.rs`](../../../crates/registry-manifest-cli/src/main.rs) (`publish_command`). All paths are relative to the `--out` directory. @@ -272,7 +272,7 @@ The `index.json` structure contains the schema version, digest metadata, top-lev artifact URLs, and arrays for per-profile, per-schema, per-policy, and per-offering documents. Source: -[`crates/registry-manifest-cli/src/main.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-cli/src/main.rs) +[`crates/registry-manifest-cli/src/main.rs`](../../../crates/registry-manifest-cli/src/main.rs) Digest fields use `sha256:` values: @@ -356,7 +356,7 @@ Minimal example shape: ## Golden fixture coverage The test suite in -[`crates/registry-manifest-core/tests/metadata_core.rs`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/crates/registry-manifest-core/tests/metadata_core.rs) +[`crates/registry-manifest-core/tests/metadata_core.rs`](../../../crates/registry-manifest-core/tests/metadata_core.rs) asserts exact output for the following renderer and profile combinations. These golden files live under `crates/registry-manifest-core/tests/fixtures/golden/`. diff --git a/products/manifest/docs/validate-and-render.md b/products/manifest/docs/validate-and-render.md index 3e9bc1f2..3725dcae 100644 --- a/products/manifest/docs/validate-and-render.md +++ b/products/manifest/docs/validate-and-render.md @@ -25,7 +25,7 @@ If you have installed the binary directly, replace that prefix with `registry-ma The four example profile fixtures in the repository serve as ready-made manifest inputs. The commands in this how-to use -[`profiles/example-civil-registration/fixtures/metadata.yaml`](https://github.com/jeremi/registry-manifest/blob/bb7bc6d015f519a9d1a6b6a0b661a2d28566af9d/profiles/example-civil-registration/fixtures/metadata.yaml) +[`profiles/example-civil-registration/fixtures/metadata.yaml`](../profiles/example-civil-registration/fixtures/metadata.yaml) as the example path. Substitute your own manifest path as needed. diff --git a/products/notary/docs/README.md b/products/notary/docs/README.md index afb65d3d..e9b8e21b 100644 --- a/products/notary/docs/README.md +++ b/products/notary/docs/README.md @@ -15,7 +15,7 @@ Pick your path below. New to Registry Notary? Start with the hosted walkthrough - [Configure DHIS2 claim checks](https://docs.registrystack.org/tutorials/configure-dhis2-claim-checks/): use the built-in `http_json` source adapter to evaluate DHIS2 Tracker claims and issue an SD-JWT VC from the result. - [Architecture overview](architecture-overview.md): what Registry Notary is, the request lifecycle, and how the layers relate. -- [Capability matrix](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/notary-capability-matrix.md): which flows Notary supports today, by persona and system. +- [Capability matrix](notary-capability-matrix.md): which flows Notary supports today, by persona and system. - [Identity and record matching](identity-and-record-matching.md): how Notary resolves the target entity to a source record, the outcome model, and matching policy. ## Integrate @@ -23,14 +23,14 @@ Pick your path below. New to Registry Notary? Start with the hosted walkthrough For application and wallet developers calling the API or the SDKs. - [Client SDK guide](client-sdk-guide.md): evaluate claims and issue credentials from Rust, Python, and Node.js. -- [API reference](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/api-reference.md): the route-to-client-method matrix and the stable problem-code registry. +- [API reference](api-reference.md): the route-to-client-method matrix and the stable problem-code registry. - [Wallet interop with OID4VCI](oid4vci-wallet-interop.md): the OpenID4VCI wallet facade contract and compatibility checklist. - [SD-JWT VC conformance](sd-jwt-vc-conformance-profile.md): the supported credential wire contract and the explicit non-support list. - [OpenCRVS tutorial](opencrvs-dci-standalone-tutorial.md): issue local demo SD-JWT VCs from OpenCRVS birth-record evidence. - [OpenCRVS onboarding model](opencrvs-onboarding.md): understand the registryctl-generated project boundary, evidence question, demo signing posture, and lightweight PDP model. -- [Scenario patterns](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/notary-scenario-patterns.md): reusable evaluation, federation, and issuance flows with sequence diagrams. +- [Scenario patterns](notary-scenario-patterns.md): reusable evaluation, federation, and issuance flows with sequence diagrams. - [GITB conformance suite design](../specs/gitb-conformance-suite.md): target runtime scenarios and claim boundary for ITB/GITB interoperability evidence. ## Operate @@ -44,9 +44,9 @@ For operators deploying, configuring, and running a Registry Notary. - [Script (Rhai) source adapter](script-rhai-source-adapter-guide.md): run a sandboxed, orchestration-only Rhai script for sources that need a little branching across a few governed reads. - [Signing key providers](signing-key-provider.md): SD-JWT VC signing-key configuration, rotation, and PKCS#11 setup. - [Self-attestation](self-attestation-operator-guide.md): citizen OIDC subject binding, token policy, allow-lists, and rollout. -- [Federated evaluation](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/federated-evaluation-operator-guide.md): static-peer setup, environment variables, and the replay limitation. +- [Federated evaluation](federated-evaluation-operator-guide.md): static-peer setup, environment variables, and the replay limitation. - [Credential lifecycle and status](credential-lifecycle-status.md): short-lived credentials, optional live status, retention, and verifier caveats. -- [Sidecar trust and secret handling](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/sidecar-trust-and-secrets.md): how the source adapter sidecar verifies its configuration, how Notary pins the sidecar it trusts, how secrets are handled, and what that path does and does not protect against. +- [Sidecar trust and secret handling](sidecar-trust-and-secrets.md): how the source adapter sidecar verifies its configuration, how Notary pins the sidecar it trusts, how secrets are handled, and what that path does and does not protect against. - [Deployment hardening runbook](deployment-hardening-runbook.md): production-readiness checklist for network boundaries, secrets, Redis, audit, and rollback. ## Build and maintain @@ -56,9 +56,9 @@ For maintainers changing the code or reviewing design history. - [Workspace layout](../README.md#layout): the crates and bindings and what each owns. - [Command-line interface](../../../crates/registry-notary/README.md): the server binary and its subcommands. - [Design records](../specs/README.md): specifications and implementation traces, kept as design history. -- [Security assurance](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/security-assurance.md): CI security gates, image publication and signing policy. +- [Security assurance](security-assurance.md): CI security gates, image publication and signing policy. ## Related -- [Release notes](https://github.com/jeremi/registry-notary/blob/f182385a5065873aac030c41d9fe020704afc4e2/docs/release-notes.md) +- [Release notes](release-notes.md) - [Security policy](../../../SECURITY.md) diff --git a/products/notary/docs/client-sdk-guide.md b/products/notary/docs/client-sdk-guide.md index 24ff2a32..05882027 100644 --- a/products/notary/docs/client-sdk-guide.md +++ b/products/notary/docs/client-sdk-guide.md @@ -74,16 +74,16 @@ integrations to a release tag or commit. ```toml [dependencies] -registry-notary-client = { git = "https://github.com/jeremi/registry-notary", tag = "vX.Y.Z" } -registry-notary-core = { git = "https://github.com/jeremi/registry-notary", tag = "vX.Y.Z" } +registry-notary-client = { git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z" } +registry-notary-core = { git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z" } ``` ```bash -python -m pip install "git+https://github.com/jeremi/registry-notary.git@vX.Y.Z#subdirectory=bindings/python" +python -m pip install "git+https://github.com/registrystack/registry-stack.git@vX.Y.Z#subdirectory=products/notary/bindings/python" ``` ```bash -pnpm add "github:jeremi/registry-notary#vX.Y.Z&path:bindings/node" +pnpm add "github:registrystack/registry-stack#vX.Y.Z&path:products/notary/bindings/node" ``` npm does not support installing from a subdirectory of a git repository. With @@ -857,7 +857,7 @@ In Rust, enable the `oid4vci` feature: ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["oid4vci"] } @@ -890,7 +890,7 @@ In Rust, enable the `federation` feature: ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["federation"] } @@ -1006,7 +1006,7 @@ Enable optional routes only when needed: ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["oid4vci", "federation", "json-facade"] } @@ -1059,7 +1059,7 @@ network refreshes or trust-policy decisions. ```toml registry-notary-client = { - git = "https://github.com/jeremi/registry-notary", + git = "https://github.com/registrystack/registry-stack", tag = "vX.Y.Z", features = ["verifier"] } diff --git a/products/notary/docs/sidecar-trust-and-secrets.md b/products/notary/docs/sidecar-trust-and-secrets.md index aef410e6..59290d35 100644 --- a/products/notary/docs/sidecar-trust-and-secrets.md +++ b/products/notary/docs/sidecar-trust-and-secrets.md @@ -13,7 +13,7 @@ The general governed-configuration model (signed bundles, TUF verification, trus roots, signer thresholds, and anti-rollback) is a shared Registry Platform capability used by Registry Relay and Registry Notary alike. This page does not restate it. See -[Governed configuration](../../registry-platform/docs/governed-configuration.md) +[Governed configuration](../../platform/docs/governed-configuration.md) for the shared model and how verification, authorization, and rollback protection work. Configuration integrity there is built on TUF (The Update Framework) through a standard, maintained client, not homegrown cryptography. What follows @@ -39,7 +39,7 @@ is the Notary- and sidecar-specific layer on top of that model. ## What you are responsible for Key custody and trust-root distribution are part of the -[shared governed-configuration model](../../registry-platform/docs/governed-configuration.md#trust-roots-roles-and-change-classes) +[shared governed-configuration model](../../platform/docs/governed-configuration.md#trust-roots-roles-and-change-classes) and matter just as much here: the guarantees above are only as strong as your protection of the signing keys. Specific to the sidecar path: @@ -134,7 +134,7 @@ without production key custody. ## Where to go next -- [Governed configuration](../../registry-platform/docs/governed-configuration.md): +- [Governed configuration](../../platform/docs/governed-configuration.md): the shared platform model behind signed configuration, trust roots, signer thresholds, and anti-rollback. Read this first for the trust model itself. - [Model sources and claims](source-claim-modeling-guide.md): configure the From 930c6f3556b450ecddceb07ed4db15a52189625a Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 13:25:28 +0700 Subject: [PATCH 21/25] docs: publish linked adopter-facing product docs on the site 24 repo docs that product pages already linked (Relay API guide, ops runbook, metadata, provenance, OpenFn and standards adapter guides; Notary API reference, capability matrix, operator and adapter guides; Manifest ITB/SEMIC validation) join the sync allowlist, so their links rewrite to site routes instead of bouncing readers to GitHub markdown. Contributor docs (development, perf, specs) intentionally stay GitHub links. The two security-assurance docs stay unpublished until their pre-monorepo container-signing claims are corrected; tracked in #186. New entries are excluded from the old-repo beta docsets and verified present at the v0.8.1 through v0.8.3 archive refs. Signed-off-by: Jeremi Joslin --- docs/site/src/data/generated/sidebar.json | 124 ++++++++++++++++++- docs/site/src/data/repo-docs.yaml | 144 ++++++++++++++++++++++ 2 files changed, 264 insertions(+), 4 deletions(-) diff --git a/docs/site/src/data/generated/sidebar.json b/docs/site/src/data/generated/sidebar.json index 2b6cf46c..7b513aae 100644 --- a/docs/site/src/data/generated/sidebar.json +++ b/docs/site/src/data/generated/sidebar.json @@ -8,12 +8,67 @@ "slug": "products/registry-relay" }, { - "label": "Client integration", - "slug": "products/registry-relay/client-integration" + "label": "How-to", + "items": [ + { + "label": "Client integration", + "slug": "products/registry-relay/client-integration" + }, + { + "label": "Operations runbook", + "slug": "products/registry-relay/ops" + }, + { + "label": "Signed response credentials", + "slug": "products/registry-relay/provenance" + }, + { + "label": "Call Relay from OpenFn", + "slug": "products/registry-relay/openfn-relay-adaptor-guide" + }, + { + "label": "Standards adapter operator guide", + "slug": "products/registry-relay/standards-adapter-operator-guide" + } + ] + }, + { + "label": "Reference", + "items": [ + { + "label": "API guide", + "slug": "products/registry-relay/api" + }, + { + "label": "Configuration", + "slug": "products/registry-relay/configuration" + }, + { + "label": "Portable metadata", + "slug": "products/registry-relay/metadata" + }, + { + "label": "XLSX readiness contract", + "slug": "products/registry-relay/xlsx-readiness-contract" + }, + { + "label": "Release notes", + "slug": "products/registry-relay/release-notes" + } + ] }, { - "label": "Configuration", - "slug": "products/registry-relay/configuration" + "label": "Explanation", + "items": [ + { + "label": "Standards assumptions", + "slug": "products/registry-relay/standards-assumptions" + }, + { + "label": "Relay scenario catalog", + "slug": "products/registry-relay/relay-scenario-catalog" + } + ] } ] }, @@ -25,6 +80,15 @@ "label": "Overview", "slug": "products/registry-notary" }, + { + "label": "Tutorials", + "items": [ + { + "label": "OpenCRVS DCI standalone tutorial", + "slug": "products/registry-notary/opencrvs-dci-standalone-tutorial" + } + ] + }, { "label": "How-to", "items": [ @@ -36,13 +100,41 @@ "label": "Source and claim modeling", "slug": "products/registry-notary/source-claim-modeling-guide" }, + { + "label": "FHIR source adapter", + "slug": "products/registry-notary/fhir-source-adapter-guide" + }, + { + "label": "Script and Rhai source adapters", + "slug": "products/registry-notary/script-rhai-source-adapter-guide" + }, + { + "label": "OpenCRVS onboarding", + "slug": "products/registry-notary/opencrvs-onboarding" + }, + { + "label": "Federated evaluation operator guide", + "slug": "products/registry-notary/federated-evaluation-operator-guide" + }, + { + "label": "Self-attestation operator guide", + "slug": "products/registry-notary/self-attestation-operator-guide" + }, { "label": "Credential lifecycle and status", "slug": "products/registry-notary/credential-lifecycle-status" }, + { + "label": "Sidecar trust and secrets", + "slug": "products/registry-notary/sidecar-trust-and-secrets" + }, { "label": "Signing key provider", "slug": "products/registry-notary/signing-key-provider" + }, + { + "label": "Deployment hardening runbook", + "slug": "products/registry-notary/deployment-hardening-runbook" } ] }, @@ -53,9 +145,21 @@ "label": "Operator configuration reference", "slug": "products/registry-notary/operator-config-reference" }, + { + "label": "API reference", + "slug": "products/registry-notary/api-reference" + }, + { + "label": "OID4VCI wallet interop", + "slug": "products/registry-notary/oid4vci-wallet-interop" + }, { "label": "SD-JWT VC conformance profile", "slug": "products/registry-notary/sd-jwt-vc-conformance-profile" + }, + { + "label": "Release notes", + "slug": "products/registry-notary/release-notes" } ] }, @@ -66,6 +170,14 @@ "label": "Notary architecture", "slug": "products/registry-notary/architecture-overview" }, + { + "label": "Capability matrix", + "slug": "products/registry-notary/notary-capability-matrix" + }, + { + "label": "Scenario patterns", + "slug": "products/registry-notary/notary-scenario-patterns" + }, { "label": "Identity and record matching", "slug": "products/registry-notary/identity-and-record-matching" @@ -90,6 +202,10 @@ "label": "Validate against profile fixtures", "slug": "products/registry-manifest/profile-fixtures" }, + { + "label": "ITB and SEMIC validation", + "slug": "products/registry-manifest/itb-semic-validation" + }, { "label": "Reference", "slug": "products/registry-manifest/reference" diff --git a/docs/site/src/data/repo-docs.yaml b/docs/site/src/data/repo-docs.yaml index 64e2acd1..1f2b39d2 100644 --- a/docs/site/src/data/repo-docs.yaml +++ b/docs/site/src/data/repo-docs.yaml @@ -27,6 +27,66 @@ repos: label: Configuration nav_order: 30 doc_type: reference + - src: crates/registry-relay/docs/api.md + dest: products/registry-relay/api + label: API guide + nav_order: 25 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/metadata.md + dest: products/registry-relay/metadata + label: Portable metadata + nav_order: 35 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/ops.md + dest: products/registry-relay/ops + label: Operations runbook + nav_order: 40 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/provenance.md + dest: products/registry-relay/provenance + label: Signed response credentials + nav_order: 45 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/openfn-relay-adaptor-guide.md + dest: products/registry-relay/openfn-relay-adaptor-guide + label: Call Relay from OpenFn + nav_order: 50 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/standards-adapter-operator-guide.md + dest: products/registry-relay/standards-adapter-operator-guide + label: Standards adapter operator guide + nav_order: 55 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/xlsx-readiness-contract.md + dest: products/registry-relay/xlsx-readiness-contract + label: XLSX readiness contract + nav_order: 60 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/STANDARDS_ASSUMPTIONS.md + dest: products/registry-relay/standards-assumptions + label: Standards assumptions + nav_order: 65 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/relay-scenario-catalog.md + dest: products/registry-relay/relay-scenario-catalog + label: Relay scenario catalog + nav_order: 70 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: crates/registry-relay/docs/release-notes.md + dest: products/registry-relay/release-notes + label: Release notes + nav_order: 80 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] registry-notary: remote: https://github.com/registrystack/registry-stack ref: HEAD @@ -91,6 +151,84 @@ repos: label: SD-JWT VC conformance profile nav_order: 130 doc_type: reference + - src: products/notary/docs/notary-capability-matrix.md + dest: products/registry-notary/notary-capability-matrix + label: Capability matrix + nav_order: 15 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/notary-scenario-patterns.md + dest: products/registry-notary/notary-scenario-patterns + label: Scenario patterns + nav_order: 20 + doc_type: explanation + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/fhir-source-adapter-guide.md + dest: products/registry-notary/fhir-source-adapter-guide + label: FHIR source adapter + nav_order: 62 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/script-rhai-source-adapter-guide.md + dest: products/registry-notary/script-rhai-source-adapter-guide + label: Script and Rhai source adapters + nav_order: 64 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/opencrvs-onboarding.md + dest: products/registry-notary/opencrvs-onboarding + label: OpenCRVS onboarding + nav_order: 66 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/opencrvs-dci-standalone-tutorial.md + dest: products/registry-notary/opencrvs-dci-standalone-tutorial + label: OpenCRVS DCI standalone tutorial + nav_order: 68 + doc_type: tutorial + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/federated-evaluation-operator-guide.md + dest: products/registry-notary/federated-evaluation-operator-guide + label: Federated evaluation operator guide + nav_order: 72 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/self-attestation-operator-guide.md + dest: products/registry-notary/self-attestation-operator-guide + label: Self-attestation operator guide + nav_order: 75 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/sidecar-trust-and-secrets.md + dest: products/registry-notary/sidecar-trust-and-secrets + label: Sidecar trust and secrets + nav_order: 85 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/deployment-hardening-runbook.md + dest: products/registry-notary/deployment-hardening-runbook + label: Deployment hardening runbook + nav_order: 95 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/api-reference.md + dest: products/registry-notary/api-reference + label: API reference + nav_order: 100 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/oid4vci-wallet-interop.md + dest: products/registry-notary/oid4vci-wallet-interop + label: OID4VCI wallet interop + nav_order: 110 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] + - src: products/notary/docs/release-notes.md + dest: products/registry-notary/release-notes + label: Release notes + nav_order: 140 + doc_type: reference + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] registry-manifest: remote: https://github.com/registrystack/registry-stack ref: HEAD @@ -123,3 +261,9 @@ repos: label: Reference nav_order: 30 doc_type: reference + - src: products/manifest/docs/itb-semic-validation.md + dest: products/registry-manifest/itb-semic-validation + label: ITB and SEMIC validation + nav_order: 25 + doc_type: how-to + exclude_docsets: [beta-5, beta-4, beta-3, beta-2026-06-12] From c69762f8cb34fc802bb863f5c0df380e35c2d0e2 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 14:01:37 +0700 Subject: [PATCH 22/25] docs(notary): replace em dashes with house punctuation The docs-site style guide bans em dashes and its Vale gate now covers synced product pages; these two source docs were the only pages with error-level hits. Signed-off-by: Jeremi Joslin --- .../notary/docs/operator-config-reference.md | 2 +- .../docs/script-rhai-source-adapter-guide.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/products/notary/docs/operator-config-reference.md b/products/notary/docs/operator-config-reference.md index 884ac8c0..7a7bf2d2 100644 --- a/products/notary/docs/operator-config-reference.md +++ b/products/notary/docs/operator-config-reference.md @@ -501,7 +501,7 @@ handling, or output normalization outside Notary. The sidecar source chooses `engine: http_json`, `engine: http_flow`, `engine: fhir`, or `engine: script_rhai` (a sandboxed, orchestration-only Rhai script for sources that need a little branching across a few governed source calls, such as a JSON -POST search followed by a GET, or a visible-404 fallback — see the +POST search followed by a GET, or a visible-404 fallback; see the [Script (Rhai) Source Adapter Guide](script-rhai-source-adapter-guide.md)) in its own signed manifest. The source connection must use static sidecar bearer auth through `token_env`. Do not configure target-service credentials in Notary; keep them diff --git a/products/notary/docs/script-rhai-source-adapter-guide.md b/products/notary/docs/script-rhai-source-adapter-guide.md index d9045928..fa27a396 100644 --- a/products/notary/docs/script-rhai-source-adapter-guide.md +++ b/products/notary/docs/script-rhai-source-adapter-guide.md @@ -5,7 +5,7 @@ orchestration script** when a source API needs a little imperative shaping that is awkward for the declarative `http_json` / `http_flow` engines but too small to justify a bespoke adapter. The script is **orchestration-only**: it decides which configured request to make next and shapes the returned JSON into records. It -never holds network authority, credentials, or policy — the sidecar host keeps +never holds network authority, credentials, or policy; the sidecar host keeps all of those. Notary configs use the normal `connector: source_adapter_sidecar`; the @@ -21,10 +21,10 @@ row shape every engine returns: Reach for `script_rhai` only when the declarative engines do not fit: -- `http_json` — one governed request and a JSON/CEL projection. -- `http_flow` — a fixed declarative sequence of dependent reads. -- `fhir` — a bounded FHIR R4 GET graph. -- `script_rhai` — 1–3 governed source calls where the script must **branch** on +- `http_json`: one governed request and a JSON/CEL projection. +- `http_flow`: a fixed declarative sequence of dependent reads. +- `fhir`: a bounded FHIR R4 GET graph. +- `script_rhai`: 1–3 governed source calls where the script must **branch** on a response (e.g. POST a search body, then GET a returned id; try one path, fall back on a 404), or normalize source-specific JSON that the declarative mappers cannot express. @@ -56,7 +56,7 @@ so a buggy script or a hostile *upstream response* cannot escalate: concurrency are all bounded. The script is **compiled and smoke-tested at startup**; a compile, policy, or smoke failure blocks readiness. - **Governed provenance.** The script is embedded inline in the signed runtime - target, so it is covered by the target's `config_hash` — the same TUF-verified + target, so it is covered by the target's `config_hash`, the same TUF-verified content anchor used for inline CEL today. ## The script contract @@ -222,7 +222,7 @@ for upstreams that require a JSON token request. Optional fields are `scope`, or a vendor API-version header) on every call to that target. Header values are governed config and flow through the `config_hash`. Authentication, cookie, host/length framing, hop-by-hop, and forwarding headers (and any `Proxy-*` -header) are **rejected at startup** — put credentials in `auth`, not `headers`. +header) are **rejected at startup**: put credentials in `auth`, not `headers`. ### Observable statuses @@ -230,7 +230,7 @@ By default any non-2xx terminates the run and maps to a problem code (`401`/`403` → `source.target_auth`, `429` → `source.target_rate_limit`, timeout → `source.timeout`, everything else → `source.unavailable`). List a status in a target's `visible_statuses` to let the script **observe** it as -`#{ status, body }` and branch instead — for example a `404` that means +`#{ status, body }` and branch instead, for example a `404` that means "not found here, try the fallback". The engine is compiled with the union of all targets' visible statuses as a ceiling, and the per-target list is enforced by the host. @@ -261,7 +261,7 @@ engines. Because the script is inline in the signed runtime target, it is covered by the target `config_hash` that Notary verifies before source reads. The `/ready` and `/v1/assurance` booleans (`expression_hashes_verified`, `runtime_verified`, `smoke_verified`) attest that whole-target hash together -with the startup compile and smoke check — a startup failure on any of them +with the startup compile and smoke check; a startup failure on any of them blocks readiness, so a sidecar that serves these values has satisfied all three. ## Local verification From 7788ee7286b343cdfdfa51ed029f56a568bb5853 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 14:01:37 +0700 Subject: [PATCH 23/25] fix(docs): close four style-tooling gaps - Run Vale on synced product pages: drop the !**/products/** glob from check:style. The 40 synced pages had zero error-level hits after the companion notary prose fix, so the gate stays green while gaining the backstop it lacked. - Wire evidence_gap into the pipeline: generate-data.mjs rejects non-boolean values and StandardsTable renders the flag next to the claim level (ink weight, no new color), so the style guide's instruction is enforced and visible instead of a bare convention. - Align the code-link rule with shipped practice: pin to a release tag or commit SHA; every load-bearing link on the site pins v0.8.3 tags, which the old SHA-only wording outlawed. - Fix the AGENTS.md pointer to the design doc (the relative path never resolved in this repo; the doc is maintained alongside the repository, not published in it) and the Latin vale rule's swapped %s message, which told writers to use 'e.g.' instead of spelling it out. Signed-off-by: Jeremi Joslin --- docs/site/AGENTS.md | 4 +++- docs/site/docs/style-guide.md | 4 ++-- docs/site/package.json | 2 +- docs/site/scripts/generate-data.mjs | 3 +++ docs/site/src/components/StandardsTable.astro | 10 ++++++++-- docs/site/src/styles/custom.css | 9 ++++++++- docs/site/styles/RegistryDocs/Latin.yml | 2 +- 7 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/site/AGENTS.md b/docs/site/AGENTS.md index c56809d5..3d303d3e 100644 --- a/docs/site/AGENTS.md +++ b/docs/site/AGENTS.md @@ -18,7 +18,9 @@ This repo is an Astro and Starlight documentation site. Read `docs/style-guide.md` before drafting or editing any page. It covers voice, structure, frontmatter, page types, the banned-word list, claim levels for standards, and the GitLab rules we adopt, adapt, or skip. The visual design -language is recorded separately in `../design-registry-docs.md`. +language is recorded separately in `design-registry-docs.md`, maintained +alongside the repository, not published in it; the binding visual rules for +diagrams are summarized in the style guide's "Images and diagrams" section. Every factual claim about a source repo must be anchored in code, tests, fixtures, OpenAPI, or an upstream standard. When evidence is missing, mark the diff --git a/docs/site/docs/style-guide.md b/docs/site/docs/style-guide.md index e770bac7..d1a952e3 100644 --- a/docs/site/docs/style-guide.md +++ b/docs/site/docs/style-guide.md @@ -128,7 +128,7 @@ Preferred terms. - Do not capitalize the target page's title inside link text unless it is a proper noun. - Cap one paragraph at three links. Cap one page at fifteen. If you need more, the page should be a list. - Link to upstream standards bodies first, then to mirrors or summaries. -- Link to a commit SHA, not a branch, when linking to code that may move. +- Pin links to code to a release tag (`v0.8.3`) or a commit SHA, never a branch, when the claim depends on the code state. ## Tables @@ -200,7 +200,7 @@ This applies to every page that touches a standard or a contract. - No animated GIFs or videos in v0. - No screenshots as the only source of instructions. - No real user data, real production hostnames, or real tokens, even in `example` blocks. -- No relative links into source repos. Use full URLs with a commit SHA. +- No relative links into source repos. Use full URLs pinned to a release tag or commit SHA. - No nested admonitions. No admonition immediately under H1. - No anchor links into other pages; link to the page and use the sidebar's on-this-page index. - No `should` as a promise. Either it does or it does not. diff --git a/docs/site/package.json b/docs/site/package.json index 44146a5d..85aade7c 100644 --- a/docs/site/package.json +++ b/docs/site/package.json @@ -19,7 +19,7 @@ "check:content": "node scripts/check-doc-frontmatter.mjs", "check:svg": "node scripts/check-svg-a11y.mjs", "check:markdown": "markdownlint-cli2", - "check:style": "node scripts/run-vale.mjs --glob=!**/products/** src/content/docs README.md", + "check:style": "node scripts/run-vale.mjs src/content/docs README.md", "check:style:fixtures": "node scripts/check-vale-fixtures.mjs", "check:openapi": "redocly lint registry-relay registry-notary", "check:config-vocabulary": "scripts/check-stale-config-vocabulary.sh", diff --git a/docs/site/scripts/generate-data.mjs b/docs/site/scripts/generate-data.mjs index 3370cf9b..322d5c78 100644 --- a/docs/site/scripts/generate-data.mjs +++ b/docs/site/scripts/generate-data.mjs @@ -42,6 +42,9 @@ async function loadYaml(name) { throw new Error(`${name}.yaml entry ${index + 1} is missing ${key}`); } } + if (name === 'standards' && 'evidence_gap' in item && typeof item.evidence_gap !== 'boolean') { + throw new Error(`standards.yaml entry ${index + 1} evidence_gap must be true or false`); + } } return parsed; } diff --git a/docs/site/src/components/StandardsTable.astro b/docs/site/src/components/StandardsTable.astro index af15f3a2..276bfd6f 100644 --- a/docs/site/src/components/StandardsTable.astro +++ b/docs/site/src/components/StandardsTable.astro @@ -1,9 +1,12 @@ --- import standards from '../data/generated/standards.json'; + +// evidence_gap is an optional flag; absent from the JSON unless a row sets it. +const rows = standards as ((typeof standards)[number] & { evidence_gap?: boolean })[]; ---
- {standards.map((standard) => ( + {rows.map((standard) => (

@@ -21,7 +24,10 @@ import standards from '../data/generated/standards.json'; {standard.last_checked && · {standard.last_checked}}
Claim level
-
{standard.claim_level}
+
+ {standard.claim_level} + {standard.evidence_gap && · evidence gap} +
Adoption mode
{standard.adoption_mode}
Used by
diff --git a/docs/site/src/styles/custom.css b/docs/site/src/styles/custom.css index 2d7ec21a..89871b65 100644 --- a/docs/site/src/styles/custom.css +++ b/docs/site/src/styles/custom.css @@ -709,7 +709,8 @@ pre, .status-chip, .claim-chip, -.adoption-chip { +.adoption-chip, +.gap-chip { display: inline; border: 0; padding: 0; @@ -724,6 +725,12 @@ pre, background: transparent; } +/* Unresolved evidence_gap flag from standards.yaml; ink weight, not color. */ +.gap-chip { + color: var(--registry-ink); + font-weight: 600; +} + .source-note { color: var(--registry-muted); font-size: 0.85rem; diff --git a/docs/site/styles/RegistryDocs/Latin.yml b/docs/site/styles/RegistryDocs/Latin.yml index c6509d48..1066f864 100644 --- a/docs/site/styles/RegistryDocs/Latin.yml +++ b/docs/site/styles/RegistryDocs/Latin.yml @@ -1,5 +1,5 @@ extends: substitution -message: "Spell out '%s' as '%s'." +message: "Use '%s' instead of '%s'." level: warning ignorecase: true nonword: true From a5c15042f0965b48612c5f4cc1686fab0da7a2d6 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 14:17:15 +0700 Subject: [PATCH 24/25] docs(manifest): drop stale warning issue links Signed-off-by: Jeremi Joslin --- .../site/src/content/docs/reference/itb-semic-evidence.mdx | 5 ++--- products/manifest/docs/itb-semic-validation.md | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/site/src/content/docs/reference/itb-semic-evidence.mdx b/docs/site/src/content/docs/reference/itb-semic-evidence.mdx index a1958dcf..0612ee41 100644 --- a/docs/site/src/content/docs/reference/itb-semic-evidence.mdx +++ b/docs/site/src/content/docs/reference/itb-semic-evidence.mdx @@ -36,9 +36,8 @@ It is not a certification statement, and it does not claim every generated artif [jeremi/registry-manifest#27](https://github.com/jeremi/registry-manifest/pull/27). - Registry Notary records the protected discovery policy and GITB runtime-suite target in [jeremi/registry-notary#245](https://github.com/jeremi/registry-notary/pull/245). -- The Manifest follow-up issues for BRegDCAT warnings are - [publisher CorporateBodyRestriction](https://github.com/jeremi/registry-manifest/issues/23) and - [`dcat:themeTaxonomy`](https://github.com/jeremi/registry-manifest/issues/24). +- Registry Manifest documents the BRegDCAT warning boundary in the Manifest + ITB and SEMIC validation guide. ## What is not covered diff --git a/products/manifest/docs/itb-semic-validation.md b/products/manifest/docs/itb-semic-validation.md index 39274ace..999cda62 100644 --- a/products/manifest/docs/itb-semic-validation.md +++ b/products/manifest/docs/itb-semic-validation.md @@ -66,10 +66,9 @@ data-theme concept scheme and EuroVoc concept scheme so consumers can see both catalogue theme taxonomies. The public 2.x warning profiles may still flag one value while accepting the artifact with zero validation errors. -Tracked follow-up issues: - -- [#23 Investigate BRegDCAT publisher CorporateBodyRestriction warning](https://github.com/jeremi/registry-manifest/issues/23) -- [#24 Resolve profile-specific `dcat:themeTaxonomy` warnings from BRegDCAT validation](https://github.com/jeremi/registry-manifest/issues/24) +These warning classes are intentionally documented rather than hidden: the +smoke script retains the validator reports, and public release notes should +carry the profile and warning count when BRegDCAT-AP evidence is cited. ## Public Claim Boundary From 6a26e06381c8b085575b4bb58b3fb78b6345cec2 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 2 Jul 2026 14:17:15 +0700 Subject: [PATCH 25/25] docs: catch stale config terms in generated pages Signed-off-by: Jeremi Joslin --- docs/site/scripts/check-stale-config-vocabulary.sh | 2 +- products/notary/docs/release-notes.md | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/site/scripts/check-stale-config-vocabulary.sh b/docs/site/scripts/check-stale-config-vocabulary.sh index d0a5639d..9f3726cf 100755 --- a/docs/site/scripts/check-stale-config-vocabulary.sh +++ b/docs/site/scripts/check-stale-config-vocabulary.sh @@ -12,7 +12,7 @@ PATHS=( status=0 if command -v rg >/dev/null 2>&1; then - rg -n --glob '!scripts/check-stale-config-vocabulary.sh' "$PATTERN" "${PATHS[@]}" || status=$? + rg -n --no-ignore --glob '!scripts/check-stale-config-vocabulary.sh' "$PATTERN" "${PATHS[@]}" || status=$? else grep -RInE --exclude='check-stale-config-vocabulary.sh' "$PATTERN" "${PATHS[@]}" || status=$? fi diff --git a/products/notary/docs/release-notes.md b/products/notary/docs/release-notes.md index 04747716..d3b57484 100644 --- a/products/notary/docs/release-notes.md +++ b/products/notary/docs/release-notes.md @@ -81,11 +81,10 @@ `fingerprint` reference (`provider`, `name`, `commitment`) in place of `hash_env`, so signed config apply can govern caller-credential rotation. - Renamed OIDC config fields to the shared Registry service convention: - `auth.oidc.jwks_uri` -> `auth.oidc.jwks_url`, - `auth.oidc.leeway_seconds` -> `auth.oidc.leeway`, and - `auth.oidc.allowed_typ` -> `auth.oidc.allowed_token_types`. Old names fail - config load with an error naming the replacement. `auth.oidc.leeway` now uses - humantime strings such as `30s`; self-attestation + `auth.oidc.jwks_url`, `auth.oidc.leeway`, and + `auth.oidc.allowed_token_types`. Legacy aliases fail config load with an error + naming the replacement. `auth.oidc.leeway` now uses humantime strings such as + `30s`; self-attestation `token_policy.max_clock_leeway_seconds` still bounds the resolved duration. - Removed `server.cors.allow_credentials`; Registry Notary now always disables credentialed CORS on the operator-configured server CORS layer. Remove the