Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ PORTAL_AUTH_PROVIDER=mock
# Optional local eSignet profile.
SOLMARA_ESIGNET_POSTGRES_PASSWORD=
NIA_ESIGNET_CLIENT_PRIVATE_JWK=
REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD=
REGISTRY_ESIGNET_KYC_TOKEN_SECRET=
REGISTRY_ESIGNET_PSUT_SECRET=
SOLMARA_ESIGNET_V2_KEYSTORE_PASSWORD=
PORTAL_ESIGNET_CLIENT_ID=solmara-portal
PORTAL_ESIGNET_CLIENT_KEY_ID=solmara-portal-key-1
PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64=
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.patch -whitespace
23 changes: 16 additions & 7 deletions .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
description: Tag for Solmara-owned images. Defaults to the workflow commit SHA.
required: false

esignet_native_image:
description: Verified native eSignet v2 provider image, repository@sha256 digest.
required: true

permissions:
contents: read
packages: write
Expand All @@ -16,12 +20,16 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
ESIGNET_NATIVE_IMAGE: ${{ inputs.esignet_native_image }}
SOLMARA_IMAGE_REGISTRY: ghcr.io/registrystack
SOLMARA_IMAGE_TAG: ${{ inputs.solmara_image_tag || github.sha }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Require a digest-pinned native provider input
run: |
[[ "$ESIGNET_NATIVE_IMAGE" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]]
- name: Read immutable Registry Stack source identity
id: registry-stack
run: |
Expand All @@ -33,8 +41,8 @@ jobs:
printf 'source_commit=%s\n' "$REGISTRY_STACK_SOURCE_COMMIT" >> "$GITHUB_OUTPUT"
for key in REGISTRY_STACK_REQUIRED_VERSION \
REGISTRY_RELAY_IMAGE SOLMARA_EVIDENCE_IMAGE SOLMARA_MINT_IMAGE \
ESIGNET_BASE_IMAGE ESIGNET_POSTGRES_IMAGE ESIGNET_UI_IMAGE \
ESIGNET_AUTHENTICATOR_JAR_URL ESIGNET_AUTHENTICATOR_JAR_SHA256 \
ESIGNET_POSTGRES_IMAGE ESIGNET_NGINX_IMAGE NODE_BUILD_IMAGE \
ESIGNET_SOURCE_COMMIT ESIGNET_SOURCE_ARCHIVE_SHA256 \
REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256; do
value="$(printenv "$key")"
printf '%s=%s\n' "$key" "$value" >> "$GITHUB_ENV"
Expand Down Expand Up @@ -388,7 +396,7 @@ jobs:
platforms: linux/amd64
push: true
tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-portal:${{ env.SOLMARA_IMAGE_TAG }}
- name: Build and push eSignet Relay V2 authenticator image
- name: Build and push native eSignet BREG provider image
id: esignet_relay
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
Expand All @@ -398,9 +406,7 @@ jobs:
push: true
tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-relay:${{ env.SOLMARA_IMAGE_TAG }}
build-args: |
ESIGNET_BASE_IMAGE=${{ env.ESIGNET_BASE_IMAGE }}
ESIGNET_AUTHENTICATOR_JAR_URL=${{ env.ESIGNET_AUTHENTICATOR_JAR_URL }}
ESIGNET_AUTHENTICATOR_JAR_SHA256=${{ env.ESIGNET_AUTHENTICATOR_JAR_SHA256 }}
ESIGNET_CANDIDATE_IMAGE=${{ env.ESIGNET_NATIVE_IMAGE }}
- name: Build and push isolated eSignet database
id: esignet_postgres
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
Expand All @@ -422,7 +428,10 @@ jobs:
push: true
tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-ui:${{ env.SOLMARA_IMAGE_TAG }}
build-args: |
ESIGNET_UI_IMAGE=${{ env.ESIGNET_UI_IMAGE }}
NODE_BUILD_IMAGE=${{ env.NODE_BUILD_IMAGE }}
ESIGNET_NGINX_IMAGE=${{ env.ESIGNET_NGINX_IMAGE }}
ESIGNET_SOURCE_COMMIT=${{ env.ESIGNET_SOURCE_COMMIT }}
ESIGNET_SOURCE_ARCHIVE_SHA256=${{ env.ESIGNET_SOURCE_ARCHIVE_SHA256 }}
ESIGNET_NGINX_CONF=config/esignet/nginx-hosted.conf
- name: Build and push eSignet seed image
id: esignet_seed
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ The reset has two evidence cadences:

Six Evidence gateways have distinct providers, issuers, signing keys, JWKS, audit
sinks, subject-binding secrets, and endpoints. Five Relays expose only the
named non-enumerating operations needed by the lab. NIA's Relay is reserved for
the optional eSignet UserInfo profile; NIA Evidence reads its own extract.
named non-enumerating operations needed by the lab. The optional eSignet profile
now reads a separate governed BREG population fixture; NIA Evidence continues
to read its own extract.

## Release prerequisite

Expand All @@ -30,10 +31,12 @@ official OCI references by digest, and the `relayctl` binary checksum in
same full references without reconstructing them from a second deployment
input.

The eSignet profile uses the separately released
`esignet-relay-authenticator` v0.2.0 JAR and its matching SHA-256 checksum. No
source-build, locally wrapped runtime, floating-tag, or v0.19 compatibility
fallback is accepted.
The optional eSignet profile uses the native `0.3.0` BREG provider candidate
with pinned eSignet `2.0.0-beta.1` source and its matching UI. Build and run the
isolated identity journey using [the eSignet guide](docs/esignet.md). It needs
matching native BREG/Mint tools supporting `bregctl dev export-client`; the
existing v0.23.0 Evidence/Relay release pin does not provide that development
workflow. Hosted image references remain required immutable digests.

## Quick start

Expand Down Expand Up @@ -72,7 +75,7 @@ never assumes a national Evidence host.
| Authority | Evidence source | Relay V2 role |
|---|---|---|
| CRA | immutable birth extract; Relay for death and civil link | `civil-person/death-by-uin`, `civil-person/citizen-link-by-uin` |
| NIA | immutable population extract | `population-person/esignet-userinfo` for eSignet |
| NIA | immutable population extract | retained `population-person/esignet-userinfo` fixture; native eSignet uses BREG |
| SRO | immutable poverty extract | none |
| MoSD | Relay lookup | `beneficiary-enrolment/by-uin` |
| SIPF | Relay lookups | `pension-payment/by-pensioner-uin`, `survivor-case/by-spouse-uin` |
Expand Down
1 change: 1 addition & 0 deletions breg/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/.breg/
138 changes: 138 additions & 0 deletions breg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Synthetic population registry for eSignet 2

This local fixture runs an authored Base Registry Engine project over its own
PostgreSQL database. Registry Mint issues OAuth access tokens to separate
operator and eSignet clients. It uses the native `bregctl dev` lifecycle and
installed matching `bregctl`, `breg` and `mint` binaries, without a Registry Stack
source checkout. The tools must support `bregctl dev export-client`.

The fixture contains the first twelve canonical synthetic citizens from
`ministries/interior-population/fixtures/population_person.csv` and one explicit
inactive negative case. It is an isolated authentication test population, not a
replacement for the ministry's thousand-record scenario database. The model is
ordinary registry configuration; no Rust domain types or special runtime routes
are required.

## Start and verify

Install the matching native Registry Stack toolchain on `PATH`, start Docker,
and generate the lab's ignored secrets with `uv run scripts/gen-secrets.py`.
Then, from the repository root:

```sh
uv run python breg/generate-seeds.py --check
bregctl check breg/population
uv run python breg/dev.py start
uv run python breg/dev.py verify
```

### Build a pinned native toolchain

When an installed toolchain lacks `dev export-client`, build all three tools
together. The following uses Registry Stack source commit
`2710bf5163c08f9699e486a05e46603184229155`, whose native lifecycle includes the
required command, and its pinned Rust 1.95.0 toolchain and Cargo lockfile. Use
fresh directories outside the lab checkout:

```sh
git clone https://github.com/registrystack/registry-stack.git registry-stack-breg-tools
cd registry-stack-breg-tools
git checkout --detach 2710bf5163c08f9699e486a05e46603184229155
CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 \
cargo build --locked -p registry-breg --features registry-breg/runtime \
-p registry-bregctl -p registry-mint --bins
mkdir -p ../breg-tools/bin
install -m 755 target/debug/breg target/debug/bregctl target/debug/mint ../breg-tools/bin/
export PATH="$(cd ../breg-tools/bin && pwd):$PATH"
bregctl dev export-client --help
```

This pins build inputs; it does not promise identical binary bytes across host
toolchains. Return to the lab and run the checks above against a fresh fixture
before relying on that build. This exact source pin was built on macOS arm64
with Rust 1.95.0 and passed all thirteen live fixture checks. A coordinated
native stop/start preserved record identifiers, revisions, field values,
package revision and client credentials while switching to those binaries.
Normal startup uses the installed binaries only. Stop an existing fixture
normally before changing its binary set; never remove its data to change tools.

The wrapper calls `bregctl dev breg/population --breg-port 18190 --mint-port
18191 --database-port 55449`. Native startup rehearses the authored journeys
against real PostgreSQL, activates the compiled package, starts the services,
and creates the explicit seeds through authenticated BREG HTTP requests.

It exports only the `esignet` client's private ES256 JWK into the owner-only,
ignored `config/evidence/local/esignet-v2/` directory. The provider configuration
references `mint-private.jwk`, `psut-secret` and `static-otp` under its container
mount `/etc/registry-esignet`. Mint's token endpoint transport address is
`http://host.docker.internal:18191/token`; its client assertion audience remains
`http://127.0.0.1:18191/token`. The private key's `kid` comes from the JWK.
The lab's generated static OTP is explicitly enabled for this synthetic fixture.

BREG listens on `http://127.0.0.1:18190`; containers use
`http://host.docker.internal:18190`. Docker must support reaching host loopback
services through that address, as OrbStack does. The wrapper's port options
apply only to first start; the native lifecycle preserves retained ports and
refuses conflicting authored inputs. It never resets another service.

The live verification obtains fresh tokens without writing or printing them.
It checks the exact minimum account projection and a narrower consent
projection, concealed missing/inactive identities and unknown selectors,
withheld fields, denied source list/get/create, anonymous and invalid bearer
refusals, and rejection of a duplicate UIN by the separate operator role.

```sh
uv run python breg/dev.py stop
```

Stop preserves this fixture's database, credentials, and seed checkpoints.
Restart reuses them. Do not remove retained state to apply model changes. Follow
the native governed package lifecycle or create a separate disposable project.
Regenerate committed seeds explicitly with `uv run python breg/generate-seeds.py`
before a fresh fixture's first start; changing seeds after startup does not
overwrite retained citizens.

## Governed HTTP contract

The eSignet client has only the `lookup` operation, the `by-uin` selector, and
six readable fields: `uin`, `status`, `givenName`, `familyName`, `birthdate`, and
`gender`. The registry enforces the unique UIN constraint. Its row boundary
compares `status` with Mint's fixed `registry_identity_status: active` claim.
The caller-provided UIN is a selector input, never an authorization grant.
The source has no list, get, create, or patch grant. `operatorNote` remains
operator-only. The operator's unrestricted row finding is intentional for
maintenance and seeding; its credential is never exported to eSignet.

After the challenge has been verified, the provider checks only:

```http
POST /v1/records/population:lookup?accessProfile=esignet-source&%24select=uin%2Cstatus
Authorization: Bearer <short-lived token>
Content-Type: application/json

{"selector":"by-uin","values":{"uin":"2300010248"}}
```

The result is a Registry Record envelope. Actual facts are exclusively in
`data.domainData`, here exactly `{"uin":"2300010248","status":"active"}`.
For consented claims the provider requests the intersection of requested claim
fields with its provisioned fields using `$select`. It never falls back to
enumerating records. `givenName` and `familyName` are HTTP names for authored
logical fields `given-name` and `family-name`.

The portal requests `individual_id` explicitly as an essential consented claim.
It maps to the governed `uin` field for business-record correlation. The OIDC
`sub` remains the pairwise pseudonymous subject token and never substitutes for
that identifier. Display names use the separate `given_name` and `family_name`
claims; this fixture does not invent a stored full-name field.

A missing identity, inactive identity, or unresolved selector produces
`404 lookup.unresolved`; denied profile or operation produces concealed
`404 resource.not_found`. A rejected presented token produces
`401 authentication.refused`. The provider collapses concealed subject failures
and does not expose upstream differences. UIN uniqueness prevents an ambiguous
stored match, and BREG also fails unresolved lookup closed.

Native `tests/journeys.yaml` rehearses create, exact lookup, and missing lookup.
`breg/dev.py verify` exercises the wider HTTP refusal boundary, including routes
that cannot appear as authorized operations in a native journey fixture.
Loading
Loading