Skip to content
Closed
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
38 changes: 35 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -558,10 +558,42 @@ jobs:
- name: Test upgrade exercise validator
run: python3 -m unittest release/scripts/test_validate_upgrade_exercise.py

- name: Validate upgrade exercise template
- name: Test upgrade exercise asset preparation
run: python3 -m unittest release/scripts/test_prepare_upgrade_exercise_assets.py

- name: Prepare committed upgrade candidate assets
id: upgrade-assets
env:
GH_TOKEN: ${{ github.token }}
run: >-
python3 release/scripts/validate-upgrade-exercise.py --template
release/exercises/upgrade-exercise-v1.template.json
python3 release/scripts/prepare-upgrade-exercise-assets.py
--discover release/exercises
--asset-root target/upgrade-exercise-assets
--github-output "${GITHUB_OUTPUT}"

- name: Install cosign for committed upgrade evidence
if: steps.upgrade-assets.outputs.has_candidates == 'true'
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3

- name: Install SLSA verifier for committed upgrade evidence
if: steps.upgrade-assets.outputs.has_candidates == 'true'
env:
SLSA_VERIFIER_SHA256: 946dbec729094195e88ef78e1734324a27869f03e2c6bd2f61cbc06bd5350339
SLSA_VERIFIER_VERSION: v2.7.1
shell: bash
run: |
set -euo pipefail
tools_dir="${RUNNER_TEMP}/upgrade-evidence-tools"
mkdir -p "${tools_dir}"
curl --proto '=https' --tlsv1.2 --fail --location --retry 3 \
--output "${tools_dir}/slsa-verifier" \
"https://github.com/slsa-framework/slsa-verifier/releases/download/${SLSA_VERIFIER_VERSION}/slsa-verifier-linux-amd64"
echo "${SLSA_VERIFIER_SHA256} ${tools_dir}/slsa-verifier" | sha256sum --check
chmod 0755 "${tools_dir}/slsa-verifier"
echo "${tools_dir}" >> "${GITHUB_PATH}"

- name: Validate committed upgrade exercise records
run: python3 release/scripts/validate-upgrade-exercise.py --discover release/exercises --candidate-asset-root target/upgrade-exercise-assets

- name: Stable surface compatibility
env:
Expand Down
49 changes: 44 additions & 5 deletions release/conformance/openid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ themselves:

- The supported Registry Notary topology must use a frozen release-candidate
image pinned by digest and checked-in non-secret configuration.
- The full OID4VCI issuer plan needs an adapter that sends the issuer-initiated
credential offer to the suite's `/credential_offer` callback.
- The owner-only `submit-offer` adapter can send the real issuer-initiated
pre-authorized offer to the suite's `/credential_offer` callback without
exposing it in process arguments or command output.
- The upstream full-plan shape currently selects DPoP. Registry Notary 1.0 does
not support or claim DPoP, wallet attestation, PAR, EUDI, HAIP, an
authorization-code wallet grant, or ES256 holder proof.
Expand All @@ -45,9 +46,10 @@ and unmodified result status without retaining secrets.
Notary's registry-backed OID4VCI issuer. It runs
`oid4vci-1_0-issuer-test-plan` with only
`oid4vci-1_0-issuer-metadata-test`.
- `notary-oid4vci-issuer-full` is mapped but blocked until a topology adapter
bridges a Notary pre-authorized offer into the suite callback and the suite
path matches the supported Registry Notary profile.
- `notary-oid4vci-issuer-full` is mapped but blocked until the suite path
matches the supported Registry Notary profile. The offer adapter closes only
the callback transport gap; it does not add DPoP, attestation, batch,
notification, or other unsupported product behavior.

The suite's `sender_constrain=dpop` selector is required by the upstream plan
shape. The metadata-only module does not exercise DPoP, and the selector must
Expand Down Expand Up @@ -91,6 +93,33 @@ Candidate-only scenarios are directly runnable. `--allow-blocked` is reserved
for deliberate investigation of scenarios whose status is explicitly blocked;
it does not turn their output into release evidence.

