Skip to main content

Trusted software supply chains with SigStore

Trojanised libraries are an increasingly growing problem in sofware supply chain due to the fact that almost every Java, PHP, Python or Node project typically uses a dozen of third-party libraries which then chain-load further libraries. A compilation of a Java project or installation of Node or Python project is continous stream of third-party libraries loaded from repositories such as Maven, NPM or Pypi — and abuse is just matter of statistics.

SigStore is an interesting development that has a chance to become an industry standard as a project supported by a number of large players but at the same developed in a transparent and open manner. SigStore objective is quite clear: provide a strong, cryptographic signatures to “build artifacts”, which basically means source code bundles, libraries, container images and everything else we currently use as inputs in software build workflows.

SigStore provides the following functionality:

  • Strong, cryptographic signature ensuring integrity and authenticity of the content signed.
  • Timestamping and public ledger of signing certificates and signed content hashes, preventing signature forging, signer account takeover, signature backdating etc.

All of that is done using modern cryptographic algorithms (ECDSA, SHA2) in well-established X.509 framework. On high level, part of this functionality was already in use since 1990’s using PGP web-of-trust model, which had one major deficiency: the latter didn’t work outside of relatively small and closed groups. PGP signatures worked great for signing packages developed as part of a Linux distribution which was able to establish the actual web of trust among its package maintainers who are responsible for establishing similar chains of trust to the upstream packages. They didn’t work very well for very large heterogenic software distributions such as NPM or PyPi, simply because there was no easy and scalable way to establish whether a PGP signatures on a thousand of packages imported by a software project are all generated by their respective authors rather than someone who took over a project with the sole purpose of trojanizing it. The opposite side of the spectrum was Microsoft code signing, which is a proprietary walled garden that applies to a single operating system and doesn’t really cover the use scenarios of building software out of largely open-source libraries.

In its simplest form, SigStore services can be used by python-sigstore implementation:

$ python -m pip install sigstore
$ echo "Hello world" > test.txt
$ sigstore sign test.txt
Waiting for browser interaction...
Using ephemeral certificate:
-----BEGIN CERTIFICATE-----
MIICvzCCAkWgAwIBAgIUdAPJlDvqAAOC+7hs5QtPA/nvHmwwCgYIKoZIzj0EAwMw
...
-----END CERTIFICATE-----

Transparency log entry created at index: 6424650
Signature written to file test.txt.sig
Certificate written to file test.txt.crt

What follows after the initial sigstore command is the primary difference to PGP model, where long-term signing keys are used. SigStore instead relies on short-lived signing keys issued on-demand whenever a signature is needed, and issued to an OpenID-established user identity. Which means, each time you run sigstore manually from command-line, you will need to complete an OpenID/OAuth2 session through a web browser (of course, doing it in automated release pipelines would be inconvenient, so these will use “ambient credentials” which are essentially API tokens and certificates embedded into pipelines as environment variables).

Ultimately it’s your OpenID identity that will end up in the signing certificate — note the email:p+github@krvtz.net attribute in X509v3 Subject Alternative Name section of the certificate. Another attribute https://github.com/login/oauth indicated which OpenID provider supplied this authenticated identity into the SigStore workflow. Relying on OpenID allows a very broad range of authentication levels, from simple automated “ambient credentials” to strong person-bound hardware authenticators for a specific release.

