Source onboarding¶
A GBO component must know, for each source, which data service to call, with which query, and what to do with the answer. That knowledge comes from the source itself, and GBO takes a source into use in a fixed set of steps. The decisions are ADR-0016 (mechanism), ADR-0017 (mapping profile for attestations), and ADR-0039 (attributes for the ASIP). For the requirements, see Source metadata and activation. For the components, see the building block view. For the step-by-step flow, see Admission and activation.
The behavior on this page is the behavior of gbo-demo v0.9.0 (docs/source-onboarding.md, docs/source-metadata-cache.md).
Why the source owns its description¶
The bronhouder keeps control of its data (P-10). It can add services through its source-specific GBO configuration (S07). Central GBO facilities read the part of this configuration that they need. They read it in machine-readable form, validated and versioned (S10).
As a result, a GBO component takes the service description of a source only from what the source publishes itself (REQ-64, F-57):
- The component does not keep the description, add to it, or carry it per source in code.
- A new service or attestation type needs no code change in GBO.
- The source does not publish its GraphQL schema. It publishes the queries and mappings that it takes responsibility for.
Before this mechanism, every new source, attestation type, or tax year was a change in GBO code or in a GBO catalog file. With this mechanism, the adapter parameters come from the source (Q-19). In GO terms, the document is the service catalog (dienstencatalogus) that the bronhouder manages. The validation rules of the reconciler are the common profile.
The mechanism (publish, validate, activate, roll back) is a GBO design
decision. The document format gbo-source-metadata-v1 is the proposal of
the demo for the open PSA S10 decision. It is not a final interface
agreement.
Three inputs together describe a source. Each input has a different owner:
| Item | Owner | Where |
|---|---|---|
source_id, transport, metadata location, FSC Peer ID |
GBO operator | source configuration file (sources/configured/<source_id>.yaml) |
| Legal OIN, organization name, issuer name and logo | bronhouder, through its certificate | certificate set under the key source_id (Identity and trust) |
| Access to the metadata and data services | bronhouder and GBO, by agreement | FSC contracts and grant hashes |
| Data service, query, parameters, offers, display, claims, attributes | bronhouder | the source document at /.well-known/gbo |
Shape of an ASI attribute value (value_schema) |
GBO, until the EU catalog of attributes exists | schemas/asi-attributes.json |
| SD-JWT VC Type Metadata | GBO generates it from the document | source registry |
| Candidates, states, releases, active pointer | GBO | source registry (PostgreSQL) |
Lifecycle¶
Onboarding goes through nine steps:
- Configure: the operator adds one file per source in
sources/configured/. The file name is the stablesource_id. The file names the transport, the location of the metadata, and the FSC Peer ID of the provider (source_configuration.go#L16-L28). It does not name the OIN or the organization name. These come from the certificate set. - Discover the contracts: for the
fsctransport, the reconciler looks in the existing FSC contracts. There it finds the metadata contract and the data contract of the configured peer. It does not discover sources: a contract without a source file activates nothing. - Fetch or read: only the reconciler fetches source metadata. It uses
ETagandIf-None-Match. A304only extends freshness. The reconciler validates every200in full. The runtime never fetches metadata. - Validate: the reconciler validates schema, origin, version, validity
window, query, mapping, offers, claims, and the binding to the data
contract. The
source_oinin the document must equal the OIN in the certificate set (source-registry/onboarding/service.go#L403-L413). - Candidate: a valid result becomes a complete candidate with state
rollout_required. This candidate is not active. An invalid result does not replace the last complete candidate. - Promote: an operator promotes a complete candidate set. In the demo,
the reconciler can do this itself (
--auto-promote). Promotion is a separate step (reconcile_command.go#L134-L151). - Immutable release: promotion writes one release from the complete
candidate set. The SHA-256 of the content identifies the release. A
release never changes
(
postgresregistry/store.go#L300-L390). - Active pointer: in the same database transaction, promotion sets the
singleton
active_release_id. If a promotion fails, the previous release stays active. - Runtime reload: the readers poll the active pointer with a read-only role. They build the new release in memory and swap it atomically. The nl-wallet issuance server reads its configuration only at startup. So a new release needs a controlled restart of that server.
Four readers use the active release:
| Reader | What it derives | Reload |
|---|---|---|
| EUDI adapter | source snapshot (source, query, mapping, FSC grant, validity window), Type Metadata, offer catalog /eudi-offers.json |
live, by polling |
| Issuance init container | issuance_server.toml: offers, disclosure, certificates, trust anchors |
at a restart of the issuance server |
| ASIP | per data service: query, attribute mapping, legal name of the authentic source, discovery records | live, every 5 seconds (asi-provider/main.go#L101) |
| Developer portal and landing page | the offers that a citizen can start, from the adapter | live |
The source registry is a shared module,
services/source-registry
(onboarding, postgresregistry, asisnapshot). The reconciler writes to
it with the writer role. The adapter, the issuance init container, and the
ASIP read it with the reader role. The reconciler has no Kubernetes API
rights. It reads certificates and contracts and writes only to the
registry.
Transports¶
A source has two legs: the metadata leg and the data leg. Each leg selects
its own transport
(service.go#L18-L28):
| Transport | Metadata leg | Data leg | Authenticated |
|---|---|---|---|
fsc |
fixed path /.well-known/gbo through a separate FSC metadata service |
the data service named in the document, under its contract | yes |
unsecured |
an absolute HTTP or HTTPS URL, with no FSC headers and no redirects | an absolute URL | no |
file |
a document in the metadata directory of the operator (--metadata-dir). The path cannot leave that directory |
not possible: a file cannot answer a query | no |
Five rules apply when the two legs combine transports:
- If no
data_accessblock is present, the data leg uses the metadata transport. fscmetadata withunsecureddata is refused.filemetadata needs an explicitdata_access.- If both legs use
fsc, the two services must differ. provider_peer_idis on the leg that speaks FSC, never on both.
The registry reports both legs separately, as transport_authenticated
and data_transport_authenticated
(service.go#L56-L66).
A single flag hides an FSC-authenticated data call whose description came
over an unauthenticated channel. The party that controls the document
decides the query, the mapping, and the wallet claims.
The intended form is fsc on both legs. The other combinations are for
an onboarding phase:
-
fileis for a source that publishes no/.well-known/gbo. The operator then keeps the description of the product of another party. This is an exception under D-13. The transport flag makes the exception visible, andexpires_atmakes it temporary.The exception is admissible only with recorded agreement of the bronhouder (AV-08, F-71). The demo does not record this agreement. The need for agreement is only an instruction in
source-metadata/README.md. -
unsecuredis for local demonstrations. Only the local compose file mounts that source (sources/local-demo/). Deployments use onlysources/configured/.
Four bounds stay in place with every transport:
- The Peer ID comes from the GBO configuration.
- The grant hash pins the contract.
- The reconciler compares the OIN with the certificate.
- Promotion is a separate step.
The trust model for each transport is an open point in demo issue #221.
Contract and activation¶
A valid FSC contract does not take a source into use. It also does not put the offer of the source in the wallet (REQ-65, F-58). Transport and mandate are two decisions with two owners. If a contract alone activates a source, admission becomes automatic again through a back door (F-55). Each situation has a fixed result:
| Situation | Result |
|---|---|
| Valid metadata contract, no source file | nothing happens |
| Source file, no metadata contract | source blocked: METADATA_CONTRACT_NOT_FOUND |
| Source file, no contract for the data service | source blocked: DATA_CONTRACT_NOT_FOUND |
| FSC Manager unreachable | source blocked: FSC_MANAGER_UNAVAILABLE |
| Validated candidate without promotion | not active |
The reason codes are in
service.go#L141-L152.
Automatic promotion is off by default
(reconcile_command.go#L197).
Compose and the Helm example turn it on for the demo. Then the source file
is the management action. Demo v0.9.0 does not build two things: control
over who can do that action, and a record of the admission decision
(PSA B-09 in the
decision register,
REQ-123).
Versions, freshness and isolation¶
Source metadata fails closed for each source and ages within fixed bounds. An operator can roll it back (REQ-66). The mechanism has five parts:
- Freshness: a successful check is fresh for at most 15 minutes. After
that, it is stale for at most one hour. At acceptance, a document must
be valid for at least one more hour
(
source_metadata_cache.go#L10-L14). The issuance route showsX-GBO-Metadata-Cache: fresh|stale. - States per source: each source has one of five application states.
Kubernetes conditions are a separate thing.
pending: the check started.rollout_required: a complete candidate waits.active: the source is in use.stale: the refresh fails, and the previous snapshot is within grace.blocked: no usable snapshot exists, or the grace is over.
- Per-source refusal: if a source is past its grace, only the requests for that source get HTTP 503.
- Version rule: activation refuses a lower version than the last
validated candidate. It also refuses the same version with other
content
(
source_registry_backend.go#L163-L175). The rule has no test (#610). - Rollback: an operator activates a stored release with
activate-source-release --release-id. The command refuses a release that the adapter cannot load. An auto-promoter respects the rollback until new candidate content arrives.
Demo v0.9.0 has two deviations from the isolation rule (#606):
- Promotion needs a valid version of every configured source
(
registry.go#L133-L158). The aim of this design is that no source leaves the wallet offer without notice. As a side effect, one broken source freezes new releases for all sources. - The offer list
/eudi-offers.jsontakes the shortest grace of all sources (source_release_runtime.go#L95-L100). So the list fails for all sources at the same time.
Source releases have the lifecycle that policy bundles lack: validation before activation, bounded staleness, atomic promotion, and rollback (F-59). For the policy side, see Policy lifecycle and ADR-0021.
A certificate set per source¶
Every source has its own certificate set for issuance: issuer, reader (access), and status-list certificates. GBO never shares a set between sources and never substitutes one (REQ-58). The issuer name and the purpose that the wallet shows are bound to the signing and reader certificates. A fallback to the set of another source made the wallet show the wrong issuer (F-50).
If material is missing, onboarding fails hard for that source only:
| Situation | Result |
|---|---|
No set for the source_id |
source blocked: CERTIFICATE_SET_NOT_FOUND |
| Set without OIN or organization name | source blocked: CERTIFICATE_SET_INVALID |
| Mounted material differs from the active release | GBO generates no issuance configuration (issuance_release.go#L113-L116) |
| Two sources behind one FSC peer | each source has its own set |
The reconciler gets only public leaf certificates and public CA certificates. Only the issuance init container gets the private keys of the leaf certificates. Provisioning is a separate privileged command. For where these certificates come from and how they map to EUDI trust roles, see Identity and trust.
The source document¶
The document is JSON, validated against
gbo-source-metadata-v1.schema.json.
The format is closed: validation refuses unknown fields. The envelope has
these fields:
| Field | Meaning |
|---|---|
schema_version |
1.0 or 1.1. Version 1.1 adds the asi capability (#L17-L23) |
source_oin |
20-digit OIN. It must equal the OIN in the certificate set |
version |
Numeric version. One version identifies one content |
issued_at, expires_at |
Validity window. At acceptance, at least one hour must remain |
capabilities.eudi |
attestations for EUDI issuance |
capabilities.asi |
attributes for verification through the ASIP (1.1 only) |
The eudi capability: claims once, Type Metadata generated¶
Each attestation has type_id, type_version, offers, graphql,
mapping_profile, display, and claims. Four of these fields hold
structured content:
offersare the concrete wallet offers, each with fixed parameters (for examplejaar: 2024). A request with parameters outside the published offers is refused with HTTP 400.graphqlholdsservice_reference,endpoint, one read operation indocument,subject_variable, declaredparameters, andresult_pointer.displayis keyed by language. It holds a name, a description, asummarywith{{claim}}placeholders, and rendering.claimsdefines each claim once. A claim has onegbo-simple-v1rule insource(pointer,datatype, optionaloptional), alabel, an optionaldescription, andsd(alwaysby default,allowed, ornever).
Earlier versions had separate mapping, attribute_schema, and
type_metadata blocks. The format removed them. GBO derives the mapping
from claims. It generates the SD-JWT VC Type Metadata from display and
claims
(type_metadata.go#L64).
Each Type Metadata field has a fixed origin in the document:
| Type Metadata | Derived from |
|---|---|
name, description |
the first language in display |
display[] |
display, with lang from the key |
claims[].path, svg_id |
the claim name |
claims[].display[] |
label and description |
claims[].sd |
sd |
schema.properties |
source.datatype (date becomes a string with format: date) |
schema.required |
every claim without optional: true, plus vct and vct#integrity |
Because the schema comes from the same rules as the projection, a source
cannot publish a schema that differs from what the mapping fills. Five
validation rules apply to the claims
(source_claims.go#L216-L242):
- A
summaryplaceholder must be a claim name. - Validation refuses reserved names (
vct,vct#integrity,iss,status,cnf,iat,nbf,exp,sub,attestation_qualification). - The order of the claims is the order in which the wallet shows them.
- Validation refuses the datatype
numberfor EUDI, because nl-wallet v0.5 cannot show a non-integral value. - Only scalar datatypes pass. GBO does not support nested claims (REQ-78).
The VCT of a type version is /types/{source-id}/{type-id}/v{type-version}.
The value vct#integrity covers the exact bytes that GBO serves at that
URL. A change to claims or display changes those bytes, so the change
needs a new type_version. GBO never deletes published type versions
automatically.
For how an answer of the source becomes an attestation, and for the error codes if the answer does not fit, see ADR-0017 (REQ-116).
The asi capability (schema 1.1)¶
From schema 1.1 on, a source can also describe the attributes that it
offers for verification through the ASIP
(ADR-0039,
Q-38). The asi capability has
three parts:
authentic_source: the legal name and the country. The ASIP uses them in responses and discovery records (REQ-94).graphql: one query that ends in only...Selected, the subject variable, and the result pointer. For each request, the ASIP replaces...Selectedwith the fields of the requested attributes (REQ-92).-
attributes: for each attribute, anidentifier, an optional type conditionon, andfields. Each member of the value has exactly one operation:Operation Meaning pointercopy a field joinwithseparatorconcatenate two or more fields, without empty parts consta fixed value of the member type eachwithpointerfor each element of a list, the field at pointer
The identifier must exist in the attribute catalog of GBO,
schemas/asi-attributes.json.
The catalog gives a value_schema for each identifier. The identifiers
are placeholders until the EU catalog of attributes exists
(Q-31).
The profile gbo-simple-v1 applies only to the eudi capability. The asi
capability composes values, because a source cannot always deliver a
catalog value with a copy. For example, a house number is three BRP
fields. The BRP document shows this composition
(brp-graphql-server/config/gbo-source-metadata.json#L204).
Onboarding applies six rules to asi
(source_registration.go#L396-L406):
- If
asihas an error, onboarding refuses the whole source, also ifeudiis valid. The active release stays. - The
asiquery uses the same data service as the attestations: one document, one data service. - Onboarding also refuses a document with only
asi, because onboarding requires the issuance certificate sets. - If the reconciler cannot read the catalog, it refuses only the sources
with
asi. - A source with
asigets release snapshot version 4.0. A source withoutasistays at 3.0. - Onboarding validates only the form. It does not validate the GraphQL schema of the source, or whether the policy of the source allows the selection. A difference becomes visible at the first request.
If the description is absent, has no asi, or is stale, the ASIP answers
503 for verify, retrieve, and discovery
(REQ-93). If no registry is available,
the ASIP can read a release file (asi-provider write-release-file).
Connection conditions for bronhouders¶
GBO asks three things of a bronhouder, as candidate connection conditions:
- The bronhouder publishes a machine-readable, versioned description for each data service at the fixed location. The description includes the read API of the logbook of the bronhouder (AV-07).
- If the bronhouder has no endpoint of its own, it agrees provably with the description that the operator keeps (AV-08).
- The bronhouder provides a certificate set that carries its OIN and organization name (REQ-67).
Open points¶
Seven points are open:
- The document is not signed. The origin depends on the authenticated transport plus the OIN from the certificate set.
- The description lacks three items: the read API of the logbook of the
source, a reference to the DCAT-AP-NL description, and central
findability (F-57,
F-68,
AV-07). Without the
read API, a caller cannot fill
dpl.read.nextLogbookIdfrom the description. - DvTP does not use the description. The DvTP offer of a source is in the GBO rules. DvTP scope comes from the demand side. EUDI scope comes from the supply side (F-74).
- Traceability of versions: no record shows which versions of the mapping, the source schema, and the attestation schema an attestation used (REQ-124).
- Key custody: the central issuance server holds the private keys of the issuer and status-list certificates of every source (Q-17).
- ASI resource list: in demo v0.9.0, the ASI authorization server takes its data services from configuration. It does not use the descriptions (#592).
- Scale: how many sources the ASIP must connect, and how fast, is open (Q-38, sub-question 1).
Traceability¶
- Requirements: REQ-58, REQ-64, REQ-65, REQ-66, REQ-67, REQ-78, REQ-92, REQ-93, REQ-94, REQ-116, REQ-123, REQ-124, AV-07, AV-08
- Findings: F-50 (closed), F-55 (closed), F-57 (open), F-58 (closed), F-59 (closed), F-68 (open), F-71 (open), F-74 (open)
- Open questions: Q-17 (open), Q-19 (answered), Q-31 (open), Q-38 (partly answered)
- Demo increments: SL-32 (done), SL-46 (done), SL-49 (done)
- Decisions: ADR-0016, ADR-0017, ADR-0039