Component catalog¶
This catalog is the lowest zoom level of the building block view. It lists every component of gbo-demo v0.9.0 with its responsibility and a link to its code. Each block is either part of the target architecture or demo scaffolding. Demo scaffolding makes the demo usable or observable.
To read the design from the top down, start at the overview. It shows GBO Basis and the channels that GBO Basis feeds, and each block there zooms in to this catalog.
All components in one view¶
The demo implements three routes end to end: the DvTP query under consent, EUDI issuance, and ASI verification for QTSPs. The SDG-OOTS adapter has no implementation and is not in this view (ADR-0020). The QTSP and the Discover Interface are external roles. The context view shows them.
The status lookup is one edge in the diagram. It runs from the PDP's own
Outway to the register's Inway, as the FSC service consent-status. The
dotted LDV edges stand for every component that writes processing records
(Level 2: logging and audit).
Level 2: authorization chain¶
The authorization chain has the following components:
| Component | Responsibility | Implementation |
|---|---|---|
| OpenFTV PDP | Decides every data request and every FSC contract submission. The build uses OpenFTV tag v0.1.0 and patches in the GBO FTV GraphQL mapper. If a patch step changes nothing, the build fails. The PDP offers AuthZEN 1.0 at POST /authzen/v1/evaluation over HTTPS. It writes one ADL record per evaluation. It does not start without the ADL database. |
services/openftv-pdp, Dockerfile |
| FTV GraphQL mapper | Implements the mapping function of the FTV GraphQL profile (ftv-graphql/0.1). It parses the raw body and resolves parent types from the bundled schema of the FSC service in the access token. It returns the field list with arguments as the source executes them, or COVERAGE_UNVERIFIABLE with a subcode. It accepts only queries. For any other operation it returns OPERATION_NOT_SUPPORTED. It applies fixed limits: body 1 MiB, depth 64, and 1000 field records. It verifies no token, reads no flow, and calls no external service. |
services/ftv-graphql-mapper, limits, OpenFTV adapter |
| Bundled schemas and pins | One directive-free schema per FSC service (bri, brp, lvg). The policy bundle pins the digest of each schema. The policy denies an evaluation against another schema (SCHEMA_MISMATCH). A CI job compares the bundled schemas with the source schemas. |
schemas/pdp-mirror, graphql_schemas.rego, check-schema-drift.sh |
| Policy entrypoint | Allows the source-metadata fetch for two known peers, on the two metadata services only. It sends every other request to the rule engine. It passes only allow and the consumer's reason code to OpenFTV. |
policies/authz.rego |
| Rule engine | Selects the regime from the evidence. If the request carries a consent token, the regime is consent. Without a token, the regime is PID. The engine binds a field to the rules of that regime that serve the FSC service and name the field key. It denies a field that no rule covers (NO_APPLICABLE_RULE). The first bound rule without a reason grants the field. The engine checks the integrator mandate on every delegated call. It returns a coarse reason code to the consumer and keeps the per-field codes for the operator. |
policies/gbo/engine.rego |
| Request reader | Reads the field list, the arguments, the caller, the represented party, and the time from the input. It does no checks. | policies/gbo/lib.rego |
| Consent PIP | Verifies the consent token in policy: ES256 signature, issuer, audience, type, validity with 30 s skew, and required claims. It fetches the JWKS directly from the register and caches it for 300 s. It fetches the status for each evaluation over FSC, through the PDP's Outway. The status fetch has no cache and a 2 s timeout. Every failure has its own reason code. The engine copies the resolved consent into its response. | policies/gbo/consent.rego |
| Authorization rules | Five plain-Rego rules, listed in the next table. Each rule names its regime, its FSC services, and its fields. It also states its own reasons to deny. | policies/gbo/rules |
| Contract acceptance policy | Decides whether a provider's FSC Manager signs a submitted contract. It accepts grant types service connection and delegated service connection. It applies eight ordered refusal codes, from NOT_AN_AUTOSIGN_REQUEST to DELEGATION_NOT_REGISTERED. It keeps a missing register (ADMISSION_REGISTER_UNAVAILABLE) apart from a missing entry (PARTY_NOT_IN_REGISTRY). |
policies/fsc/autosign.rego |
| Admission register (toelatingsregister) | Holds the admitted participants: bronhouders, GBO system participants (EDI issuer, PDP, ASIP), and consumers. Each entry has active and allowed_source_peer_ids. The register also holds integrator mandates (integrations) and serves them as acts_for. OpenFTV pulls it every 5 s over a network that only the PDP shares. The register deactivates an entry and never deletes it. It keeps no change log (REQ-123). It is a SQLite database with an admin UI. |
services/dvtp-onboarding-register, PIP pull |
The PID regime has no positive evidence of its own. A request without a consent token enters it (F-70). For this reason, the PID rules admit only named actors. These actor lists must stay apart from the consent consumers. The five rules grant these fields:
| Rule | Regime | Services | What it grants | Own deny reasons | File |
|---|---|---|---|---|---|
| DVT0001 | consent | bri |
Income declaration fields under consent | consent codes, SCOPE_NOT_ALLOWED, CONSTRAINT_MISMATCH, YEAR_NOT_COVERED |
dvt0001.rego |
| LVG0001 | consent | lvg |
Ownership check at LVG, scope lvg:vbo:eigendom |
consent codes, SCOPE_NOT_ALLOWED, CONSTRAINT_MISMATCH |
lvg0001.rego |
| EUD0001 | pid | bri |
Income statement (inkomensverklaring) to the wallet, years 2024 and 2025 | PID_NOT_PRESENT, ACTOR_NOT_ALLOWED, YEAR_NOT_ALLOWED |
eud0001.rego |
| EUD0002 | pid | brp |
Death certificate ("akte van overlijden") to the wallet | PID_NOT_PRESENT, ACTOR_NOT_ALLOWED |
eud0002.rego |
| ASI0001 | pid | brp |
Fields of the Personal Records Database (BRP) behind the ASI attributes, for the ASIP peer …1200 only |
PID_NOT_PRESENT, ACTOR_NOT_ALLOWED |
asi0001.rego |
The EUDI rules share one actor list, designated_issuers
(eudi_helper.rego).
The consent rules share the consent checks in
dvtp_helper.rego.
Rule shape¶
A rule is plain Rego. It declares what it covers and states its reasons to
deny. If a rule has no deny reason for a field, it grants that field.
box2Inkomen and box3Inkomen are not in covers_fields, so the engine
denies them under consent. This excerpt from
DVT0001
shows the shape of a rule:
rule_id := "DVT0001"
regime := "consent"
services := {"bri"}
covers_types := {"Bedrag"}
covers_fields := {
"Query.ingeschrevenPersoon",
"IngeschrevenPersoon.heeftBelastingjaarAangifte",
"AangifteIH.verzamelinkomen",
"AangifteIH.box1Inkomen",
# ... scalar fields of the declaration
}
deny contains helper.consent_invalid_reason if not helper.consent_verified
deny contains "CONSENT_WITHDRAWN" if not helper.consent_active
deny contains "CONSENT_ACTOR_MISMATCH" if not helper.consent_given_to_caller
deny contains "SCOPE_NOT_ALLOWED" if not income_scope_granted
# The subject must be the identity placeholder, not a literal BSN.
deny_field[i] contains "CONSTRAINT_MISMATCH" if {
some i, f in lib.fields
lib.key(f) == "Query.ingeschrevenPersoon"
not helper.names_consent_citizen(f, "bsn", {lib.identity_placeholder})
}
Authorization explains regime selection, rule binding, and the two layers of reason codes. The decisions are in ADR-0002, ADR-0009, ADR-0013, ADR-0015, and ADR-0031.
Level 2: consent chain¶
The consent chain has the following components:
| Component | Responsibility | Implementation |
|---|---|---|
| Consent portal (toestemmingsportaal) frontend | The citizen UI. It has a DigiD login (mock), one toggle per data set, a consent overview, and revocation. It shows only the requested data sets and refuses a data set that it cannot describe. It checks the return origin against an allowlist. It returns the consent proof in the URL fragment, or status=denied. |
toestemmingsportaal-frontend |
| Consent portal backend | Keeps the BSN away from the register and the consumer. The type system enforces this boundary, and the backend never sends a BSN to the register. It derives subject_ref as a keyed HMAC of the BSN, with a key version. It activates a citizen at BSNk once and stores PI and PP in the register. At each consent, it reads the BSN authorization list and has BSNk transform the values. Every configured source gets a VP. Only a source on the list also gets a VI. |
services/consent-portal-backend, portal.go, subjectref.go |
| Consent register (toestemmingsregister) | Stores consent records: subject_ref, consumer, scopes, scope entries per source, purpose (use_case), validity, and status. It also stores the portal's PI and PP per subject_ref (route D, ADR-0040). It stores no BSN, no VI, and no VP. It signs the consent proof and serves the JWKS. A separate listener serves only the status, published as the FSC service consent-status. By default, the register keeps its data in memory. If it uses Postgres, it requires a persistent signing key. |
services/consent-register, status.go, polymorphic.go |
| Consent proof | ES256 JWT, type gbo-consent+jwt. Claims: consent_id, encrypted_subject, scopes, scope_entries, dienstverlener_oin, valid_until (equal to exp), iss, aud, iat, nbf, jti. encrypted_subject holds a pseudonym per source OIN. For a source on the BSN authorization list, it also holds an identity. Each value has a key_set_version. dienstverlener_oin holds the consumer's FSC Peer ID (Q-68). The PDP denies a token in the old form with pi. |
consent_token.go |
The consumer holds no citizen identifier. Its query names the subject with
the placeholder consent:identity or consent:pseudonym. The source
gateway replaces the placeholder after the allow decision (see the next
section).
Consent explains the record and its lifecycle.
Pseudonymization explains the values. The
decision is ADR-0032.
Level 2: bronhouder¶
The bronhouder runs the following components:
| Component | Responsibility | Implementation |
|---|---|---|
| GraphQL source API | The query endpoint of the source. Sources identify a citizen by BSN. The demo has three sources. Service bri is a Belastingdienst profile with income declarations per year. Service brp is a BRP profile with the personal record (persoonslijst) and the death certificate view. Service lvg is an LVG mock for the LVG pilot (vbo(bsn, vboId) { vboId }). |
graphql-server, brp-graphql-server, lvg-graphql-server |
| Source gateway (bron-sidecar) | Source-owned gateway between the Inway and the API, one instance per source. Its behavior depends on the presence of a consent token. With a token, it takes the value for its own OIN from encrypted_subject and has the decryption component read it. It checks that the value names its own OIN. Then it replaces the placeholder. It refuses a literal subject under a token. Without a token, it passes the request through. It accepts only the transport shape the PDP judged: POST, one path, no query string. |
services/bron-sidecar, substitution, transport.go |
| Decryption component | Reads a VI or VP with the source's own keys. It calls no other service, so there is no BSNk call at query time. The demo uses the bsnk-mock image with only the decryption endpoints. The three demo sources share one instance and one OIN. Production needs one instance per source. |
decryption.go, bsnk-mock README |
| Source metadata endpoint | Serves the source-owned description at /.well-known/gbo over a separate FSC metadata service. |
source_metadata.go, BD document |
Only the source Inways can reach the source gateway. Because of this network isolation, every request at the gateway comes after a PDP decision. For this reason, the gateway does not verify the token again (ADR-0033, F-79). The deployment view shows the networks.
Source metadata document¶
A source describes its own products in one JSON document. It serves the
document at /.well-known/gbo. For a source without an endpoint, an
operator supplies it through the file transport. GBO validates the
document against
gbo-source-metadata-v1
(urn:gov:nl:gbo:schema:gbo-source-metadata:1).
GBO holds no catalog of sources, years, or schemas. This document is the catalog, and the source owns it (ADR-0016). The document format is a proposal for the open PSA decision on S10.
The document has the following fields:
| Field | Content |
|---|---|
schema_version |
1.0 or 1.1. Version 1.1 adds capabilities.asi. |
source_oin, version, issued_at, expires_at |
Envelope. source_oin must match the OIN in the certificate set. version must increase. GBO refuses a validity of less than one hour. |
capabilities.eudi |
version and attestations[], one entry per attestation type. |
type_id, type_version |
Identity of the attestation type. With the source ID they form the VCT path /types/{source-id}/{type-id}/v{n}. |
offers[] |
Products a wallet can request: id, label, description and fixed parameters (for example {"jaar": 2025}). |
graphql |
The source-owned query plan: service_reference (the FSC data service), endpoint, document (exactly one read operation), subject_variable, typed parameters, and result_pointer (RFC 6901). |
mapping_profile |
gbo-simple-v1 (ADR-0017). It applies to the eudi capability only. |
display |
Per language: name, description, a summary with {{claim}} placeholders, and rendering. |
claims |
Per claim: source (one gbo-simple-v1 rule: pointer, datatype, optional optional), label, optional description, and sd (always by default, allowed or never). The claim order is the display order in the wallet. |
capabilities.asi |
version, authentic_source (legal_name, country), a graphql block with one ...Selected fragment, and attributes[]. Each attribute has an identifier, an optional type condition on, and fields built with pointer, join, const or each (ADR-0039). |
GBO generates the SD-JWT VC Type Metadata from display and claims. It
publishes the Type Metadata immutably per version, with vct#integrity.
A change to claims or display therefore needs a new type_version. The
schema has no unit. The reconciler refuses reserved claim names (vct,
iss, status, cnf) and nested objects
(REQ-78).
The adapter puts the BSN from the PID into $bsn and the year of the offer
into $jaar. It reads the result at result_pointer and copies each
source.pointer into its claim. This shortened excerpt from the
Belastingdienst document
(version 2.4.0) shows how the query plan and the claims relate:
{
"type_id": "inkomensverklaring",
"type_version": "2.4",
"offers": [
{"id": "inkomensverklaring_2025", "label": "Inkomensverklaring 2025",
"description": "Inkomensverklaring over belastingjaar 2025",
"parameters": {"jaar": 2025}}
],
"graphql": {
"service_reference": "bri",
"endpoint": "/graphql",
"document": "query Inkomensverklaring($bsn: BSN!, $jaar: Int!) { ... }",
"subject_variable": "bsn",
"parameters": {"jaar": {"type": "integer", "required": true}},
"result_pointer": "/data/ingeschrevenPersoon/heeftBelastingjaarAangifte"
},
"mapping_profile": "gbo-simple-v1",
"display": {
"nl-NL": {"name": "Inkomensverklaring",
"summary": "€{{verzamelinkomen}} ({{belastingjaar}})"}
},
"claims": {
"belastingjaar": {"source": {"pointer": "/belastingjaar", "datatype": "gYear"},
"label": {"nl-NL": "Belastingjaar"}},
"verzamelinkomen": {"source": {"pointer": "/verzamelinkomen/waarde", "datatype": "integer"},
"label": {"nl-NL": "Verzamelinkomen"}}
}
}
The
BRP document
(version 2.6.0, schema 1.1) also publishes the asi capability. This
excerpt shows two of its attributes:
{
"version": "1.0",
"authentic_source": {"legal_name": "Basisregistratie Personen (mock)", "country": "NL"},
"graphql": {
"service_reference": "brp",
"endpoint": "/graphql",
"document": "query Asi($bsn: BSN!) { ingeschrevenPersoon(bsn: $bsn) { ...Selected } }",
"subject_variable": "bsn",
"result_pointer": "/data/ingeschrevenPersoon"
},
"attributes": [
{"identifier": "https://gbo.example/attributes/family_name/1",
"fields": {"family_name": {"join": ["/voorvoegsel", "/geslachtsnaam"], "separator": " "}}},
{"identifier": "https://gbo.example/attributes/nationality/1",
"fields": {"nationalities": {"each": "/heeftNationaliteit", "pointer": "/nationaliteit"}}}
]
}
By design, the document leaves out three things:
- The GraphQL schema. The PDP keeps its own bundled copy. It authorizes the source's query plan field by field, like any other query.
- Access decisions. The EUD and ASI rules decide which fields a channel can receive. The mapping of the source has no part in that decision.
- The
value_schemaof an ASI attribute. GBO keeps it per attribute identifier in schemas/asi-attributes.json until an EU catalog of attributes exists.
Provenance rests on the authenticated FSC transport and the OIN in the certificate set. The source does not sign the document. The document also has no reference to an LDV read API or a DCAT-AP-NL entry (F-57, AV-07).
Source configuration on the GBO side¶
The GBO configuration per source is small: one YAML file, named after the
source_id. The file names where the description comes from and how GBO
reaches the data. This is
sources/configured/belastingdienst.yaml:
metadata_endpoint:
transport: fsc
provider_peer_id: "99999999900000000200"
service_reference: gbo-metadata-bd
An optional data_access block (transport, provider_peer_id) gives the
data leg a different transport than the metadata leg. The file transport
is an exception for a source without its own endpoint. In that case, the
operator takes over the description. GBO does not record the agreement of
the bronhouder
(F-71, AV-08).
The legal identity of the source comes from the certificate set with the
same source_id. The OIN comes from the certificate's serialNumber, and
the name comes from its O. GBO refuses a configured value that differs
(REQ-67). The configuration holds
the FSC Peer ID apart from the OIN. The data service and the grant hash
come from the source document and the FSC contracts in force. They never
come from this file.
A valid FSC contract activates nothing. A source becomes active only after
three steps: it is configured, its candidate is valid, and an operator
promotes a release (REQ-65). If
a precondition is missing, the reconciler reports
METADATA_CONTRACT_NOT_FOUND, DATA_CONTRACT_NOT_FOUND, or
FSC_MANAGER_UNAVAILABLE. Source onboarding
and admission and activation explain the lifecycle.
In demo v0.9.0, the RvIG source
(rvig.yaml)
uses the Belastingdienst peer …0200 and the OIN of the Belastingdienst.
This is a demo shortcut. The demo therefore does not show one OIN per
bronhouder.
Source schema and the PDP copy¶
The source owns its schema. The PDP holds a directive-free copy per FSC service to resolve parent types. The year selector is in the query, so the PDP can enforce consent per year. This excerpt from the BD copy shows the DvTP part:
type IngeschrevenPersoon {
bsn: BSN!
heeftBelastingjaarAangifte(belastingjaren: [Int!]): [BelastingjaarAangifte!]
}
type Query {
ingeschrevenPersoon(bsn: BSN!): IngeschrevenPersoon
}
The BRP source adds a flat view for the death certificate. The selection of the deceased partner from the marriage history is a security-sensitive invariant. The source holds this selection and its tests. The policy does not contain it (REQ-57, F-47). For the view, see the BRP copy and EUD0002:
type Query {
ingeschrevenPersoon(bsn: BSN!): IngeschrevenPersoon
akteVanOverlijden(bsn: BSN!): [AkteVanOverlijden!]!
}
Level 2: FSC group¶
The FSC group fsc-demo runs OpenFSC v2.4.0. The
deployment view lists the peers. The group has
the following components:
| Component | Responsibility | Implementation |
|---|---|---|
| Directory | The registry of the group. It signs service publications automatically. It signs nothing else. | fsc-infra/docker-compose.yml |
| Manager (per organization) | Issues access tokens for contracted connections. The token carries the signed service_name, which binds the PDP rules to a service. The provider Managers of BD, BRP, and the consent register decide contract acceptance through the PDP (AuthZEN autosign). |
bd-manager |
| Inway (per provider) | bd-inway and brp-inway are the PEP. Their AuthZEN plugin sends every request with its body to the PDP before the request reaches the source gateway. cr-inway has no AuthZEN hook. Behind cr-inway, the contract authorizes consent-status, and the register reads the calling peer from the Fsc-Authorization token. |
bd-inway, cr-inway |
| Outway (per consumer) | Opens mTLS connections to Inways under a grant hash. Grant links give each contracted service a path: /bri, /brp, /lvg, /consent-status. |
seed-bri-contract.sh |
| Grants | Service-connection grants carry no properties. The demo removed flow in PR #363 and subject_id_type in #473. The regime follows from the evidence on the request (ADR-0031). |
seed-bri-contract.sh |
| Delegated grants | An integrator connects under a DelegatedServiceConnectionGrant. The integrator, the represented consumer and the provider sign it. The provider's Manager admits it only for a registered integration (ADR-0034). |
seed-bri-delegation-int.sh |
| Consent-status service | The register peer …0700 publishes consent-status. The PDP peer …0800 connects to it with grant link /consent-status. The register's Manager signs the connection only for peers in the admission register. |
fsc-seed-consent-status |
| ASIP peer | The ASIP peer …1200 has an Outway only. It holds a contract for brp on …0200 with grant link /brp. |
fsc-seed-asip |
| Txlog API (per peer) | Records each transport transaction (FSC-Logging). It is the authoritative transport record. The ADL records the decisions, and the LDV logbooks record the processings. | fsc-infra/docker-compose.yml |
Level 2: EUDI chain¶
The EUDI chain has the following components:
| Component | Responsibility | Implementation |
|---|---|---|
| EUDI adapter | The bridge between the issuance server and the FSC chain. The issuance server calls POST /attestations/{sourceID}/{typeID}. The adapter takes the BSN from the disclosed PID and builds the query from the source's published graphql block. It calls the source through the EDI Outway with Fsc-Grant-Hash and traceparent. It projects the answer into claims through gbo-simple-v1. It writes two LDV records per issuance: one for the BSN read and one for the assembly of the attestation. It fetches source data per request and stores none (REQ-11). |
services/eudi-adapter, routes, ldv_issuance.go |
| Adapter callback and public endpoints | The callback route has no caller authentication (REQ-45, #133). The same listener serves GET /eudi-offers.json (the offer catalog for the frontends) and GET /types/... (Type Metadata). |
source_release_runtime.go |
| Issuance error contract | The adapter returns 400 for offer parameters outside the offer, or for a PID without a BSN. It returns 404 if there is no result. If a result does not fit the claims, it returns 502 with GBO_SIMPLE_PATH_MISSING, GBO_SIMPLE_TYPE_MISMATCH, GBO_SIMPLE_RESULT_TYPE, or GBO_SIMPLE_RESULT_AMBIGUOUS. It never issues a partial attestation (REQ-116). |
internal/gbosimplev1 |
| Issuance server | The nl-wallet issuance server (v0.5.0). It does OpenID4VP disclosure and OpenID4VCI issuance (dc+sd-jwt). It verifies the PID disclosure against issuer_trust_anchors. That list also holds non-PID anchors: the development issuer CA and the issuer CA of every source. An attestation is valid for 365 days. Each type has one status list (REQ-77). A second use case, qtsp-attestation, issues the reference QTSP's test statement. An init step writes the server configuration from the active release. |
services/eudi-issuance-server, issuance_config.go, qtsp_issuance.go |
| Source reconciler | Reconciles the configured sources to a desired state. It discovers the FSC contracts. It fetches the source document over FSC, or reads the file for the file transport. It validates the document, and it validates the asi capability against the attribute catalog. Then it writes candidates and statuses. Promotion creates an immutable release and activates it. The --auto-promote flag is for the demo only. |
reconcile_command.go, source_configuration.go |
| Source registry | A shared Go module with a PostgreSQL store. The store holds candidates, statuses, immutable releases (content-addressed by SHA-256), and one active-release pointer. It has a writer role and a read-only role. The readers are the adapter, the issuance init step, and the ASIP. The states are pending, active, stale, blocked, and rollout_required. A source is fresh for 15 min and then has a stale grace period of 1 h (REQ-66). |
services/source-registry, source_metadata_cache.go |
| Release runtime | Polls the active-release pointer in the adapter. If the release changes, it rebuilds the attestation bindings. If an error occurs, it keeps the previous snapshot. | source_release_runtime.go |
| gbo-simple-v1 profile | A minimal mapping profile that fails closed. It maps one source object to flat claims through RFC 6901 pointers. It allows only scalar data types, with no conversions or expressions. | profile, docs/gbo-simple-v1.md |
| Certificate set (per source) | Issuer, reader, and status-list leaf certificates per source. Sources never share them. A separate privileged command mints the leaves under pre-provisioned CAs. It never mints a CA. Missing or incomplete material blocks only that source (CERTIFICATE_SET_NOT_FOUND, CERTIFICATE_SET_INVALID) (REQ-58). |
development_ca_provider.go |
The issuance route runs under the PID regime: it carries no consent token. The BSN from the PID is in the PDP input and in both decision logs (REQ-60, ADR-0043). The decision is ADR-0018.
Level 2: ASI chain¶
The ASI chain implements the Authentic Source Interface of ETSI TS 119 478, clause 6.1 (ADR-0037). The chain is demo grade. It uses a stub register, development certificates, and a DigiD mock. It has the following components:
| Component | Responsibility | Implementation |
|---|---|---|
| ASIP | Offers clause 6.1.1 verify and 6.1.2 retrieve per data service at /<ds>/authsrc-api. It first verifies the DPoP-bound token. It takes the BSN only from the token (REQ-87). It derives one GraphQL query per request from the source's asi description and sends it through its own Outway (REQ-92). For each attribute, it answers Match, NoMatch, MatchWithVariation, or Unknown. It never returns a source value (REQ-85). It seals every 200 response with a detached JWS in X-JWS-Signature (REQ-83). |
services/asi-provider, seal.go |
| Data services | One per authentic source, each with its own base URL, contract, discovery records and token audience (ADR-0038). The demo has one: brp, source rvig. |
config/dataservices.json |
Source description (asi capability) |
The source maps its fields onto GBO attribute identifiers. The ASIP reads the active release every 5 s with the reader role. If no current description is available, the data service answers 503. | services/asi-mapping, services/source-registry, schemas/asi-attributes.json |
| Authorization server | Clause 6.1.3. It registers a QTSP with RFC 7591 and a software statement sealed by the QTSP certificate. It verifies the chain and the entitlement QEAA_Provider in the register (REQ-88). It checks the entitlement again every hour (REQ-89). It runs the code flow with PKCE S256 and private_key_jwt. It issues DPoP-bound RFC 9068 tokens (10 min) for exactly one resource (REQ-95). The request page shows who asks for which data at which source (REQ-91). The login is the approval (REQ-90). The token carries the BSN and acr/amr (REQ-86). |
services/asi-authorization-server |
| Register stub | Stands in for the national register of relying parties. The server reads it at every lookup. | testdata/register.json |
| Wallet verifier | The nl-wallet verification_server (v0.5.0). It runs the OpenID4VP PID disclosure for the authorization server (use case asi-login, BSN required, REQ-102). As a demo shortcut it also serves the reference QTSP (qtsp-login, no BSN). |
services/asi-wallet-verifier |
| DigiD mock (ASI) | A stub means of identification. It accepts nine digits and returns only a BSN. | internal/digidmock |
| Rule ASI0001 | Grants the ASIP peer the BRP fields behind the attributes, on brp only. It does not verify the token or the QTSP. |
asi0001.rego |
| Discover Interface (stub) | External role. It answers retrieve from the ASIP's dataService records (REQ-99). For search, it returns 501. |
services/asi-discovery-stub |
| Reference QTSP | External role, demo scaffolding. It discovers, registers, gets a token, verifies or retrieves, and accepts a result only with a valid seal. Then it issues a test statement, marked "not qualified", to the wallet. | services/qtsp-reference |
| Conformance matrix | Gives each ETSI requirement ID a status and named tests. If an ID or a test is missing, a CI job fails. | conformance |
Trust boundary¶
The QTSP is not an FSC participant (F-87). It is an OAuth client of the authorization server. Its certificate and its register entitlement admit it. It has no FSC contract.
The ASIP is an FSC consumer peer of its own. For the PDP, the actor is the ASIP, never the QTSP or the citizen. The ASIP passes nothing about the QTSP into the chain. In demo v0.9.0, the ASIP writes no LDV records.
ASI verification shows the flow. The ASI requirements give the profile per ETSI clause.
Level 2: logging and audit¶
GBO keeps three authoritative records, one per Logius standard (ADR-0036, ADR-0026). One W3C trace ID joins them. The records store the FSC transaction ID next to it (ADR-0035). Logging explains the model. The logging and audit chain has the following components:
| Component | Responsibility | Implementation |
|---|---|---|
| Authorization Decision Log (ADL) | The authoritative record of each PDP decision, contract decisions included. OpenFTV writes it to Postgres ftv_adl. Readers get a SELECT-only role. It holds the request and {allow, reason}. It has no policy version (REQ-59) and no consent-status answer (F-72). For decisions behind the Inway, it has no trace ID and no transaction ID (#369). |
docker-compose.yml, adl-reader.sql |
| LDV logbook | Demo stand-in for the logbook of each controller (Verantwoordelijke), one instance per controller. It accepts one record per processing (POST /logboek/records) and serves the read extension (POST /data-processing-operations). It keeps a versioned stand-in register of processing activities. It refuses any record with a run of nine digits that passes the BSN check digit test (elfproef). It uses SQLite with synchronous = FULL. Write and read take separate tokens. |
services/ldv-logboek |
| LDV client | A shared Go module. In production, GBO ships this module to chain participants. It does not ship the logbook. The client takes the trace from traceparent. It writes records and waits for the logbook to acknowledge them. It derives the subject in the controller's own pseudonym space. It sets nextLogbookId on outbound calls. A local durable outbox stores each record before delivery. As a result, a logbook outage delays records but does not fail requests. |
services/ldv-client, outbox.go |
| FSC txlog | Authoritative transport record per peer (see FSC group). | fsc-infra/docker-compose.yml |
| Observability stack | OpenTelemetry collector, Jaeger, Promtail, Loki and Grafana. The engine's console decision log goes to Loki. These stores are for debugging only. They have no audit function. | otel/collector-config.yaml, OBSERVABILITY.md |
The demo runs seven logbooks. Each controller logs its own processings (REQ-72):
| Logbook | Controller | Writers |
|---|---|---|
logboek-bd |
Belastingdienst | graphql-server, bron-sidecar |
logboek-brp |
RvIG | brp-graphql-server, brp-sidecar |
logboek-lvg |
LVG | lvg-graphql-server, lvg-sidecar |
logboek-toestemming |
GBO consent facility (toestemmingsvoorziening) | consent-register, consent-portal-backend |
logboek-eudi-adapter |
GBO issuance facility | eudi-adapter |
logboek-afnemer |
Hypotheek-BV (consumer) | dienstverlener-backend, integrator-backend |
logboek-ir |
Installatie Register (consumer) | ir-backend |
The register logs each status lookup with the verified FSC peer of the PDP as processor (REQ-76). The ASIP writes no records. The consumer logbooks follow candidate condition AV-02.
Level 2: shared services and mocks¶
The demo has the following shared services and mocks:
| Component | Responsibility | Implementation |
|---|---|---|
| BSNk PP mock | Follows the BSNk interface as JSON instead of SOAP: /v2/activate, /v2/transform, /v2/provide-dv-keys, /v2/bsn-authorisation-list, and /v2/scheme-keys. It enforces the rules of BSNk. A PI or PP belongs to its requester. A transformation has at most four parties. Only a party on the BSN authorization list gets an identity. It does not encrypt: anyone who holds a value can read it. Only the portal backend calls it, at consent time. |
services/bsnk-mock, README |
| DigiD mocks | The consent portal has a DigiD login page. The user types a test BSN, and the portal backend signs a portal token with it. The ASI authorization server has its own stub (see the ASI chain). Demo scaffolding. | DigiDAuth.tsx, portalhttp |
| Consumer backend and frontend | Mock consumer Hypotheek-BV (peer …0300). It starts the consent flow, receives the proof and sends the DvTP query with the placeholder. QUERY_KIND selects the source (bd or lvg) and its placeholder. It writes its own LDV records. Demo scaffolding. |
services/dienstverlener-backend, dienstverlener-mock |
| Installatie Register (LVG pilot) | keyper-mock is the IR/Keyper frontend. It has the installer request, the owner approval, and the installer data. ir-backend is the consumer backend with QUERY_KIND=lvg on its own peer …1000. Under one consent, IR calls LVG again at each installer access. IR holds the second consent (citizen to installer). GBO does not hold it. Demo scaffolding. |
keyper-mock, docs/lvg-ir.md |
| Integrator backend | Consumer backend on the integrator peer …1100. It calls bri for Hypotheek-BV under a delegated grant. It is opt-in: run make demo-integrator. Demo scaffolding. |
docs/integrator.md |
| Developer portal and backend | A cockpit for engineers. It shows the live flow from the trace event stream and a scenario library. It shows the ADL decision next to the engine detail, the LDV chain, the FSC txlogs, and an ASI tab. Rule metadata comes from a generated rules.json. Demo scaffolding. |
developer-portal, services/dev-portal-backend |
| OpenFTV Manager, UI, Keycloak | Opt-in policy administration point. It keeps the policies in Postgres and makes a tagged bundle that the PDP pulls. For more information, see policy lifecycle. | services/openftv-manager, services/openftv-manager-ui |
| Landing page | Public explainer with links to the demo UIs. Demo scaffolding. | landing-page |
The demo removed the old sector PIP (#378). Admission runs through the admission register and the contract decision. The decision in ADR-0022 is deferred.
Traceability¶
- Requirements: REQ-04, REQ-11, REQ-17, REQ-21, REQ-42, REQ-45, REQ-48, REQ-58, REQ-61, REQ-62, REQ-64, REQ-65, REQ-66, REQ-67, REQ-68, REQ-69, REQ-71, REQ-72, REQ-74, REQ-79, REQ-80, REQ-81, REQ-83, REQ-86 to REQ-92, REQ-97, REQ-106, REQ-116, REQ-123
- Findings: F-47 (open), F-57 (open), F-70 (open), F-71 (open), F-72 (open), F-79 (open), F-87 (open), F-90 (resolved in demo)
- Open questions: Q-34, Q-38, Q-68
- Demo increments: SL-11, SL-16, SL-22, SL-28, SL-44, SL-45, SL-46, SL-47, SL-50, SL-51