For an issuer-initiated suite module, store the exact
`openid-credential-offer` URI rendered after Notary completes its authenticated
`/oid4vci/offer/callback` in an owner-only file. After `up`, export the exact
self-signed certificate generated for the suite's Nginx service from
`/etc/ssl/certs/nginx-selfsigned.crt`, then submit the offer:

```bash
release/scripts/openid-conformance-runner.py export-suite-ca \
--output target/openid-conformance/conformance-suite-ca.pem

chmod 600 /private/path/notary-offer.txt
release/scripts/openid-conformance-runner.py submit-offer \
--offer-file /private/path/notary-offer.txt \
--issuer-url https://issuer.example.test \
--suite-offer-endpoint 'https://localhost.emobix.co.uk:8443/<module>/credential_offer' \
--suite-ca-certificate target/openid-conformance/conformance-suite-ca.pem
```

The adapter accepts only an inline Notary offer with the pre-authorized-code
grant, sends it once to the pinned suite origin without proxies or redirects,
and prints no offer content. TLS uses normal hostname and certificate
validation. The checked-in certificate recipe covers
`localhost.emobix.co.uk`, `localhost`, `127.0.0.1`, and `::1`. The optional CA
file is read once without following symlinks and adds only that explicitly
captured local trust anchor. The export command refuses to overwrite an
existing output. A fabricated offer is not candidate evidence.

Set `REGISTRY_OPENID_CONFORMANCE_AUTHORIZATION_SERVER` when the authorization
server differs from the issuer. Set
`REGISTRY_OPENID_CONFORMANCE_CREDENTIAL_CONFIGURATION_ID` when the topology
Expand Down Expand Up @@ -131,7 +160,17 @@ proof JWTs, issued credentials, transaction codes, or seeded civil identifiers.
Review and redact an export before turning it into release evidence. A failed
or warned result must remain visible in the reviewed summary.

The current runner keeps raw exports private but does not yet generate the
candidate-bound allowlisted summary or review promotion required for release
evidence. Until that source-side gate and a real frozen-candidate exercise
exist, its output remains candidate-exercise material rather than a completed
conformance claim.

The first metadata-only run and its known failures are recorded in
[`initial-report.md`](initial-report.md). It is historical context only. It is
not evidence for the current candidate, any wallet, any verifier, or the full
issuer profile.

