Blog.

OCI as attestations storage for your packages

Marco Franssen

Marco Franssen /

12 min read2356 words

Cover Image for OCI as attestations storage for your packages

In my previous blog you can read about securing the software supply chain for Docker images using GitHub actions and Sigstore. We have seen how we can sign our Docker images, as well how to generate an SBOM and build provenance. Using Sigstore/cosign we attached both the signature, SBOM and build provenance to the Docker image. Using Sigstore we get a real nice integration and developer experience to add these security features to our build pipelines for Docker images.

In this blog I want to show an approach to store SBOMs and Provenance in an OCI registry for other software release assets (e.g. npm, maven, nuget, …). To do so we will also utilize cosign to interact with the registry. The developer experience will not be that nice and integrated as we have with Docker images, but at least this can be an approach to move into a more ideal situation untill there is some more integrated solution with package managers.

Using OCI as a blob storage we can leverage this to distribute and discover attestations for software assets that are not OCI/Docker images. The idea is to come up with some naming convention that can relate for example our npm package to a Blob in the OCI registry. Using this convention we can document and make it transparent for consumers of your assets where they can find the SBOM as well the build provenance.

Assuming we already have build provenance (named provenance.att) and an SBOM (named sbom-spdx.json) for a fictitious npm package, our folder structure might look like this.

terminal
$ tree examples/awesome-node-cli
examples/awesome-node-cli
├── bin
│   └── awesome-node-cli.js
├── node_modules
│   └── commander
│       └── …
├── sbom-spdx.json
├── provenance.att
├── package.json
└── yarn.lock

5 directories, 18 files

With this SBOM and provenance generated we can use an OCI registry to distribute and store the SBOM and provenance. See in the next part an example on how to do that using Cosign.

terminal
$ cosign upload blob -f provenance.att ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.provenance
Uploading file from [provenance.att] to [ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.provenance] with media type [text/plain]
File [provenance.att] is available directly at [ghcr.io/v2/marcofranssen/slsa-workflow-examples/attestations/blobs/sha256:f21e2217b67da30183139b8db23db829cd8abb7c9a5a66068e57cde07e29a215]

$ cosign sign --key cosign.key ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.provenance
Pushing signature to: ghcr.io/marcofranssen/slsa-workflow-examples/attestations

$ cosign upload blob -f sbom-spdx.json ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.sbom
Uploading file from [examples/awesome-node-cli/sbom-spdx.json] to [ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.sbom] with media type [text/plain]
File [examples/awesome-node-cli/sbom-spdx.json] is available directly at [ghcr.io/v2/marcofranssen/slsa-workflow-examples/attestations/blobs/sha256:ebdbffa2affe75b8ed7ff2e53cb41943a5201b13ff1a22375270f1c46c754f8c]

$ cosign sign --key cosign.key ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.sbom
Pushing signature to: ghcr.io/marcofranssen/slsa-workflow-examples/attestations

For any other package we can do similar by applying the same tagging convention («oci-registry»:«pkg-name»-«version».«attestation-type») like we applied in previous example.

With these attestations stored in the OCI registry, consumers of our assets can now easily download the attestations and verify its signature that guarantees its authenticity. We can do this using sget. Once downloaded we can write it to a file or pipe it to other shell tools to inspect/transform and use it for our usecases. More on consuming the stored attesations later in this blog, first lets have a look at a tool called fatt, which is a proof of concept (POC) to make the process of publishing and discoverability easier, by automating above steps.

Automating the upload and signing steps

