Skip to content
Draft
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
without changing the centrally managed review-agent credential contract.

### Security
- Bound archive-member enumeration before ZIP object-table allocation and stream
gzip/tar physical headers before semantic parsing, enforcing 10,000 members,
512 MiB expanded tar, and 1 MiB extension-header limits while rejecting
unnecessary ZIP64, multi-disk, sparse, link, device, FIFO, and special forms.
- Preflight every direct release-SBOM wheel or source archive as one regular
file with a finite 256 MiB compressed-byte ceiling before ZIP or gzip/tar
parsing, metadata reads, or artifact hashing. Symlinks, directories, devices,
Expand Down
198 changes: 121 additions & 77 deletions docs/sbom-release-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,80 @@ and package URLs (purls).
This is a read-only evidence foundation. It does not authorize a pull-request
branch to execute release logic with write credentials. The only permitted
future integration source is a protected-main or organization-level reusable workflow
whose source is immutable before receiving OIDC or attestation permissions.
No SLSA Build level is claimed merely because an SBOM or attestation exists.
whose source is immutable before receiving OIDC or attestation
permissions. No provenance, signing, publication, attestation, or SLSA Build
level follows merely from direct SBOM generation.

## Normative evidence contract

`scripts/ci/generate_release_sbom.py` treats every archive, manifest, and lock
file as untrusted input and never imports EgressWeave. It must:

1. accept only a wheel or gzip source distribution;
2. inspect the caller-supplied final release-artifact path without resolving that
final component, require a regular file, enforce a 256 MiB compressed-byte
ceiling, open it without following a final symbolic link where the platform
supports that flag, and require the opened descriptor to retain the accepted
device and inode before any parser;
3. parse and hash only that bound descriptor, keep every parser-visible read and
seek live-bounded by the same 256 MiB ceiling, bracket metadata parsing with
finite SHA-256 reads, and fail if the archive bytes change during verification;
4. reject unsafe or duplicate paths, links, devices, excessive member counts,
ambiguous metadata, oversized metadata, and malformed archives;
5. check the declared wheel metadata size before decompression;
6. read exactly one wheel `METADATA` or root source `PKG-INFO` member;
7. verify package identity, license expression, and complete direct runtime
requirement declarations against the reviewed manifest;
8. verify every dependency version, SHA-256, and environment marker against the
executable hash-locked subset in `requirements-ci.txt`, while rejecting
dependency extras that could activate packages outside the reviewed graph;
9. validate identities, SPDX license identifiers, purls, graph references,
relationships, reachability, and acyclicity;
10. compute the artifact SHA-256 without trusting its filename; and
11. emit sorted UTF-8 CycloneDX 1.7 JSON without timestamps or random identifiers.

The direct generator normalizes missing, uninspectable, symbolic-link, directory,
device, FIFO, socket, replaced, and other non-regular artifact inputs to
`release artifact is missing or unsafe`. The final artifact component remains
unresolved until no-follow validation binds the accepted path to its descriptor.
Inputs above the compressed-byte ceiling fail with
`release artifact exceeds the compressed-byte safety bound`; the parser-facing
wrapper rechecks the live regular descriptor before and after reads and seeks, so
an initially accepted archive that grows past the ceiling fails before the parser
can consume the expanded input. Bytes that change across the descriptor-bound
metadata pass fail with `release artifact changed during verification`.
Accepted-size archives remain subject to all member-count, path, link/device,
metadata-size, decompression, identity, dependency, and digest controls; the
compressed-input check does not replace those independent defenses.
1. accept only a canonical wheel or gzip source distribution;
2. inspect the caller-supplied final artifact path without resolving its final
component, require a regular file, enforce a 256 MiB compressed-byte ceiling,
open without following a final symbolic link where supported, and bind the
opened descriptor to the accepted device and inode before any parser runs;
3. parse and hash only that descriptor, keep every parser-visible read and seek
live-bounded by the same compressed-byte ceiling, bracket metadata parsing
with finite SHA-256 reads, and reject bytes that change during verification;
4. count wheel central-directory records before `zipfile.ZipFile` allocates a
complete `ZipInfo` table, permit at most 10,000 members, and require the
physical record count, record boundaries, directory size, directory offset,
and classic end-of-central-directory counts to agree exactly;
5. reject multi-disk wheels, ZIP64 wheels, truncated or inconsistent central
directories, and malformed extra fields because those formats are not needed
by the bounded canonical release contract;
6. stream the gzip/tar physical headers before semantic parsing, permit at most
10,000 physical members, enforce an aggregate expanded-tar ceiling of
512 MiB, and permit at most 1 MiB for each PAX or GNU extension-header payload;
7. reject malformed or truncated tar headers, checksum errors, links, devices,
FIFOs, sparse forms, unsupported special forms, and nonzero trailing data;
8. use sequential `tarfile` parsing without `getmembers()`, retain only the
bounded seen-name set and one root `PKG-INFO` payload, and preserve the same
semantic path, duplicate, type, and member-count checks after preflight;
9. reject unsafe or duplicate paths, ambiguous metadata, malformed archives,
and metadata larger than 1 MiB, checking declared wheel metadata size before
decompression and bounding source metadata extraction;
10. read exactly one wheel `METADATA` or root source `PKG-INFO` member;
11. verify package identity, license expression, and complete direct runtime
requirement declarations against the reviewed manifest;
12. verify every dependency version, SHA-256, and environment marker against the
executable hash-locked subset in `requirements-ci.txt`, while rejecting
dependency extras that could activate packages outside the reviewed graph;
13. validate identities, reviewed SPDX license identifiers, purls, graph
references, relationships, reachability, and acyclicity;
14. compute the artifact SHA-256 without trusting its filename; and
15. emit sorted UTF-8 CycloneDX 1.7 JSON without timestamps or random identifiers.