The Rust SD-JWT verifier is a caller-invoked library, not an OID4VP endpoint.
Its library and fixture tests therefore do not support an OID4VP verifier
conformance claim.
6 changes: 5 additions & 1 deletion release/conformance/openid/nginx.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ FROM nginx:1.27.3@sha256:bc2f6a7c8ddbccf55bdb19659ce3b0a92ca6559e86d42677a5a02ef
RUN openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/ssl/private/nginx-selfsigned.key \
-out /etc/ssl/certs/nginx-selfsigned.crt \
-subj "/CN=localhost"
-subj "/CN=localhost.emobix.co.uk" \
-addext "subjectAltName=DNS:localhost.emobix.co.uk,DNS:localhost,IP:127.0.0.1,IP:::1" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign" \
-addext "extendedKeyUsage=serverAuth"
COPY nginx.conf /etc/nginx/nginx.conf
16 changes: 14 additions & 2 deletions release/conformance/openid/plan-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{
"id": "notary-oid4vci-issuer-full",
"surface": "Registry Notary registry-backed OID4VCI issuer",
"status": "blocked-by-suite-profile-and-offer-adapter",
"status": "blocked-by-suite-profile",
"suite_plan": "oid4vci-1_0-issuer-test-plan",
"suite_modules": [],
"variants": {
Expand All @@ -66,14 +66,21 @@
"vci_authorization_code_flow_variant": "issuer_initiated"
},
"config_template": "registry-notary-oid4vci-issuer.template.json",
"offer_adapter": {
"command": "release/scripts/openid-conformance-runner.py submit-offer",
"input": "The owner-only openid-credential-offer URI rendered only after Registry Notary completes its authenticated /oid4vci/offer/callback and registry-backed evaluation.",
"output": "One no-redirect GET to the pinned suite module's exposed /credential_offer endpoint with the unchanged inline credential_offer value."
},
"requires": [
"A frozen Registry Notary release-candidate topology with a suite-reachable issuer URL.",
"A bridge that sends the issuer-initiated Notary pre-authorized offer to the suite's exposed /credential_offer endpoint.",
"The candidate-neutral submit-offer adapter must receive the real owner-only Notary offer URI produced after the authenticated callback; a fabricated offer does not qualify.",
"A reviewed suite path that does not require DPoP, wallet attestation, PAR, an authorization-code wallet grant, ES256 holder proof, EUDI, or HAIP behavior outside Registry Notary 1.0."
],
"notes": [
"Registry Notary supports issuer-initiated pre-authorized code only. The identity provider's authorization code is internal to Notary and is not a wallet grant.",
"The pinned suite accepts issuer-initiated pre-authorized offers at its exposed /credential_offer endpoint. The adapter now closes that transport gap without adding a Registry Notary product endpoint.",
"The checked-in sender_constrain=dpop variant is required by this upstream suite plan shape and does not represent Registry Notary DPoP support.",
"The full plan remains blocked because its current module and variant roster exercises unsupported DPoP, attestation, batch, notification, and other behavior outside the Registry Notary 1.0 profile.",
"Do not treat this blocked mapping, a development topology, or historical wallet smoke output as certification evidence."
]
}
Expand All @@ -84,6 +91,11 @@
"evidence": "The release-owned candidate-neutral Relay and Zitadel smoke is directly runnable against a published Relay image digest. Its live output requires candidate binding and maintainer review before it becomes release evidence.",
"reason": "The OIDF suite does not publish a generic resource-server conformance plan for a Relay-style protected API. The separate release topology verifies discovery, signature, audience, token type, and native Zitadel role-object scope mapping."
},
{
"surface": "Registry Notary Rust SD-JWT verifier",
"evidence": "Rust library and fixture interoperability tests only; no OIDF OID4VP verifier-plan result applies to this surface.",
"reason": "The Rust SD-JWT verifier is a caller-invoked verification library, not an OID4VP endpoint. Registry Stack 1.0 therefore makes no OID4VP verifier conformance claim for it."
},
{
"surface": "Third-party OpenID Providers used by demonstration topologies",
"evidence": "Out of Registry Stack conformance scope.",
Expand Down
47 changes: 25 additions & 22 deletions release/conformance/relay-oidc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ resource server with `auth.mode: oidc`. It does not build Relay from the source
checkout and does not depend on a hosted environment or Solmara Lab.

The topology uses digest-pinned Zitadel, PostgreSQL, and Python images. The
runner accepts Relay only as the exact image reference
`ghcr.io/registrystack/registry-relay@sha256:<digest>`. The operator-supplied
candidate source commit and release identifier are mandatory and recorded for
later comparison with the release manifest. The runner does not derive or
cryptographically verify either identifier from the image. A maintainer must
perform that manifest binding before treating a report as release evidence.
runner resolves Relay from the exact release manifest and matching
`registryctl-<tag>-image-lock.json` release asset. It rejects a source-ref,
release-tag, product-version, or image-digest mismatch before starting Docker.
The image lock must remain in its downloaded release asset directory alongside
`SHA256SUMS`, the release capsule, the shared release provenance, and the
Cosign signature and certificate files for both the image lock and capsule.
The runner verifies those bindings with installed `cosign` and `slsa-verifier`
before using either product image digest.
For a closed release manifest, it accepts only the byte-exact tag manifest with
the single `stack.status: release-candidate` to `released` closeout transition.
Any other post-tag manifest drift is rejected.

## Evidence boundary

The checked-in assets and their offline tests prove that the harness is
reviewable and candidate-neutral. They are not live release evidence. A live
run writes a report classified as `unreviewed-live-candidate-output` with
`review_required: true`. A maintainer must bind that report to the published
candidate manifest and review it before it can become release evidence.
`review_required: true`. A maintainer must review the embedded candidate
binding and results before the report can become release evidence.

The report contains identifiers, configuration and topology digests, bounded
diagnostics, and assertion results. It never contains the bootstrap PAT, client
Expand Down Expand Up @@ -69,21 +74,18 @@ HTTP issuer and discovery URL on loopback, which is the only insecure fetch
form Relay permits for local development. The Relay API is published separately
on a randomly selected loopback port.

## Offline review
## Candidate review

Python 3.11 or later is required. Validate the checked-in topology and render a
candidate-bound plan without Docker or network access:
candidate-bound plan without Docker. Candidate binding invokes `cosign` and
`slsa-verifier`, which may use their normal verification network paths:

```bash
RELAY_IMAGE='ghcr.io/registrystack/registry-relay@sha256:'\
'<64-lowercase-hex>'

release/scripts/relay-oidc-smoke.py validate

release/scripts/relay-oidc-smoke.py plan \
--relay-image "$RELAY_IMAGE" \
--candidate-source-ref '<40-lowercase-hex-commit>' \
--release-id '1.0.0-rc.1'
--release-manifest 'release/manifests/registry-stack-<release-id>.yaml' \
--image-lock '/private/path/registryctl-v<version>-image-lock.json'
```

The plan deliberately records `live_evidence: false`.
Expand All @@ -94,20 +96,21 @@ Docker with Docker Compose is required. The Relay image must already be
published by digest:

```bash
RELAY_IMAGE='ghcr.io/registrystack/registry-relay@sha256:'\
'<64-lowercase-hex>'

release/scripts/relay-oidc-smoke.py run \
--relay-image "$RELAY_IMAGE" \
--candidate-source-ref '<40-lowercase-hex-commit>' \
--release-id '1.0.0-rc.1'
--release-manifest 'release/manifests/registry-stack-<release-id>.yaml' \
--image-lock '/private/path/registryctl-v<version>-image-lock.json'
```

The command prints only the path to the unreviewed report. Use `--output-dir`
to choose an empty report directory and `--host-port` only when a fixed free
loopback port is required. The default output is under
`target/relay-oidc-smoke/`, which Git ignores.

The release-owned topology is the default. An optional Solmara adopter
exercise must add
`--topology solmara --solmara-source-ref '<40-lowercase-hex-commit>'`;
an unpinned Solmara checkout is rejected.

If teardown fails, treat the run as an error. The diagnostic includes the exact
random Compose project name only in the local command output, so an operator
can inspect and remove that isolated project without risking unrelated Docker
Expand Down
30 changes: 26 additions & 4 deletions release/exercises/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,38 @@ release are frozen and independently verified:
1. Copy the template to a candidate-specific JSON file.
2. Change `record_kind` to `candidate_evidence`.
3. Replace every placeholder with an exact version, commit, digest, timestamp,
bounded authority identifier, or evidence label.
bounded authority identifier, or evidence label. `target_release.source_ref`
is the reviewed prepare commit P and `source_commit` is the finalized target
T. The manifest path and hash must identify the manifest stored at T.
4. Hash each committed configuration schema and every complete recovery-set
artifact. Do not copy secret values into the record.
5. Exercise every required check against the pinned standalone Solmara
5. Fill the canonical artifact set with the two P and T binary inventories,
image-input inventories, retained image-layout-pair identities, target
images, manifest, image lock, and P/T release-input identities. Its
`sha256` is the SHA-256 of canonical compact JSON for the `artifacts` object
(`sort_keys=True`, separators `,` and `:`).
Under a private candidate-asset root, keep one directory named for each
target version. Each version directory contains the downloaded
`registryctl-<target-version>-image-lock.json` beside its `SHA256SUMS`,
signed release capsule, Cosign signatures and certificates, and shared SLSA
provenance. The validator authenticates each exact release asset and
requires its byte digest and image pins to match the corresponding record.
6. Exercise every required check against the pinned standalone Solmara
topology. Record `passed` only when the retained evidence proves the check.
6. Set `candidate_frozen` and `candidate_independently_verified` to `true`.
7. Validate the candidate record without `--template`:
Honest `failed` and `not_run` records remain structurally valid; a `not_run`
result uses null evidence fields.
7. Set both candidate attestations to `true` only after independent review.
8. Validate the record structure, then require every promotion check to pass:

```sh
python3 release/scripts/prepare-upgrade-exercise-assets.py \
--discover release/exercises \
--asset-root /private/path/candidate-release-assets
python3 release/scripts/validate-upgrade-exercise.py \
--candidate-asset-root /private/path/candidate-release-assets \
release/exercises/<candidate-upgrade-record.json>
python3 release/scripts/validate-upgrade-exercise.py --require-pass \
--candidate-asset-root /private/path/candidate-release-assets \
release/exercises/<candidate-upgrade-record.json>
```

Expand Down
30 changes: 29 additions & 1 deletion release/exercises/upgrade-exercise-v1.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@
},
"target_release": {
"version": "<TARGET_VERSION>",
"release_id": "<TARGET_RELEASE_ID>",
"source_ref": "<TARGET_SOURCE_REF>",
"source_commit": "<TARGET_COMMIT>",
"relay_image_digest": "<TARGET_RELAY_IMAGE_DIGEST>",
"notary_image_digest": "<TARGET_NOTARY_IMAGE_DIGEST>"
},
"target_release_manifest_sha256": "<TARGET_RELEASE_MANIFEST_SHA256>",
"target_release_manifest": {
"path": "<TARGET_RELEASE_MANIFEST_PATH>",
"sha256": "<TARGET_RELEASE_MANIFEST_SHA256>"
},
"candidate_frozen": false,
"candidate_independently_verified": false,
"config_schemas": {
Expand All @@ -28,6 +33,29 @@
"sha256": "<NOTARY_CONFIG_SCHEMA_SHA256>"
}
},
"candidate_artifact_set": {
"sha256": "<CANDIDATE_ARTIFACT_SET_SHA256>",
"artifacts": {
"p1_binaries": "<P1_BINARY_INVENTORY_SHA256>",
"p1_image_inputs": "<P1_IMAGE_INPUT_INVENTORY_SHA256>",
"p2_binaries": "<P2_BINARY_INVENTORY_SHA256>",
"p2_image_inputs": "<P2_IMAGE_INPUT_INVENTORY_SHA256>",
"t1_binaries": "<T1_BINARY_INVENTORY_SHA256>",
"t1_image_inputs": "<T1_IMAGE_INPUT_INVENTORY_SHA256>",
"t2_binaries": "<T2_BINARY_INVENTORY_SHA256>",
"t2_image_inputs": "<T2_IMAGE_INPUT_INVENTORY_SHA256>",
"p_notary_layouts": "<P_NOTARY_LAYOUT_PAIR_SHA256>",
"p_relay_layouts": "<P_RELAY_LAYOUT_PAIR_SHA256>",
"t_notary_layouts": "<T_NOTARY_LAYOUT_PAIR_SHA256>",
"t_relay_layouts": "<T_RELAY_LAYOUT_PAIR_SHA256>",
"image_lock": "<IMAGE_LOCK_SHA256>",
"manifest": "<TARGET_RELEASE_MANIFEST_SHA256>",
"notary_image": "<TARGET_NOTARY_IMAGE_DIGEST>",
"relay_image": "<TARGET_RELAY_IMAGE_DIGEST>",
"p_release_inputs": "<P_RELEASE_INPUTS_SHA256>",
"t_release_inputs": "<T_RELEASE_INPUTS_SHA256>"
}
},
"topology": {
"repository": "registrystack/solmara-lab",
"release_tag": "<SOLMARA_RELEASE_TAG>",
Expand Down
Loading
Loading