Supply chain: SBOMs and attestations¶
Every release publishes, next to the nine NuGet packages, one CycloneDX SBOM per package and two signed attestations per package. This page says what is in them, what is deliberately not, and how to check them.
Is this required?¶
Not for this project, as far as the EU Cyber Resilience Act (Regulation (EU) 2024/2847) is concerned — and this is a reading of the text, not legal advice. The CRA's obligations fall on manufacturers who make a product available in the course of a commercial activity; a library released under the MIT licence without payment or paid support is not that. Where it does apply — to whoever builds a product with these packages — the vulnerability-handling duty in Annex I, Part II, point 1 asks the manufacturer for an SBOM "in a commonly used and machine-readable format covering at the very least the top-level dependencies", kept in the technical documentation. It does not have to be published. At the time of writing the Commission has not adopted an implementing act that fixes the SBOM format (Article 13(24)).
So the SBOMs here are voluntary, and for the benefit of the people who must have one: a manufacturer who ships a product built on CanKit.Pro can take the package's SBOM as the input for their own, instead of reconstructing the dependency closure by hand.
Nothing here is a certification. In particular, no one has checked these SBOMs against a conformance tester, and the CRA does not name a technical standard for them yet.
What is published¶
| Artefact | Where | Made by |
|---|---|---|
CanKit.Pro.<Name>.cdx.json — CycloneDX 1.6 JSON, one per package |
assets of the GitHub Release | eng/generate-sboms.py |
SBOM attestation — the SBOM bound to the .nupkg it describes |
GitHub attestation store | actions/attest |
Build-provenance attestation — which workflow and commit built the .nupkg |
GitHub attestation store | actions/attest |
The SBOM describes the package, and lists everything installing it resolves to — across both target
frameworks, so the list is a superset: it includes the netstandard2.0 polyfills (System.Memory and
friends) a net10.0 consumer never installs. An SBOM that omits something a consumer might get is worse
than one that lists a package they happen not to.
How it meets BSI TR-03183-2¶
The CRA names no format, so the usable reference is the German BSI's technical guideline
TR-03183-2 (CycloneDX 1.6 or newer, or SPDX 3.0.1 or newer). The
mapping below is the author's reading of version 2.x, and eng/verify-sboms.py enforces what it can of it
on every pull request and again before a release.
| TR-03183-2 field | In the SBOM |
|---|---|
| SBOM creator | metadata.manufacturer — a URL, not an e-mail (the guideline allows either) |
| Timestamp | metadata.timestamp |
| Component creator | manufacturer on the package and on every component; for third parties the author the package declares, with the project's own URL or its nuget.org page |
| Name, version | name, version |
| Filename | bsi:component:filename — the .nupkg name |
| Dependencies and their completeness | dependencies (a node for every component, leaves included) and a compositions entry with aggregate: complete |
| Distribution licence | licenses, as an SPDX id; eng/verify-sboms.py rejects a free-text name |
| Hash of the deployable | SHA-512 in hashes. For CanKit.Pro.* it is the hash of the .nupkg in the release, computed from that file |
| Executable / archive / structured | bsi:component:executable = non-executable, bsi:component:archive = archive, bsi:component:structured = structured |
| Source code URI, URI of the deployable | externalReferences of type vcs and distribution |
| Unique identifier | purl (pkg:nuget/…) |
Beyond the guideline: externalReferences of type security-contact and advisories point at the
security policy, and lifecycles says the data
comes from the build.
Where the reading is a judgement¶
- Granularity is the package. The DLLs inside a
.nupkgare not listed as separate components. The guideline asks that an archive's contents be listed; for a NuGet package the contents are the package, and its third-party dependencies are listed. Whether a stricter reading would want the assemblies too is not settled by anything this repository can check. - A
.nupkgis markednon-executable. It is a zip; the code is in the assemblies inside. - The creator is a URL. Add a contact e-mail to
CREATORineng/generate-sboms.pyif you want one. - "Complete" means the resolved NuGet closure. The .NET runtime a consumer runs on is theirs.
- Build-time-only packages are left out (
NETStandard.Library,Microsoft.NETCore.Platforms): they are not part of what ships or what a consumer restores, and one of them is licensed under a non-SPDX Microsoft licence. - Licences the tool cannot map are mapped by exact
licenseUrlineng/generate-sboms.py(LICENSE_URLS). A new dependency whose.nuspecgives only an unknown URL makes the SBOM step fail, on the pull request that adds it; the fix is to add the URL to that table after reading what it says.
An SBOM is a list of components, not a statement that any of them is free of vulnerabilities. Nothing here replaces a vulnerability scan of it (see below) or a VEX statement; this repository publishes neither.
Checking a release¶
Take any package and its SBOM from the release page, then:
# The package was built by this repository's release workflow, from the commit it names.
gh attestation verify CanKit.Pro.RawCan.<version>.nupkg --repo dborgards/CanKit.Pro
# The SBOM is the one attested for that package.
gh attestation verify CanKit.Pro.RawCan.<version>.nupkg --repo dborgards/CanKit.Pro \
--predicate-type https://cyclonedx.org/bom
# The SHA-512 in the SBOM is the package's.
sha512sum CanKit.Pro.RawCan.<version>.nupkg
jq -r '.metadata.component.hashes[] | select(.alg=="SHA-512") | .content' CanKit.Pro.RawCan.cdx.json
To scan an SBOM for known vulnerabilities, import it into
Dependency-Track or feed it to
Grype (grype sbom:CanKit.Pro.RawCan.cdx.json).
To validate against the schema (the repository's own check is structural and does not do this):
git clone --depth 1 https://github.com/CycloneDX/specification
pip install check-jsonschema
check-jsonschema --schemafile specification/schema/bom-1.6.schema.json CanKit.Pro.RawCan.cdx.json
Maintaining it¶
dotnet pack CanKit.Pro.sln -c Release -p:Version=9.9.9 -o artifacts/nuget
python3 eng/generate-sboms.py artifacts/nuget 9.9.9 artifacts/sbom
python3 eng/verify-sboms.py artifacts/sbom artifacts/nuget 9.9.9
python3 -m unittest discover -s eng -p "test_*.py" # the checks' own tests
verify-sboms.py cross-checks every SBOM against its package's .nuspec: each declared dependency must be
listed, at a version no lower than the floor the .nuspec gives it (the SBOM is the restored closure, so it
may sit above a floor, never below). Where the target frameworks declare different floors, the highest
version listed has to reach the highest floor.
The SBOMs are generated in the release workflow's verify job — the one with no credentials — from the
packages it has just packed, and handed on as an artifact. prepareCmd checks them again against the
downloaded packages before the tag exists, and .releaserc.json attaches them to the GitHub Release.
The attestations are made by the attest job, after the release, with its own id-token and
attestations permissions and no other credential. After, because an attestation for a version that then
fails to publish would vouch for something nobody can install. If that job fails, Re-run failed jobs
repeats it alone against the retained artifacts; nothing is released twice.
CycloneDX is pinned in .config/dotnet-tools.json. Its version changes the SBOM's content, not the
packages', so Dependabot groups it with the test tooling and a bump publishes no release.