## Resource boundaries and failure behavior

The controls are deliberately layered rather than interchangeable:

| Boundary | Exact limit | Enforced before | Purpose |
|---|---:|---|---|
| Compressed release artifact | 256 MiB | hashing or archive parsing | Bounds descriptor-visible input and concurrent growth |
| Archive members | 10,000 | `ZipFile` table creation and semantic tar materialization | Prevents member-table and parser-object amplification |
| Expanded gzip/tar stream | 512 MiB | physical payload skipping or semantic tar parsing | Bounds decompression and aggregate tar processing |
| PAX/GNU extension payload | 1 MiB per header | retaining extension bytes | Bounds parser metadata controlled by archive authors |
| Core package metadata | 1 MiB | email metadata parsing | Bounds `METADATA` and `PKG-INFO` processing |

The direct generator normalizes missing, uninspectable, symbolic-link,
directory, device, FIFO, socket, replaced, and other non-regular artifact inputs
to `release artifact is missing or unsafe`. Inputs above the compressed-byte
ceiling fail with `release artifact exceeds the compressed-byte safety bound`.
The parser-facing wrapper rechecks the live regular descriptor before and after
reads and seeks, so an initially accepted archive that grows past the ceiling
fails before the parser consumes the expanded input. Bytes that differ across
the descriptor-bound metadata pass fail with
`release artifact changed during verification`.

Canonical wheels and ordinary gzip-compressed source distributions remain
accepted. Multi-disk or ZIP64 wheels and sparse or special tar forms fail closed
because they add parser complexity without serving the release profile.
Accepted compressed size does not imply safe expansion: member, expanded-tar,
extension-header, metadata, path, type, dependency, and digest controls remain
independent defenses.

The root component uses a digest-derived `bom-ref`, preventing different
artifacts from sharing evidence identity. The dependency graph is the union
Expand All @@ -65,10 +95,9 @@ The reviewed manifest is
`scripts/ci/release_runtime_dependencies.json`. Its versions, hashes, and
markers must match `requirements-ci.txt`. Lock entries with PEP 508 extras are
invalid for SBOM parity because an extra can introduce additional runtime
requirements that are absent from the reviewed component graph. This prevents
buyer-facing evidence from describing one dependency set while CI executes
another. A dependency change is incomplete until the lock, manifest, tests,
license evidence, and SBOM semantics are reviewed together.
requirements absent from the reviewed component graph. A dependency change is
incomplete until the lock, manifest, tests, license evidence, and SBOM semantics
are reviewed together.

## Generate and verify locally

Expand All @@ -94,24 +123,32 @@ mismatch must fail closed. Never edit generated evidence to fit existing
artifacts; correct the reviewed inputs, rebuild, and regenerate every evidence
file.

Run direct generation from an isolated, read-only exact-artifact directory.
Descriptor identity, bounded reads, physical archive preflight, and digest
bracketing reduce ordinary pathname-replacement, growth, mutation, and resource
amplification risks. They do not make mutable storage immutable: a privileged
writer able to alter and restore the same inode entirely between observations
remains a residual risk.

## Offline operator verification

An operator evaluating an acquired or air-gapped release should:

1. obtain each distribution, CycloneDX JSON, `SHA256SUMS`, and signed
attestation bundle through independently authenticated media;
1. obtain each distribution, CycloneDX JSON, `SHA256SUMS`, and any separately
produced signed attestation bundle through independently authenticated media;
2. verify `SHA256SUMS` before parsing an archive;
3. confirm the SBOM root hash equals the artifact hash;
4. validate the document against the CycloneDX 1.7 JSON schema;
5. verify attestation subject, repository, immutable workflow source, exact
commit, and predicate bytes;
5. when attestations exist, independently verify their subject, repository,
immutable workflow source, exact commit, and predicate bytes;
6. inventory purls, versions, SPDX licenses, markers, and relationships; and
7. reject any digest, identity, schema, workflow, signature, or graph mismatch.

