From 4e42911477c70abd54d0c87a5e107ebf8fefc2bb Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 22 Jul 2026 20:42:35 +0200 Subject: [PATCH] Document artifact origin and installer receipts --- docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md | 104 ++++++++++++++++------ docs/RELEASE_CONSOLE.md | 63 +++++++++++-- 2 files changed, 131 insertions(+), 36 deletions(-) diff --git a/docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md b/docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md index 4ae2bd0..f36fd7f 100644 --- a/docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md +++ b/docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md @@ -389,7 +389,7 @@ that observation to the assessment and signed catalog: --output /var/tmp/govoplan-fit-review.json ``` -The collector follows the strict version `0.3.0` +The collector follows the strict version `0.4.0` [`installed-composition-evidence.schema.json`](installed-composition-evidence.schema.json) contract. It enumerates all installed distributions whose normalized name starts with `govoplan-`, compares the enabled assessed package and module-manifest @@ -419,6 +419,9 @@ For each distribution, the collector also: package-index/unknown origins and malformed metadata; - compares a non-editable VCS commit with the assessment commit and, when one is present, the signed catalog `selected_units` commit; +- derives two content identities from bytes it actually verifies: an + install-stable identity for immutable wheel-declared files and a full + installed-RECORD identity, while retaining only SHA-256 digests and counts; - records only package/module identifiers, versions, bounded counters, hashes and stable error codes. It never writes direct URLs, paths, hostnames, usernames, exception text or file contents. @@ -436,25 +439,31 @@ remain mutable even when their small editable-install `RECORD` is valid. Archive or index artifacts without a hash anchored by the assessed release remain unanchored. A matching declaration is reported under `installed_source_provenance` as local consistency only, with -`release_origin_bound: false`. The separate `installed_release_origin` scope -remains unchecked until an independently anchored artifact digest or signed -installer receipt binds each installed payload to the signed catalog. The tool -does not accept a digest merely asserted inside the installed evidence document. +`release_origin_bound: false` until the separate origin proof succeeds. A signed +catalog can contain `release.artifacts` identities computed directly from built, +bounded wheel files. Wheels with installer-transformed scripts or headers are +marked `requires_installer_receipt`; catalog metadata alone cannot attest those +post-install bytes. A role-scoped installer receipt instead binds the exact +installed-evidence digest, full installed payload identities, catalog archive +digests and canonical signed-catalog digest. The tool never accepts an installed +hash merely because the installed evidence asserted it. Only evidence produced in-process by `--collect-installed-evidence` is a local observation, and it must be no more than five minutes old (with at most 30 seconds of future clock skew) at the selected verification time. Use `--installed-evidence PATH` to compare evidence collected in a separate -environment. Imported JSON is unsigned and therefore always carries a blocker: -it can identify differences but cannot establish checked or valid installed -proof. A future signed collector-attestation format is required to cross that +environment. Imported JSON without a valid independent installer receipt +carries a blocker: it can identify differences but cannot establish checked or +valid installed proof. A receipt signed by a separately provisioned installer +authority authenticates that exact imported evidence across the process boundary. The proof scope records observation mode, collection/evaluation time, -freshness, and whether evaluation used current time or an explicit historical -override. Evidence input and output are bounded to 4 MiB. Collection loads -the `govoplan.modules` entry-point factories in order to read module IDs and -manifest versions; that executes installed GovOPlaN manifest code. Run it only -inside the installation being assessed and with the same isolation expected for -other installed-artifact acceptance checks. This collector does not observe +receipt authentication, freshness, and whether evaluation used current time or +an explicit historical override. Evidence input and output are bounded to 4 +MiB. Collection loads the `govoplan.modules` entry-point factories in order to +read module IDs and manifest versions; that executes installed GovOPlaN manifest +code. Run it only inside the installation being assessed and with the same +isolation expected for other installed-artifact acceptance checks. This +collector does not observe which modules a running service activated, migrations, configuration, health, or reference-journey behavior. Runtime activation therefore remains an explicit unchecked boundary. @@ -495,14 +504,50 @@ the tool's deterministic canonicalization. ``` With `--installed-evidence`, this command performs comparison and proof-binding -diagnostics only: the imported installed document is unsigned, so neither it nor -the boundary claim becomes accepted proof. Direct in-process collection removes -that unsigned-import gap, but still establishes only local metadata consistency. -No boundary claim is admitted by the current format because it has no separately -anchored artifact digest or signed installer receipt. A future evidence contract -must bind such an independently trusted release-artifact receipt to the installed -evidence digest; a signed collector-attestation format should also make that -exchange durable across processes. +diagnostics. Without an installer receipt, the imported document remains +unsigned, so neither it nor the boundary claim becomes accepted proof. Direct +in-process collection can match catalog-anchored artifact identities for wheels +without installer-transformed files. The installer-receipt flow below +authenticates a durable cross-process observation and is required for transformed +script/header payloads. + +Issue a receipt only in the installation process performing the live collection. +The command refuses evidence supplied from a file and first validates the +assessment, independently trusted signed catalog, composition and RECORD bytes: + +```bash +./.venv/bin/python tools/assessments/installer-receipt.py \ + --assessment /srv/govoplan/assessment.json \ + --catalog /srv/govoplan/catalogs/stable.json \ + --keyring /srv/govoplan/catalogs/keyring.json \ + --trusted-keyring /srv/govoplan/trust/catalog-keyring.json \ + --receipt-id install:production:20260722 \ + --signing-key installer-production=/run/secrets/installer-ed25519.pem \ + --python-artifact govoplan-core=/srv/govoplan/staged/govoplan_core-0.1.13-py3-none-any.whl \ + --python-artifact govoplan-campaign=/srv/govoplan/staged/govoplan_campaign-0.1.10-py3-none-any.whl \ + --evidence-output /srv/govoplan/evidence/installed.json \ + --receipt-output /srv/govoplan/evidence/installer-receipt.json +``` + +Repeat `--python-artifact PACKAGE=/exact/consumed.whl` for every enabled +GovOPlaN distribution. The issuer reopens each exact wheel, verifies its archive +and payload identities against the signed catalog, and refuses a different +build with the same name and version. Receipt issuance must follow collection +within five minutes, and live verification must still fall inside that bounded +window; retain the pair for a reproducible historical review at its explicit +verification time. + +Verify that durable pair with its separately managed trust root: + +```bash +./.venv/bin/python tools/assessments/capability-fit.py \ + --catalog /srv/govoplan/catalogs/stable.json \ + --keyring /srv/govoplan/catalogs/keyring.json \ + --trusted-keyring /srv/govoplan/trust/catalog-keyring.json \ + --installed-evidence /srv/govoplan/evidence/installed.json \ + --installer-receipt /srv/govoplan/evidence/installer-receipt.json \ + --installer-authority-keyring /srv/govoplan/trust/installer-authorities.json +``` Target-environment and production-approval claims must use the assessment's `deployment_profile.id` as `subject_id`. External-provider claims require the @@ -510,11 +555,16 @@ operator to supply a bounded opaque expected subject with `--expected-external-provider-subject`; without it, such a claim remains unchecked and blocks. Expected and observed IDs are retained in proof scope. -The authority keyring is a governance trust root. Do not download or generate it -from the proof bundle being checked. The checker rejects proof-authority public -keys reused by either the published or independently trusted catalog keyring, -and rejects the same proof public-key material assigned to multiple authority -IDs. A malformed key, an invalid or empty validity interval, or ambiguous key +Both authority keyrings are governance trust roots. Installer receipt keys use +the strict +[`installer-receipt-authority-keyring.schema.json`](installer-receipt-authority-keyring.schema.json) +contract and may attest only `installed_release_origin`; their public material +must not be reused by catalog or boundary-proof authorities. Do not download or +generate them from the proof bundle being checked. The checker rejects +proof-authority public keys reused by either the published or independently +trusted catalog keyring, and rejects the same proof public-key material assigned +to multiple authority IDs. A malformed key, an invalid or empty validity +interval, or ambiguous key identity invalidates the supplied authority set. Authority `not_after` is exclusive. A target operator or approver must validate the referenced drill/result artifacts before signing. The rerun verifies the diff --git a/docs/RELEASE_CONSOLE.md b/docs/RELEASE_CONSOLE.md index 4e544c4..953ac02 100644 --- a/docs/RELEASE_CONSOLE.md +++ b/docs/RELEASE_CONSOLE.md @@ -212,7 +212,9 @@ an alternative condition without `required_scopes` or `any_scopes`. The catalog workflow panel can also operate on the same selected rows: -- `Generate` creates a signed candidate below `runtime/release-candidates/`. +- `Generate` creates a signed candidate in the operator's private XDG state + directory and records only an opaque candidate ID plus the canonical catalog + SHA-256 in durable run state. - `Preview` validates a candidate and shows what would be copied into the website repository. - `Apply + Website Tag` requires `APPLY` in the confirmation field. @@ -241,6 +243,30 @@ cannot be applied or published while any referenced source tag is absent or inconsistent; the preview and API response list each repository and missing or invalid ref so the operator can repair the exact source releases first. +Every selected Python release must also supply its exact built wheel. Candidate +generation computes the archive SHA-256 and an install-stable payload identity +from one bounded, regular-file descriptor and signs those values into +`release.artifacts`. Updating a Python version without a matching wheel removes +the stale identity. A source-only preview can still explain the gap, but apply, +commit, tag, and push fail closed until every selected Python unit has a matching +built-artifact identity. Repositories selected only for a meta/source tag do not +need a Python artifact. + +Candidate directories and files are operator-owned `0700`/`0600` state. Before +any later release step consumes one, the executor re-resolves the opaque handle +below the configured root and re-hashes the signed catalog against its persisted +receipt. Shared roots, symlinks, channel path fragments, altered candidates, and +unowned files are rejected. + +The server-owned wheel builds remain under the private candidate's `artifacts/` +directory. This slice signs their identities but does **not** upload the wheel or +add a download URL to the public catalog; the existing Git `python_ref` is source +provenance and rebuilding it is not an equivalent artifact. Keep the private +candidate until the exact wheels have been transferred through an approved +deployment channel, verified against `archive_sha256`, consumed by the installer, +and covered by its signed receipt. Public artifact transport and receipt-aware +candidate cleanup remain separate release-lifecycle work. + Read-only provenance checks derive a same-host HTTPS Git URL from conventional SSH remotes when possible, with a non-interactive check of the configured remote as fallback. Source tag creation and atomic publication always use the explicit @@ -269,18 +295,28 @@ Build a signed selective catalog candidate: ```sh KEY_DIR="$HOME/.config/govoplan/release-keys" +ARTIFACT_DIR="$(mktemp -d)" + +../govoplan-files/.venv/bin/python -m pip wheel \ + --no-deps \ + --no-build-isolation \ + --wheel-dir "$ARTIFACT_DIR" \ + ../govoplan-files ./.venv/bin/python tools/release/release-catalog.py selective \ --repo-version govoplan-files=0.1.9 \ + --python-artifact \ + "govoplan-files=$ARTIFACT_DIR/govoplan_files-0.1.9-py3-none-any.whl" \ --channel stable \ --catalog-signing-key "release-key-1=$KEY_DIR/release-key-1.pem" ``` -This writes candidate catalog/keyring files below `runtime/release-candidates/`, -generates the browsable module directory below `modules/`, validates the signed -candidate with the module installer validator, and reports whether the candidate -catalog/keyring match the currently published channel. It does not publish to -the website repository. +This writes candidate catalog/keyring files below +`$XDG_STATE_HOME/govoplan/release-candidates/` (or +`~/.local/state/govoplan/release-candidates/`), generates the browsable module +directory below `modules/`, validates the signed candidate with the module +installer validator, and reports whether the candidate catalog/keyring match the +currently published channel. It does not publish to the website repository. Regenerate the browsable module directory from an existing catalog/keyring: @@ -295,8 +331,10 @@ Regenerate the browsable module directory from an existing catalog/keyring: Preview publication of a reviewed candidate: ```sh +CANDIDATE_ROOT="${XDG_STATE_HOME:-$HOME/.local/state}/govoplan/release-candidates" + ./.venv/bin/python tools/release/release-catalog.py publish-candidate \ - --candidate-dir runtime/release-candidates/stable-YYYYMMDD-HHMMSS \ + --candidate-dir "$CANDIDATE_ROOT/stable-YYYYMMDD-HHMMSS" \ --channel stable ``` @@ -304,7 +342,7 @@ Apply the reviewed candidate into the website repository without pushing: ```sh ./.venv/bin/python tools/release/release-catalog.py publish-candidate \ - --candidate-dir runtime/release-candidates/stable-YYYYMMDD-HHMMSS \ + --candidate-dir "$CANDIDATE_ROOT/stable-YYYYMMDD-HHMMSS" \ --channel stable \ --apply \ --commit \ @@ -315,7 +353,7 @@ Push is a separate explicit flag: ```sh ./.venv/bin/python tools/release/release-catalog.py publish-candidate \ - --candidate-dir runtime/release-candidates/stable-YYYYMMDD-HHMMSS \ + --candidate-dir "$CANDIDATE_ROOT/stable-YYYYMMDD-HHMMSS" \ --channel stable \ --apply \ --commit \ @@ -323,6 +361,13 @@ Push is a separate explicit flag: --push ``` +Publication validates the same in-memory catalog object that it writes; it does +not copy a path that can change after validation. On commit, the console reads +the catalog, keyring, and complete module directory back from the immutable Git +tree and requires byte-for-byte equality with those validated objects. Tags and +remote branch updates then reference that exact commit SHA rather than the +mutable worktree `HEAD`. + Published channels are expected below the public catalog base URL: - `https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json`