Why Vexcalibur publishes evidence about itself¶
Vexcalibur should be able to describe its own vulnerability posture using the same package and GitHub Action that downstream users run. That is useful only if the evidence is tied to the release bytes. A VEX file generated from a checkout while different bytes reach PyPI would be reassuring but meaningless.
The release design therefore treats the wheel, source distribution, locked inventory, reviewed findings, direct CLI output, and Action output as one immutable publication unit.
Two bundles serve different purposes¶
The repository keeps two related contracts:
Contract |
Purpose |
Published with a release |
|---|---|---|
Local bundle, manifest schema 1 |
Fast, deterministic maintainer and CI checks of one installed wheel |
No |
Publication bundle, manifest schema 2 |
Flat GitHub Release assets and the exact distributions later sent to PyPI |
Yes |
The local bundle remains useful because it can be generated without release credentials or GitHub state. The publication bundle adds isolation between producers, companion-Action equivalence, source-distribution checks, immutable release state, and recovery rules. Schema 2 extends the design; it does not silently change what an old schema-1 manifest means.
What is trusted¶
The design narrows trust instead of pretending to eliminate it:
Input or service |
Trusted claim |
Important limit |
|---|---|---|
Exact Git commit |
Source identity and commit-derived timestamp |
A checkout alone does not identify published bytes |
|
Cross-platform reference runtime reviewed at one digest |
It is not an environment-specific deployed inventory |
|
Publicly reviewable finding snapshot and attribution |
JSON cannot authenticate the reviewer or prove that no finding was missed |
Wheel and source distribution |
Exact package bytes and embedded version/source metadata |
Archive validation does not prove the source code is benign |
Pinned Vexcalibur Action commit |
One reviewed wrapper implementation |
The pin must be advanced deliberately when the Action changes |
GitHub Actions and Releases |
Job isolation, artifact transport, server-authenticated release and asset uploader identity, attestations, and immutable release enforcement |
GitHub and the selected hosted-runner images remain part of the trusted platform |
PyPI Trusted Publishing |
OIDC-bound upload identity |
PyPI availability and account governance remain external dependencies |
Production review policy currently permits only explicit in_triage findings.
An empty findings file means zero assertions. It never means that every
dependency is not_affected.
Isolation is the main security boundary¶
The reusable release-validation workflow divides candidate handling across fresh jobs:
build ───────────────┐
├──> fresh finalizer ──> flat release assets
inventory oracle ────┤
│
direct installed CLI ┤
pinned Action ────────┘
The inventory-oracle job never installs or executes the candidate wheel and never invokes the companion Action. It exports strict hash-locked runtime constraints and a normalized CycloneDX 1.5 SBOM from the exact lock, then binds those files to the reviewed inputs.
The direct-generation job receives that oracle plus the exact wheel. It
installs the wheel with a SHA-256-bound local URI and runs the installed
vexcalibur entry point outside the checkout. The Action-generation job runs
the full-commit-pinned companion Action in a separate environment. Each job
emits only the files its consumer needs.
A fresh finalizer downloads all producer artifacts, verifies their GitHub artifact identity and transport digests, revalidates every input, and requires the direct and Action VEX files to be byte-for-byte equal. It writes into a fresh directory and removes the incomplete directory after any late failure. It never merges into or overwrites an existing output.
GitHub artifact archive digests protect transport within one workflow run, but
they are not stable publication data. The schema-2 manifest instead records
stable payload digests over filename-sorted {name, sha256, size} records.
That distinction keeps recovery runs byte-reproducible even if GitHub changes
the archive envelope.
The evidence is deterministic¶
The release commit supplies SOURCE_DATE_EPOCH and every VEX timestamp. The
workflow builds once. The wheel must contain clean, full-commit SCM metadata;
the source distribution must contain the exact version and matching generated
SCM prefix. Both archive readers reject unsafe paths, duplicate members,
links, special files, oversized metadata, excessive member counts, and
excessive expanded size.
The bundled runtime constraints start with --require-hashes and
--only-binary :all:. Every requirement is an exact version with at least one
SHA-256 hash. This prevents dependency substitution; it does not mean package
installation is network-free. A runner may still download those exact bytes
from its configured index.
VEX generation itself selects only the reviewed local-findings provider. Proxy settings provide an additional failure boundary, but the precise claim is provider selection, not that every process on the runner lacks network access.
Publication preserves the checked bytes¶
The release publisher verifies the validation artifact and every proposed
asset before it receives the short-lived write token. It accepts only a narrow
state machine: the tag is absent or is the exact bot-authored annotated tag for
the target; the release is absent, is an exact matching draft, or is the exact
already-published immutable release. Normal-mode idempotency may reuse the
exact release tag already at the current main tip. Recovering an older tag
requires the explicit recovery input.
The annotated tag also protects the release notes before the draft becomes immutable. Its message is canonical, closed-world JSON containing the tag, the exact secret-scanned notes, and their SHA-256. Recovery validates the tag ref, object type, target commit, bot tagger, payload schema, and digest, then reconstructs the notes from that protected object. An existing draft or release body must match those bytes. The mutable draft body is therefore a checked replica, not recovery’s source of truth.
It never uploads with a clobber option. Existing assets must already match the
validated bytes. The only disposable marker is a zero-byte state=starter
asset created for the draft transaction. After publication, GitHub must report
the release immutable and the workflow verifies the release and each asset.
GitHub’s API, rather than a self-authored manifest field, supplies each asset’s
uploader identity. Reconciliation accepts a completed asset only when
uploader.login is vexcalibur-dev-automation[bot]. Immediately before the
immutable transition, the publisher snapshots every asset’s ID, name, size,
state, uploader, and empty display label, downloads the bytes, and rejects any
metadata or byte change. It queries the immutable release again afterward and
requires the same bot uploader and empty label for every uniquely named,
completed asset.
Normal releases require the validated commit to remain the tip of main.
Manual recovery names an existing tag whose commit may be older, but that
commit must still be an ancestor of current main. Recovery therefore does
not break merely because unrelated commits landed after a partial release.
PyPI publishing downloads the wheel and source distribution from that exact immutable GitHub Release. It does not rebuild. If neither file exists on PyPI, both are uploaded; if one exact hash already exists, only the missing file is selected. An existing filename with a different hash or package type stops the workflow. The OIDC-bearing job receives only the already-checked subset and uses no checkout, package installation, cache, or repository script.
PyPI publication independently queries the server-authenticated asset metadata
when it resolves the release, when validation downloads the asset set, and
immediately before the OIDC exchange. Every boundary requires a completed asset
uploaded by vexcalibur-dev-automation[bot] with no display label; an immutable
release authored by the bot is insufficient if even one asset has another
uploader or mutable UI label. Resolve and pre-OIDC checks also revalidate the
first-level bot-authored tag, notes envelope and digest, and exact release body.
What the bundle does not prove¶
Self-release evidence does not replace an independent security assessment. It
does not prove that all vulnerabilities were discovered, authenticate the
human review field by itself, describe every consumer environment, or make an
in_triage assertion stronger than it is. It also does not make historical
releases immutable retroactively.
Those limits are intentional. The bundle is strongest when every recorded claim is narrow enough to verify and every transition either preserves exact bytes or fails closed.