An SBOM is inventory evidence, not proof that a dependency is vulnerability
free, correctly licensed for every use, or benign. Vulnerability assessment,
legal review, provenance verification, and deployment policy remain separate
controls.
free, correctly licensed for every use, benign, reproducibly built, or produced
by a trusted builder. Vulnerability assessment, legal review, provenance
verification, deployment policy, and reproducible-build evidence remain
separate controls.

## Protected release integration

Expand All @@ -128,42 +165,34 @@ current permission contract during protected integration.

Before public GitHub Release publication, verify each attestation against the
exact artifact SHA-256, repository identity, immutable workflow source, exact
protected-main commit, CycloneDX predicate bytes, and release tag.
protected-main commit, CycloneDX predicate bytes, and release tag. Public release
must fail closed on any mismatch.

Public release must fail closed on any mismatch.
A branch must never add a temporary job that publishes, moves refs, writes contents,
pushes to a pull-request branch, self-modifies workflows, or executes
A branch must never add a temporary job that publishes, moves refs, writes
contents, pushes to a pull-request branch, self-modifies workflows, or executes
model-modified source under a write credential.

## Threats, failure, and recovery

These controls address omitted inventory, evidence bound to the wrong artifact,
filename substitution, path replacement between inspection and parsing,
manifest-versus-lock drift, undeclared dependency extras, mutable dependency
resolution, nondeterministic evidence, unsafe archives, compressed-input resource
exhaustion, metadata decompression, stale or wrong-workflow attestations, and
publication before exact verification.

Descriptor identity, live parser bounds, and digest bracketing close ordinary
final-symlink, pathname-replacement, unbounded-growth, and in-place mutation races
during parsing. They do not convert a writable build host into an immutable-storage
system: a privileged writer able to alter and restore the same inode entirely
between verification observations remains a residual mutable-storage risk. Run
evidence generation from an isolated, read-only exact-artifact directory, and
rely on the later sealed-evidence descriptor, digest, and post-publication checks
before any credential-bearing use. No provenance or SLSA claim follows from these
direct-generator controls.

These controls do not detect every compromised upstream source, malicious but
correctly hashed package, license obligation, build-host compromise, or
undisclosed vulnerability. Those risks require provenance, reproducible builds,
vulnerability management, legal review, and hardened runners.
resolution, nondeterministic evidence, unsafe archives, member-table exhaustion,
compressed-input and decompression resource exhaustion, oversized extension or
package metadata, stale or wrong-workflow attestations, and publication before
exact verification.

They do not detect every compromised upstream source, malicious but correctly
hashed package, license obligation, build-host compromise, undisclosed
vulnerability, or privileged mutable-storage attack. Those risks require
provenance, reproducible builds, vulnerability management, legal review,
hardened runners, and sealed or read-only artifact storage.

On any generator, digest, semantic, manifest, lock, or attestation failure,
publish nothing. Correct the source through normal review and regenerate from the
exact accepted commit. Never replace published bytes under an existing version.
If protected main advances, discard the stale attempt and rebuild. Partial
publication requires a new version and transparent changelog entry.
publish nothing. Correct the source through normal review and regenerate from
the exact accepted commit. Never replace published bytes under an existing
version. If protected main advances, discard the stale attempt and rebuild.
Partial publication requires a new version and transparent changelog entry.

## SLSA statement

Expand All @@ -186,9 +215,24 @@ https://github.com/actions/attest
MITRE. (2026). *CWE-400: Uncontrolled resource consumption.* Common Weakness
Enumeration. https://cwe.mitre.org/data/definitions/400.html

MITRE. (2026). *CWE-409: Improper handling of highly compressed data (data
amplification).* Common Weakness Enumeration.
https://cwe.mitre.org/data/definitions/409.html

MITRE. (2026). *CWE-770: Allocation of resources without limits or throttling.*
Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/770.html

Python Software Foundation. (n.d.). *tarfile—Read and write tar archive files.*
Python 3.13 documentation. Retrieved August 7, 2026, from
https://docs.python.org/3.13/library/tarfile.html

Python Software Foundation. (n.d.). *zipfile—Work with ZIP archives.* Python
3.13 documentation. Retrieved August 7, 2026, from
https://docs.python.org/3.13/library/zipfile.html

Python Software Foundation. (n.d.). *zipfile—Work with ZIP archives:
Decompression pitfalls.* Python 3 documentation. Retrieved August 6, 2026, from
https://docs.python.org/3/library/zipfile.html#decompression-pitfalls
Decompression pitfalls.* Python 3.13 documentation. Retrieved August 7, 2026,
from https://docs.python.org/3.13/library/zipfile.html#decompression-pitfalls

Python Packaging Authority. (n.d.). *Core metadata specifications.* Python
Packaging User Guide. Retrieved August 5, 2026, from
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ testpaths = ["tests"]

[tool.coverage.run]
branch = true
source = ["egressweave"]
source_dirs = ["src/egressweave", "scripts/ci"]

[tool.coverage.report]
fail_under = 100
Expand Down
Loading