Skip to main content
Version: 0.1.0-alpha.4 (alpha)

Package SBOMs and bounded content policy

latent-packaging generates and checks a closed CycloneDX JSON 1.6 inventory, embeds it in a package, and checks detached SBOM associations and content policy. The maintained echo observer exports normalized inventory from the build records it already captures. Publisher authentication and the final catalog admission decision remain separate from inventory validation.

decode_sbom_inventory validates the closed normalized input. build_package_with_sbom generates and adds the reserved asset before normal package assembly. generate_cyclonedx_sbom and inspect_cyclonedx_sbom expose the standalone bounded encoder and inspector, preserving an exact-byte digest.

After producing the observed echo inputs, use a fresh output directory:

cargo run -p latent-packaging --example package --locked -- build-with-sbom target/capsules/echo-provenance/package-source.json target/capsules/echo-provenance/sbom-inputs.json target/capsules/echo-provenance target/package-echo-sbom
cargo run -p latent-packaging --example package --locked -- inspect target/package-echo-sbom

The example reads the normalized inventory separately with a 1 MiB cap. The observer's sbom-inputs.json is an unsigned handoff beside its recipe, not an extra package layer. Inspection reports the checked SBOM digest and entry count.

Package identity and exact bytes​

The reserved asset package/sbom.cdx.json uses media type application/vnd.cyclonedx+json. Generate it before package assembly. Its root records logical package name, version and kind; it never contains the final PackageDigest. The manifest then binds the exact SBOM bytes through the asset's SHA-256 and size. This avoids a circular hash dependency.

The inventory excludes itself and package/build-inputs.json, the generated packaging receipt. It records limited input/output coverage rather than claiming a complete dependency closure. Component, WIT and supported output asset rows must match independently inspected package content. Metadata canonicalized by packaging is not mislabeled as an unchanged output file.

Inspection preserves received bytes. A malformed file at the reserved path fails package inspection even when policy permits packages without an SBOM. A checked summary has private construction and bounded retained data; deserializing public inventory input does not create a trusted result.

After assembly, the same SBOM bytes can be attached as application/vnd.latent.sbom.v1 evidence. The existing OCI evidence envelope has exact empty configuration {}, one application/vnd.latent.sbom.payload.v1+json layer, and an outer subject containing the final package media type, digest and size. The payload must equal the unique checked embedded SBOM in digest, length and bytes.

attach_package_sbom borrows the checked bundle's exact payload; inspect_sbom_association verifies supplied envelope/config/payload bytes. Their returned identities carry no publisher authorization.

Matching evidence establishes association. Authenticating a publisher proof for the exact package also binds the embedded inventory; changing its bytes changes package identity. Referrer presence by itself authenticates no author and cannot turn declared dependencies into independently proven program behavior.

Observed inputs and attribution​

The build observer emits sbom-inputs.json while its captured source, selected dependency manifests and compiler records are available. Rust performs the CycloneDX encoding. The observer does not run another compiler or a dependency-resolution command merely to collect inventory.

Cargo compiler-artifact records identify units observed during the maintained build, including cached units. Host build dependencies, procedural macros and build scripts remain distinct from guest target dependencies. A package used in multiple contexts may have multiple role-specific rows. The inventory also records pinned WIT inputs, selected tools and package assets.

Source and license information remains explicit declared attribution or unavailable. Captured registry archive checksums identify lockfile-declared archives; they do not hash unpacked source, a linked binary or an independently verified cache. Manifest metadata read from an approved cache is identified as such. Raw compiler messages, private host paths and credentials are not exported.

Only independently known associations are checked. Opaque Wasm does not expose a complete linked dependency inventory, and compiler output does not prove which instructions or licensed material survived linking. Browser/SSR packages can inventory their assets while application dependencies and licenses remain unavailable when no authoritative inputs were supplied. No dependency edges are invented from flat build-unit records.

Normalized inventory identity and producer helper identities are included in the build observation. That handoff is unsigned until an independently approved builder signs the exact final package and observation. Package publisher and builder roles remain separately approved.

Supported CycloneDX profile​

The profile closes structural fields, rejects unknown properties, and makes source/license status consistent with the corresponding values. Output roles, hash meanings and source identities are constrained rather than accepted as arbitrary property bags. All hash records use SHA-256 and exactly 64 lowercase hexadecimal characters. Generation sorts supported collections for deterministic bytes; inspection does not rewrite an external document.