To automate the steps of uploading and signing the SBOMs and provenance further we build fatt as a POC. (⚠️ NOTE the signing is not yet implemented in fatt and still a manual step, see fatt#20).

Fatt requires a few command line arguments to apply the convention we did manually using cosign. To identify the attestation type fatt requires to define a URL Scheme format in front of the attestations that will be uploaded (see the example below).

terminal
$ fatt publish --repository ghcr.io/marcofranssen/slsa-workflow-examples/attestations --tag-prefix awesome-node-cli --version v0.1.5 sbom://sbom-spdx.json provenance://provenance.att
Publishing attestations…
Uploading file from [examples/awesome-node-cli/sbom-spdx.json] to [ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.sbom] with media type [text/plain]
File [examples/awesome-node-cli/sbom-spdx.json] is available directly at [ghcr.io/v2/marcofranssen/slsa-workflow-examples/attestations/blobs/sha256:ebdbffa2affe75b8ed7ff2e53cb41943a5201b13ff1a22375270f1c46c754f8c]
Uploading file from [provenance.att] to [ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.provenance] with media type [text/plain]
File [provenance.att] is available directly at [ghcr.io/v2/marcofranssen/slsa-workflow-examples/attestations/blobs/sha256:f21e2217b67da30183139b8db23db829cd8abb7c9a5a66068e57cde07e29a215]

Generating attestations.txt based on uploaded attestations…
Uploading file from [attestations.txt] to [ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.discovery] with media type [text/plain]
File [attestations.txt] is available directly at [ghcr.io/v2/marcofranssen/slsa-workflow-examples/attestations/blobs/sha256:a978b71603dfe0e1c2f7b4915059d2aefc4bb051ea8710d200cdd927fe22138c]

# These 3 signing steps can be removed once https://github.com/philips-labs/fatt/issues/20 is implemented.
$ cosign sign --key cosign.key ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-npm-cli-v0.1.5.sbom
Pushing signature to: ghcr.io/marcofranssen/slsa-workflow-examples/attestations

$ cosign sign --key cosign.key ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-npm-cli-v0.1.5.provenance
Pushing signature to: ghcr.io/marcofranssen/slsa-workflow-examples/attestations

$ cosign sign --key cosign.key ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-npm-cli-v0.1.5.discovery
Pushing signature to: ghcr.io/marcofranssen/slsa-workflow-examples/attestations

Fatt uses the tagging convention as described earlier in this blog with the manual cosign steps. We also added an additional blob, attestations.txt, that we store in the OCI registry. In this attestations.txt, fatt stores the location of SBOM and provenane in purl format. Purls are commonly used in SBOMs to link to other resources. This attestations.txt is used by fatt filter capabilities to allow some advanced consumption usecases.

terminal
$ fatt list --key cosign.pub ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.discovery
Fetching attestations from ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.discovery…
Verifying signature for ghcr.io/marcofranssen/slsa-workflow-examples/attestations@sha256:252783857d310d36a54086cd1e5035298c98c5db604edc7daa7ce4cdcc03deaf…
pkg:oci/marcofranssen/slsa-workflow-examples/attestations@sha256:68f1d97c61e62228195aedfdfdce74b52123764c0c3b5f71d03b713bc63eb897?repository_url=ghcr.io%2Fmarcofranssen%2Fslsa-workflow-examples%2Fattestations&tag=awesome-node-cli-v0.1.5.sbom
pkg:oci/marcofranssen/slsa-workflow-examples/attestations@sha256:cad11e9b6229631175321b6e35dfbf0124064ef82761e41800bc533ecc3e8f06?repository_url=ghcr.io%2Fmarcofranssen%2Fslsa-workflow-examples%2Fattestations&tag=awesome-node-cli-v0.1.5.provenance

At this stage we have our attestations published, either via cosign or using fatt as a convenience. As we used a convention it should be more recognizable for consumers of your packages where to find the attestations. This enables consumers to automate the consumption of the attestations. E.g.:

  • Check SBOMs for license violations (e.g. GPL)
  • Check SBoms for vulnerabilities
  • Check provenance if artifact was produced by a trusted build environment

Consuming

We can utilize sget to fetch a specific blob from an OCI registry. Sget is part of the Sigstore tools. It downloads the given resource and automatically verifies the signature of this artifact to proof the authenticity.

COSIGN_EXPERIMENTAL=1 sget ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.discovery

In my case I used the ephemeral keys from sigstore (COSIGN_EXPERIMENTAL=1). If you used selfsigned keys you will have to use the --key flag to provide your public key.

SBOM example

Fetching by tag requires signature verification. In this case you will receive 2 json objects. One containing the signature data and one containing your SBOM. To get the SBOM contents we can use jq.

terminal
$ COSIGN_EXPERIMENTAL=1 sget ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.sbom | grep -v '^Certificate' | jq --slurp '.[1] | .packages'

Verification for ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.sbom --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - Any certificates were verified against the Fulcio roots.
[
  {
    "SPDXID": "SPDXRef-8f4c75ddef2e1d03",
    "name": "commander",
    "licenseConcluded": "NONE",
    "downloadLocation": "NOASSERTION",
    "externalRefs": [
      {
        "referenceCategory": "SECURITY",
        "referenceLocator": "cpe:2.3:a:commander:commander:9.0.0:*:*:*:*:*:*:*",
        "referenceType": "cpe23Type"
      },
      {
        "referenceCategory": "SECURITY",
        "referenceLocator": "cpe:2.3:a:*:commander:9.0.0:*:*:*:*:*:*:*",
        "referenceType": "cpe23Type"
      },
      {
        "referenceCategory": "PACKAGE_MANAGER",
        "referenceLocator": "pkg:npm/commander@9.0.0",
        "referenceType": "purl"
      }
    ],
    "filesAnalyzed": false,
    "licenseDeclared": "NONE",
    "sourceInfo": "acquired package info from installed node module manifest file: yarn.lock",
    "versionInfo": "9.0.0"
  }
]

In case you fetch the asset by digest you can omit the signature verification. However the digest is less userfriendly to remember and type. Small advantage is you will not receive the signature in the output of the command.

terminal
$ sget ghcr.io/marcofranssen/slsa-workflow-examples/attestations@sha256:68f1d97c61e62228195aedfdfdce74b52123764c0c3b5f71d03b713bc63eb897 | jq '.packages'
[
  {
    "SPDXID": "SPDXRef-8f4c75ddef2e1d03",
    "name": "commander",
    "licenseConcluded": "NONE",
    "downloadLocation": "NOASSERTION",
    "externalRefs": [
      {
        "referenceCategory": "SECURITY",
        "referenceLocator": "cpe:2.3:a:commander:commander:9.0.0:*:*:*:*:*:*:*",
        "referenceType": "cpe23Type"
      },
      {
        "referenceCategory": "SECURITY",
        "referenceLocator": "cpe:2.3:a:*:commander:9.0.0:*:*:*:*:*:*:*",
        "referenceType": "cpe23Type"
      },
      {
        "referenceCategory": "PACKAGE_MANAGER",
        "referenceLocator": "pkg:npm/commander@9.0.0",
        "referenceType": "purl"
      }
    ],
    "filesAnalyzed": false,
    "licenseDeclared": "NONE",
    "sourceInfo": "acquired package info from installed node module manifest file: yarn.lock",
    "versionInfo": "9.0.0"
  }
]

Provenance example

See here an example of getting the provenance predicate that describes how the awesome-node-cli asset was produced.

terminal
$ COSIGN_EXPERIMENTAL=1 sget ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.provenance | grep -v '^Certificate' | jq --slurp '.[1] | .predicate'

Verification for ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.provenance --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - Any certificates were verified against the Fulcio roots.
{
  "builder": {
    "id": "https://github.com/marcofranssen/slsa-workflow-examples/Attestations/GitHubHostedActions@v1"
  },
  "buildType": "https://github.com/Attestations/GitHubActionsWorkflow@v1",
  "invocation": {
    "configSource": {
      "entryPoint": "Release NPM",
      "uri": "git+https://github.com/marcofranssen/slsa-workflow-examples",
      "digest": {
        "sha1": "01e93c44cd41e7f6025c815af1f5075a4fcbec8b"
      }
    },
    "parameters": null,
    "environment": null
  },
  "metadata": {
    "buildInvocationId": "https://github.com/marcofranssen/slsa-workflow-examples/actions/runs/2070200957",
    "buildFinishedOn": "2022-03-31T09:40:17Z",
    "completeness": {
      "parameters": true,
      "environment": false,
      "materials": false
    },
    "reproducible": false
  },
  "materials": [
    {
      "uri": "git+https://github.com/marcofranssen/slsa-workflow-examples",
      "digest": {
        "sha1": "01e93c44cd41e7f6025c815af1f5075a4fcbec8b"
      }
    }
  ]
}

Similarly like the SBOM you could also fetch the provenance by digest, which does not require signature checks and slightly changes how you apply jq in further processing.

Discovery file example

We have seen fatt was also adding the attestations.txt as an addition to the cosign steps (upload and sign). fatt list is similar to sget when it comes to fetching the ….discovery tag. It will download the blob, verify the signature and adds some additional capabilities to make more advanced commandline usecases possible. e.g.:

  • Filtering
  • Transforming purls to oci image urls
  • Working directly with the OCI blob
  • Traversing the local file system on multiple attestations.txt files

See here some examples:

Store the attestations in a local file

terminal
$ export COSIGN_EXPERIMENTAL=1
$ fatt list ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.discovery > attestations.txt
Fetching attestations from ghcr.io/marcofranssen/slsa-workflow-examples/attestations:awesome-node-cli-v0.1.5.discovery…
Verifying signature for ghcr.io/marcofranssen/slsa-workflow-examples/attestations@sha256:252783857d310d36a54086cd1e5035298c98c5db604edc7daa7ce4cdcc03deaf…

Recursively search the local file system for attestation.txt and filter by provenance

terminal
$ export COSIGN_EXPERIMENTAL=1
$ fatt list -f '{ .IsAttestationType("provenance") }'
Fetching attestations from /Users/marco/code/slsa-workflow-examples/examples/awesome-npm-cli…
pkg:oci/marcofranssen/slsa-workflow-examples/attestations@sha256:cad11e9b6229631175321b6e35dfbf0124064ef82761e41800bc533ecc3e8f06?repository_url=ghcr.io%2Fmarcofranssen%2Fslsa-workflow-examples%2Fattestations&tag=awesome-node-cli-v0.1.5.provenance

Recursively search the local file system for attestation.txt and filter by SBOM and output the OCI blob URLs

terminal
$ export COSIGN_EXPERIMENTAL=1
$ fatt list -o oci -f '{ .IsAttestationType("sbom") }'
Fetching attestations from /Users/marco/code/slsa-workflow-examples/examples/awesome-npm-cli…
ghcr.io/marcofranssen/slsa-workflow-examples/attestations@sha256:68f1d97c61e62228195aedfdfdce74b52123764c0c3b5f71d03b713bc63eb897

Specifically this last example is usefull to further automate things to automatically fetch all SBOMs for projects on your local file system (all projects do need to have an attestations.txt in their workspace and have to share the same root path to allow scanning this filepath recursively).

e.g. if you would use fatt or sget to download the attestations.txt and store them on your local disc you might end up with a folder structure like this example.

terminal
$ tree code
code
├── node-cli
│   ├── attestations.txt
│   └── …
├── another-node-cli
│   ├── attestations.txt
│   └── …
└── super-cool-go-cli
    ├── attestations.txt
    └── …

55 directories, 299 files
$ while IFS= read -r line ; do sget "${line}" | grep -v '^Certificate'; done \
    <<< "$(fatt list -f '{ .IsAttestationType("sbom") }' -o oci code)" \
    | jq --slurp 'map(.packages) | flatten | map({ name: .name, version: .versionInfo, license: .licenseConcluded }) | unique'
Fetching attestations from /Users/marco/code…
[
  {
    "name": "commander",
    "version": "9.0.0",
    "license": "NONE"
  },
  {
    "name": "commander",
    "version": "8.0.1",
    "license": "NONE"
  },
  {
    "name": "github.com/sigstore/cosign",
    "version": "1.6.0",
    "license": "NONE"
  }
]

GitHub actions

To see a full end to end example of a CI pipeline you can see following workflow.

.github/workflows/release-npm.yaml

In this workflow we make a release for our software using npm pack. Then we utilize syft to generate an SBOM and we use the slsa-provenance-action to generate the provenance for our build. Last but not least we utilize fatt to publish the sbom and provenance to an OCI registry.

The second job in the workflow showcases some ways to consume the attestations. This gives you an impression on the possibilities of using these attestations in your workflows. E.g. you could check your dependencies in npm, try to fetch the SBOMs based on a convention, and if the SBOM is provided by the dependency you can perform some license checks in your CI pipeline.

Bonus

Using crane you can easily query the OCI registry to see how the attestations are stored for your software assets.

terminal
$ crane ls ghcr.io/marcofranssen/slsa-workflow-examples/attestations
v0.1.4-rc2.sbom
v0.1.4-rc2.provenance
v0.1.4-rc2.discovery
sha256-2303915bddc4a07184bbead2c57cc157eae79529c381c54c3c731a4d37ee8c6c.sig
sha256-9b42b04bad23bedb88b72ece1e69d2548c45d6bf18a92eba205dab975136bed2.sig
sha256-6b4c73905e7065f821aa10f26a6a96fead849c31b928ef51ecc079a7d2eebc8d.sig
awesome-node-cli-v0.1.4-rc3.sbom
awesome-node-cli-v0.1.4-rc3.provenance
awesome-node-cli-v0.1.4-rc3.discovery
sha256-f7bb6199092fa87d26b9a8195842eb87f7f907e80f95db07d21b73616e271aec.sig
sha256-49a0dfce851be9d88d3b73d525b2e9b950d546f1d7d7046050ed1f058a7cefb1.sig
sha256-c6184be9cf4c8d35aa26e4f1975f1fbd9b91d756a5ea2262b258293f369bb49c.sig
awesome-node-cli-v0.1.4.sbom
awesome-node-cli-v0.1.4.provenance
awesome-node-cli-v0.1.4.discovery
sha256-d3f5585432c887e626717dc04df80f97775a4ff4a5f721b62c0466a63b865c60.sig
sha256-84a13d69302341f15b8fb7e8b645b12309956546136fa218adc7225e4f6b5aaf.sig
sha256-1cb77a219aeb2165463660e6fd908dca27e72c1481ff79d5fe49feced2bd7bca.sig
awesome-node-cli-v0.1.5-rc.sbom
awesome-node-cli-v0.1.5-rc.provenance
awesome-node-cli-v0.1.5-rc.discovery
sha256-ad3b62e3793d6354292e63dba82b455decd5c6bcdf93b94f631ea6cba56aeff5.sig
sha256-394cf9cd05af02d2352e5f528a84a0bbb55a9424db6173b968402587b6c67a36.sig
sha256-26be1c1d7d8985ec8f770c9463bc8558c9abbed8b5d8632b2084dbcb063e9808.sig
awesome-node-cli-v0.1.5.sbom
awesome-node-cli-v0.1.5.provenance
awesome-node-cli-v0.1.5.discovery
sha256-252783857d310d36a54086cd1e5035298c98c5db604edc7daa7ce4cdcc03deaf.sig
sha256-cad11e9b6229631175321b6e35dfbf0124064ef82761e41800bc533ecc3e8f06.sig
sha256-68f1d97c61e62228195aedfdfdce74b52123764c0c3b5f71d03b713bc63eb897.sig

As you can see here, we have a signature for each released tag. You can also see that we didn't use the fatt commandline option --tag-prefix in an earlier release (v0.1.4-rc2). This commandline option allows you to choose between 2 publishing strategies:

  • A repository per package
    • ghcr.io/marcofranssen/attestations-awesome-node-cli:v0.1.5.sbom
    • ghcr.io/marcofranssen/attestations-another-node-cli:v0.1.5.sbom
  • 1 repository for multiple packages
    • ghcr.io/marcofranssen/attestations:awesome-node-cli-v0.1.5.sbom
    • ghcr.io/marcofranssen/attestations:another-node-cli-v0.1.5.sbom

Wrapup

In this blog we have stored attestations (SBOM, build provenance) in an OCI registry. We also have seen how we could automate those steps a little further using fatt. Aside from the publishing part we also looked at the consumption usecases. The examples where based on an NPM package, however we can apply the same for other package managers like Maven, Nuget, Go modules etc. Also take some time to explore the GitHub actions workflow to incorporate this in your own projects.

What do you think about this approach and the proof of concept using fatt? Do you see any other usecases to be applied or do you have any suggestions on taking this idea next-level? Feel free to reach out at the fatt repository, discuss at Twitter, Reddit or any other Social platform. Looking forward to your feedback.

References

You have disabled cookies. To leave me a comment please allow cookies at functionality level.

More Stories

Cover Image for Secure your software supply chain using Sigstore and GitHub actions

Secure your software supply chain using Sigstore and GitHub actions

Marco Franssen

Marco Franssen /

With the rise of software supply chain attacks it becomes more important to secure our software supply chains. Many others have been writing about software supply chain attacks already, so I won't repeat that over here in this article. Assuming you found my article, because you want to know how to prevent them. In this blogpost I want to show you how to secure the software supply chain by applying some SLSA requirements in the GitHub actions workflow. We will utilize Sigstore to sign and attest…

Cover Image for Globally configure multiple git commit emails

Globally configure multiple git commit emails

Marco Franssen

Marco Franssen /

Have you ever been struggling to commit with the right email address on different repositories? It happened to me many times in the past, but for a couple of years I'm now using an approach that prevents me from making that mistake. E.g. when working on your work related machine, I'm pretty often also working on Opensource in my spare time, to build my own skills, and simply because I believe in the cause of Opensource. Also during work time I'm also sometimes contributing fixes back to Opensour…