$ openssl x509 -in test.txt.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            74:03:c9:94:3b:ea:00:03:82:fb:b8:6c:e5:0b:4f:03:f9:ef:1e:6c
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: O = sigstore.dev, CN = sigstore-intermediate
        Validity
            Not Before: Nov  3 13:11:03 2022 GMT
            Not After : Nov  3 13:21:03 2022 GMT
        Subject: 
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:35:11:ec:50:a8:c8:5f:61:a2:ef:91:ad:ef:7d:
                    74:70:b0:cc:38:12:39:d3:d8:38:eb:32:ae:17:24:
                    92:78:2d:66:b8:56:17:6a:86:ab:b1:26:90:72:61:
                    52:13:64:02:40:f5:85:71:a8:12:b8:64:9c:aa:31:
                    a8:ef:a8:d9:ea:ee:a9:7c:03:47:c6:85:f5:c9:18:
                    3f:1b:58:46:e8:6b:16:a8:b7:fc:b3:5b:2a:26:cc:
                    16:51:9f:9c:d8:1f:d5
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: 
                Code Signing
            X509v3 Subject Key Identifier: 
                3A:C5:D3:66:B8:53:53:42:95:9A:13:75:F2:8D:20:7D:E5:74:62:CD
            X509v3 Authority Key Identifier: 
                keyid:DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F

            X509v3 Subject Alternative Name: critical
                email:p+github@krvtz.net
            1.3.6.1.4.1.57264.1.1: 
                https://github.com/login/oauth
            CT Precertificate SCTs: 
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : DD:3D:30:6A:C6:C7:11:32:63:19:1E:1C:99:67:37:02:
                                A2:4A:5E:B8:DE:3C:AD:FF:87:8A:72:80:2F:29:EE:8E
                    Timestamp : Nov  3 13:11:03.947 2022 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:46:02:21:00:B6:D1:68:D7:D5:9F:ED:E3:63:44:7A:
                                D7:B3:34:BD:99:A1:AE:E0:54:C1:CC:8D:09:68:0B:18:
                                B4:02:C3:F1:87:02:21:00:C5:EC:3A:53:BC:6E:B8:C3:
                                4C:6E:F4:6A:9D:8A:DF:F2:D3:29:CD:9B:6A:EE:EF:B8:
                                CB:66:DD:B2:A8:22:25:13
    Signature Algorithm: ecdsa-with-SHA384
         30:65:02:30:63:0f:25:36:0d:65:37:1a:b8:95:20:00:99:82:
         24:54:64:44:cc:ca:af:bc:1c:d2:41:1f:81:bd:ef:d1:67:1a:
         6a:cb:56:48:6b:8e:68:d4:71:cd:b3:0c:a5:74:5f:95:02:31:
         00:cd:a8:30:fb:20:6e:fd:cd:b2:d7:76:87:31:bf:45:1a:46:
         b2:9e:af:bf:c8:55:b5:61:df:1c:5b:c6:db:af:79:9c:6a:d4:
         c4:cf:9a:fa:fc:21:a5:dd:58:f5:e0:26:8e

Verification of the test artifact does not require any authentication, it just relies on cryptographic verification of the signature against the file contents, plus checks for presence of the signing certificate and signature in the online transparency logs:

$ sigstore verify test.txt
OK: test.txt

If the file contents were tampered with, the signature will fail:

$ sigstore verify test.txt
FAIL: test.txt
Failure reason: Signature is invalid for input

The above verification will accept any cryptographically valid signature on the file, but it doesn’t establish any link to signer’s identity. There are two extra options for that:

$ sigstore verify --cert-email p+github@krvtz.net --cert-oidc-issuer https://github.com/login/oauth test.txt
OK: test.txt

The --cert-email p+github@krvtz.net option requires that a signature was created by a particular identity confirmed by OpenID Connect (OIDC) to be valid. The second option --cert-oidc-issuer https://github.com/login/oauth that that identity is certified by a particular OIDC platform. The latter alone could be used for example to validate all signatures issued by a particular organisation, not limiting their validity to a particular signer.

Signing and verification can be embedded into automated build pipelines, as demonstrated in one of my Ansible projects, where this release.yml pipeline creates a GitHub release, signs build artifacts with SigStore, attaches them to the release and then triggers an import into Ansible Galaxy. The key part of the pipeline control file written in declarative GitHub Actions langage in YAML format. There’s no explicit OpenID here, as explained above — sigstore uses “ambient credentials” provided by the GitHub platform here:

      - name: Sign release with Sigstore
        uses: sigstore/gh-action-sigstore-python@v0.0.9
        with:
          inputs: ${{ steps.version.outputs.version }}.tar.gz
          release-signing-artifacts: true
          upload-signing-artifacts: true
          
      - name: upload signed asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ github.token }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ${{ steps.version.outputs.version }}.tar.gz
          asset_name: ${{ steps.version.outputs.version }}.tar.gz
          asset_content_type: application/gzip

          
      - name: upload sigstore certificate
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ github.token }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ${{ steps.version.outputs.version }}.tar.gz.crt
          asset_name: ${{ steps.version.outputs.version }}.tar.gz.crt
          asset_content_type: application/x-x509-ca-cert

      - name: upload sigstore signature
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ github.token }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ${{ steps.version.outputs.version }}.tar.gz.sig
          asset_name: ${{ steps.version.outputs.version }}.tar.gz.sig
          asset_content_type: application/octet-stream

      - name: Build and Deploy Collection
        uses: 0x022b/galaxy-role-import-action@1.0.0
        with:
          galaxy_api_key: '${{ secrets.ANSIBLE_GALAXY_TOKEN }}'

This is not implemented by Ansible Galaxy platform as of time of this writing, but the logical next steps would be:

  • Ansible Galaxy checks whether the release from GitHub is accompanied by .sig and .crt files and if yes, verifies them using sigstore and only imports the role if the signature is valid (presence of the signature can be also configured to be mandatory by the maintainer).
  • The ansible-galaxy utility on the consumer system (which may be either end user, or another pipeline builder) does the same thing: check for presence of signature files, and verifies them on download of the role.

Find me on Fediverse, feel free to comment! See how