Inventory roleCycloneDX component type
guest-dependency, build-dependency, proc-macro, build-script, wit-packagelibrary
build-tool, component, rendererapplication
assetfile

The root uses bom-ref=urn:lsf:package:input; fixed metadata includes lsf:profile=lsf-cyclonedx-embedded-1 and lsf:subject-kind=package-content. Package kind is capsule, browser-assets or ssr-package. Dependency completeness is explicitly observed-units-incomplete or declared-inputs-incomplete; neither means a complete linked dependency graph.

Known SPDX declarations use the expression choice, including simple identifiers, with lsf:license-status=declared. A pinned parser validates syntax and identifiers after bounded lexical work. Invalid expressions are not silently repaired or converted to named licenses. Named licenses and standalone license-ID objects are outside this expression-only input profile. The profile does not interpret declarations as a legal compatibility decision.

The upstream CycloneDX JSON 1.6 schema is pinned with its transitive schema references and license under the pinned offline data. It accepts string expressions without validating SPDX grammar, so schema validation alone is insufficient. The supported parser vocabulary is pinned by spdx 0.13.5; optional license-text detection, network lookup and advisory feeds are not enabled.

The observer uses a narrower subset of known nondeprecated SPDX 3.23 license identifiers with bounded uppercase AND/OR and grouping. Unsupported declarations remain unavailable in that producer output. Its derived identifier list excludes exceptions; an exception cannot masquerade as a standalone license. Rust still validates every supplied expression independently, including its supported WITH exceptions, using the pinned parser. This distinction does not upgrade unavailable metadata or claim that the two accepted vocabularies are identical.

Policy and admission boundary​

Content policy explicitly selects optional or required embedded and detached presence, plus source/license requirements for fixed roles. Its canonical digest binds every choice. Supplied malformed, duplicated, conflicting or mismatched evidence fails even when its presence is optional. Policy evaluation does not grant publisher, builder, tenant or execution authority.

The admission owner combines these results with current package publisher proofs, required builder provenance and its own current policy generation at final publication. SBOM policy has no independent trust store, clock owner, hidden network discovery or authority-refresh mechanism.

SbomPolicy::new and from_json validate the explicit policy shape. Use its canonical_bytes() and digest() for policy identity, rather than hashing an unsorted input document. evaluate_sboms returns private package, policy, inventory and referrer identities after checking all supplied associations.

Validation and limits​

Document bytes, JSON depth/nodes, entries, properties, strings and license tokens are bounded before typed retention or sorting. Defaults are hard ceilings that callers may lower. Evidence count and aggregate bytes are checked before inspecting supplied associations. Limits bound supported input, work and retained summaries; they are not exact process-RSS guarantees.

SBOM resourceDefault / hard ceiling
Raw normalized inventory or CycloneDX document1 MiB
Entries / JSON depth / JSON nodes4096 / 12 / 131,072
Generic decoded strings / object members / property array4096 bytes / 24 / 16
Package name and version / entry name and version128 each / 256 and 128 bytes
Source / SPDX expression / SPDX tokens and grouping512 / 1024 bytes / 128 and 16
Output logical path / one recorded content size240 bytes / 256 MiB
Manifest sizePositive, at most 4 MiB

Strings use printable ASCII. Output paths additionally follow the package's portable segment rules. Document, node and field limits apply together, so a combination of individually valid entries may exceed the aggregate budget.

Policy JSON is limited to 4096 bytes, depth 4 and 64 nodes; its two role lists each contain at most nine unique supported roles. Evidence permits at most eight supplied referrers, 4096 bytes per manifest, 1 MiB per payload and 8 * (1 MiB + 4096 + 2) total supplied bytes. The enclosing package and SBOM parser limits still apply independently.

Tests validate generated documents against both the local LSF schema and the offline pinned CycloneDX schema. Rust checks cross-field identity, SPDX syntax, paths, duplicate/conflicting declarations, exact embedded/detached association, policy and reduced resource ceilings. The existing small observed build and disposable authenticated registry exercise the real path without a load campaign.

Run cargo test -p latent-packaging --locked and python -m unittest tools.tests.test_sbom_schemas tools.tests.test_sbom_policy_schema tools.tests.test_sbom_upstream.

See ADR-0024, packaging, publisher trust and OCI transfers.