Container evidence release contract¶
This reference defines the minimum evidence contract for a future supported Extra CODEOWNERS container release. It is an acceptance boundary, not a current asset list or a runnable verification procedure.
The raw OCI release spine now freezes one internal transport: a canonical record and an opaque byte-range file for exactly two platforms. CI uses pinned Buildx and BuildKit versions to export a real candidate to a local OCI directory, then verifies the two raw artifacts in a separate read-only job. The spine is not a release asset, a recipient evidence archive, or permission to publish.
The raw Python-distribution spine similarly carries the selected five-file application proof without archive parsing. The reusable workflow verifies and materializes that pair in a read-only job. The tagged workflow also defines a privileged consumer that would retain the three selection records and attest and sign the distributions, but the publication blocker keeps it unreachable. A separate blocked candidate assembler would revalidate the raw pair and inventory those three records, but its candidate record states that source completeness and publication are false. The GitHub release job does not consume either output.
No current release satisfies this contract
Extra CODEOWNERS does not publish a supported container release today. Do
not substitute pull-request artifacts, manual-run artifacts, or the old
unsupported main image.
Issue #28 must still
freeze the complete wire format: canonical JSON, the gzip and tar envelope and
member order, MANIFEST.json and source-record schemas, Sigstore issuer and
transparency-log requirements, and the SBOM and provenance predicate
contracts.
CI now gives image and source parsing separate rootless, networkless boundaries. A Docker-only process exports the local candidate without opening its archive. The export record binds the archive hash and size to the image configuration, subject, and platform before a separate parser inventories the layers. CI also fetches direct and Alpine sources once into verified stores, reuses those stores for both architectures, and assembles the final bundle offline.
That is substantial #28 groundwork, not a supported release path. The tagged workflow does not yet transport and attest the image-export handoff, run the recipient procedure against exact candidate assets, or grant isolated signing and publication authority. A supported recipient verifier does not exist yet.
Three open security gates separate today's CI evidence from this release contract:
| Issue | Work still required |
|---|---|
| #18 | Prove CFFI's libffi build input, close Psycopg's bundled-library sources, and bind Pydantic Core's bundled libgcc to an exact source. |
| #28 | Carry the isolated CI handoffs into the tagged candidate pipeline, freeze the wire format, and ship an adversarially tested recipient verifier, signing and publication path, and how-to. |
| #32 | Bind the retained Python selection records and exact wheel digest into the complete release evidence, then bind the installed runtime to that same wheel. |
The raw spine includes an adversarially tested transport verifier. It does not finish #28, which still requires a separate recipient verifier and a runnable how-to for the final release assets.
The collector has completed the CPython identity and source portion of #18 and the Cryptography, Greenlet, MarkupSafe, and SQLAlchemy native-owner portions on both platforms. It retains the exact locked platform wheel for every native-payload or embedded-SBOM owner and a separately addressed copy of each raw SBOM. For Greenlet, it also binds the owner sdist, the complete five-file native set, each embedded component, the exact Alpine GCC recipe and distfile, and reviewed source notices. These exact sets prove co-membership in the wheel. The SBOM has no component-to-file map, so the evidence does not assign an individual native file to the owner source or a nested component.
Cryptography adds exact archives, checksums, manifests, licenses, and notices
for 32 crates.io components. Its record also retains the sdist's local Rust
subtree and the official checksummed OpenSSL 4.0.1 release. The arm64
NotpineForGHA observation remains literal. A relationship links it to
Greenlet's reviewed Alpine GCC evidence because the libgcc payload bytes
match exactly.
Pydantic Core adds exact archives, manifests, checksums, licenses, and notices
for all 87 crates.io components in its SBOM. The retained sdist supplies its
root Cargo package and exact lockfile, including 16 registry packages that the
SBOM does not claim as components. The extension payload cites the complete
reviewed observation set. Its separate bundled libgcc payload remains
unresolved.
MarkupSafe adds one exact native payload, no SBOM observations, and an owner payload disposition. SQLAlchemy adds five exact native payloads with the same review shape. Each record binds the exact owner sdist as source evidence, not proof that every binary byte came from that archive. The Cryptography record also does not claim wheel reproducibility or build provenance. Three owners remain open; their full review records make each gap inspectable without pretending to close it.
The collector also replays wheel RECORD ownership for historical Python
installations whose bytes remain in lower layers. A release inventory must keep
that wheel_installations evidence and its effective-only
python_record_ownership projection. Completing source closure must not weaken
file attribution.
CI, manual runs, and the tagged candidate scan share one reusable build-proof workflow, and each caller builds its proof within its own run. Missing release assets remain expected until all three issues close and the project announces a supported release.
Trust statements¶
Evidence is platform-specific. Evidence for linux/amd64 says nothing about
the bytes in linux/arm64, and the reverse is also true. For each supported
platform, all of these identities must agree:
- the platform manifest digest selected from the versioned OCI index
- the subject of the signed evidence predicate and its OCI attestation
- the subject recorded in the evidence archive manifest
- the platform named by the component and all-layer inventories
- the SHA-256 and filename of the release evidence archive.
A signature proves who produced particular bytes. It does not establish that the component analysis is complete, that upstream metadata is accurate, or that a distribution satisfies legal obligations.
Required assets¶
A supported version must provide these assets for each supported architecture:
extra-codeowners-VERSION-linux-ARCHITECTURE-evidence.tar.gz- the archive's GNU-style
.sha256file - a keyless Sigstore bundle for the exact archive
- a small evidence predicate bound to the platform manifest digest
- the same predicate as a signed OCI attestation on that platform digest.
The OCI index must have exactly one linux/amd64 and one linux/arm64
manifest. Each platform manifest must have its own signed SPDX SBOM and
evidence attestation. The multi-platform index must have separate provenance
and a signature.
For selected version VERSION, the release workflow identity used for every
keyless signature and attestation must be exactly
https://github.com/stampbot/extra-codeowners/.github/workflows/release.yml@refs/tags/v${VERSION},
with ${VERSION} replaced by the already validated selected version. A
verifier must construct that one literal identity. It must not use a regular
expression that accepts every semantic-version tag, another branch,
repository, or workflow.
Evidence predicate¶
The canonical JSON predicate has exactly these fields:
| Field | Type | Requirement |
|---|---|---|
schema_version |
integer | Exactly 7. |
media_type |
string | Exactly application/vnd.stampbot.container-evidence.v7+tar+gzip. |
platform |
string | linux/amd64 or linux/arm64; it must match the selected manifest. |
subject_digest |
string | Lowercase sha256: digest of the published platform manifest, never a local image configuration digest. |
artifact |
object | Exactly filename and sha256. |
artifact.filename |
string | Exact release-asset filename for this platform. |
artifact.sha256 |
string | Lowercase SHA-256 of the raw archive bytes. |
release_url |
string | Immutable GitHub release URL for the selected version tag. |
A workflow rerun may reproduce the same canonical predicate. A recipient may deduplicate byte-identical, independently verified predicates. Two distinct valid predicates for one platform digest are an integrity failure.
Archive envelope¶
The archive is a deterministic gzip-compressed POSIX tar. Every retained member is a regular file with:
- a normalized relative POSIX path
- no duplicate path
- mode
0644 - numeric UID and GID
0 - owner and group name
root - the source commit's committer timestamp as whole Unix seconds, exactly the
value produced by
git show -s --format=%ct SOURCE_REVISION - an uncompressed size no greater than 64 MiB, except a member below
sources/native-components/, which may be no greater than 128 MiB.
Links, devices, FIFOs, sparse files, unknown member types, absolute or traversing paths, control characters, unsupported PAX fields, negative sizes, and partial archive iteration are invalid. The archive is limited to 100,000 retained files and 1 GiB of retained and compressed output. A conforming verifier must enforce its own bounded compressed input, expansion, path, member-count, PAX/GNU extension, JSON-size, and JSON-depth limits before creating output.
Generic tar extraction and ordinary iteration with Python's tarfile module
are not conforming verification procedures. Some malformed extension headers
can terminate iteration without a complete-member signal. The verifier added
by issue #28 must reject malformed headers, premature termination, and archive
trailing-data cases in its test corpus and must create output with no-follow,
exclusive-create semantics.
Required archive records¶
The archive must contain at least these entry points:
| Path | Contract |
|---|---|
MANIFEST.json |
Canonical archive identity, platform subject, reviewed policy digest, complete source status, and every retained source and license record. |
SHA256SUMS |
SHA-256 for every other retained member, with exact one-to-one path coverage. |
THIRD_PARTY_NOTICES.md |
Human-readable observed and reviewed license expressions for every effective and lower-layer component. |
inventory/components.json |
Exact normalized component inventory, including the CPython runtime and its identity files, package records, structured native payloads, structured SBOMs, raw wheel identities, historical wheel installations, and effective RECORD ownership. |
inventory/all-layer-files.json |
Every regular, directory, non-regular, and whiteout occurrence in every distributed layer, including security metadata; regular and directory records also carry effective state. |
inventory/native-component-coverage.json |
Derived per-owner ledger containing full closed and open review records, reviewed SBOM anomalies, and the exact remaining owner count and names. |
policy/container-policy.json |
The exact reviewed policy used to accept the candidate. |
artifacts/application/ |
The exact selected wheel, sdist, both native build records, and cross-architecture selection record; every file is hash-bound by MANIFEST.json. |
artifacts/native-wheels/ |
One exact locked platform wheel for every owner in the union of native_payloads and embedded_sboms, plus separately retained raw embedded-SBOM bytes. MANIFEST.json binds each owner, platform, exact requested URL, credential-safe redirect origins, path, size, and SHA-256. |
licenses/standard/ |
Hash-pinned standard license texts required by reviewed expressions. |
licenses/from-source/ |
Hash-pinned notices retained from exact source archives. |
sources/application/ |
Exact tracked Extra CODEOWNERS source blobs and Git modes at the image revision. |
sources/base/ |
Commit-pinned Docker Official Python recipe, exact recipe-selected CPython source archive, and required license evidence. |
sources/python/ |
Locked and reviewed-fallback top-level Python sources. |
sources/alpine/ |
Commit-pinned recipe subtrees and every local or downloaded source named by their verified checksums. |
sources/native-components/ |
Hash-addressed native-source artifacts or verified subtree manifests for directly reviewed components nested inside wheels. An owner-sdist source reuses the exact archive under sources/python/. |
sources/cargo-locks/ |
Exact Cargo.lock bytes verified from retained owner sdists for owners with crates.io reviews. The policy binds the original member path, digest, size, reviewed source IDs, and complete lock-only registry remainder. |
Current native-wheel manifest records¶
Until issue #28 freezes the recipient schema, this is the exact schema-v7
collector format for MANIFEST.json.native_wheel_artifacts. It is an inspection
reference, not a promise that the unfinished release wire format will remain
unchanged.
Each wheel record has exactly these fields:
| Field | Requirement |
|---|---|
owner |
Canonical python:NAME@VERSION owner derived from the inventory. |
platform |
Exact inventory platform. |
url |
Requested lock-file URL. |
urls |
The exact requested URL followed by the canonical HTTPS origin of each redirect destination. Redirected paths and queries are not persisted. |
filename |
Basename selected from the lock-file URL. |
path |
artifacts/native-wheels/NAME/VERSION/FILENAME. |
size, sha256 |
Size and lowercase SHA-256 of the retained wheel bytes. |
build, tags |
Exact WHEEL build value and sorted tag list used for selection. |
generated_files |
Sorted records for reviewed installer-generated launchers. |
embedded_sboms |
Sorted records for separately retained raw SBOM bytes. |
Each generated_files item has exactly name, kind, module, callable,
source_path, launcher_interpreter, and installed_occurrence. The
occurrence has exactly effective, layer, path, sha256, size, mode,
uid, and gid.
Each embedded_sboms item has exactly owner, platform, url, urls,
archive_path, installed_occurrence, path, size, and sha256. Its path
is
artifacts/native-wheels/NAME/VERSION/embedded-sboms/ARCHIVE_PATH, and its
occurrence uses the same exact field set described above. SHA256SUMS binds the
wheel, raw SBOM, and manifest bytes independently of these records.
Current native-component coverage records¶
inventory/native-component-coverage.json and
MANIFEST.json.native_component_coverage contain the same canonical object:
| Field | Requirement |
|---|---|
schema_version |
Exactly 7. |
platform |
Exact inventory platform. |
complete |
Derived boolean; true only when every observed native/SBOM owner has a closed review. |
resolved_owners |
Sorted, full policy records whose review state is closed. |
unresolved_owners |
Sorted, full policy records whose review state is open. |
observed_sbom_anomalies |
Sorted anomaly-review records copied from accepted SBOM metadata-root echoes. |
remaining_owner_count |
Number of open owner records. |
remaining_owner_names |
Sorted owner names derived from the open records. |
Schema 7 keeps each CycloneDX occurrence distinct. A nonempty bom-ref is the
document-local identity; PURL is the fallback only when bom-ref is empty.
Repeated PURLs are allowed only when every occurrence has a unique, nonempty
bom-ref. The ledger therefore retains all four krb5 and both libldap
occurrences reported by Psycopg.
An accepted auditwheel metadata-root echo appears in both the observation and
observed_sbom_anomalies. It must be canonically identical to the metadata
component and have an explicit metadata-root-echo review. Cryptography,
Greenlet, and Psycopg each have one such reviewed anomaly on each platform.
Closed records hold direct component reviews, source IDs, reviewed license
expressions, payload dispositions, and any narrowly validated cross-owner
relationship. Such a relationship binds byte-identical payloads and requires
both named payload dispositions to cite the corresponding observations. Open
records retain those same fields plus structured known_omissions; they are
not reduced to path/hash summaries. The current open records are:
| Owner | Omission IDs |
|---|---|
python:cffi@2.1.0 |
unproven-libffi-build-input |
python:psycopg-binary@3.3.4 |
missing-libpq-sbom, unreviewed-bundled-library-sources |
python:pydantic-core@2.46.4 |
missing-libgcc-sbom |
Cryptography, Greenlet, MarkupSafe, and SQLAlchemy are closed. The resulting
ledger has complete: false, remaining_owner_count: 3, and the three sorted
owner names.
Native sources use a four-way tagged union: commit-pinned Alpine aports sources, canonical crates.io archives, canonical subtrees of the locked owner sdist, and upstream releases bound by a strict checksum document. Their bundle directory is the first 20 hexadecimal characters of SHA-256 over the source ID:
- kind-specific artifacts or subtree manifests:
sources/native-components/SOURCE_DIGEST_PREFIX/ - source notices:
licenses/from-source/native-SOURCE_DIGEST_PREFIX/.
For owner-sdist-subpath, the exact locked archive remains under
sources/python/; the native-source directory retains the verified subtree
manifest.
Any owner with a crates.io component review must also carry a non-null
cargo_lock record. Collection extracts the exact named lockfile from the
retained owner sdist and checks the complete registry package set. Reviewed
crate entries must match their source archive checksums; every other crates.io
entry must appear in the sorted non_sbom_packages remainder. Foreign
registries, missing or duplicate packages, checksum drift, and unexplained
local packages fail collection. The verified bytes are retained below
sources/cargo-locks/.
The raw component inventory does not carry a source_completeness assertion.
MANIFEST.json.source_completeness is derived from the coverage ledger. A
supported release requires that manifest value and ledger complete to be
true, with no open owner records, a zero remaining count, and an empty
remaining-name list. CPython identity/source evidence and historical RECORD
replay must also remain intact. Editing a boolean cannot satisfy these gates.
Collection and publication boundary¶
No job that parses a contributor-controlled image or archive may hold package write, signing, attestation, GitHub release, or OpenID Connect authority.
CI currently implements this source and bundle path:
trusted direct-source plan
-> unprivileged direct-source fetch
-> rootless offline Alpine distfile plan
-> unprivileged Alpine distfile fetch
-> verified stores reused by both architecture jobs
-> rootless offline final parse and deterministic bundle
The two stores are uploaded once and downloaded by immutable artifact ID. Each bundle consumer binds them to trusted plan digests and sizes; there is no network fallback. Each source plan retains its exact requested URL. Persisted redirect destinations are reduced to canonical HTTPS origins, so a redirected path, signed query, or credential does not enter the store or evidence manifest.
The rootless parser runs without network access, secrets, a Docker socket,
Linux capabilities, or privilege escalation. It uses a read-only image and
inputs, bounded tmpfs scratch, work, and output mounts, and explicit memory,
CPU, process, file-descriptor, byte, and inode limits.
Issue #28 still needs to put image and layer inventory parsing behind that boundary. It must also add the recipient verifier and a short-lived isolated signing and publication path. That privileged phase may accept only bounded, schema-validated, digest-addressed outputs from the parser.
The raw spine can carry OCI objects across the unprivileged-to-privileged boundary, but it does not complete the boundary by itself. The root OCI index digest must come from the pinned build action, outside the spine record. A future publisher must consume only the bounded object snapshot returned from the descriptor retained by successful spine verification. The verifier hashes the entire snapshot before exposing it and never rereads the source for that snapshot. A publisher must not reopen a verified path or finalize a manifest, tag, release, or other reference until the verification context exits successfully.
Retention and mirror behavior¶
Recipients should preserve the original signed archive, signature bundle, predicate, release URL, subject platform digest, and release-workflow identity together. A mirror must retain the original filename and hashes and must not replace the upstream signature with only a mirror-local signature.
The current pull-request evidence artifacts expire after five days. They are unsigned review inputs for maintainers and are outside this recipient contract. If they expire, rerun CI for the exact source revision; do not use an artifact produced for another commit.
See container distribution evidence for design rationale and review container evidence for the current maintainer-only CI procedure.