By William Woodruff

Read the official announcement on the Sigstore blog as well!

Trail of Bits is thrilled to announce the first stable release of sigstore-python, a client implementation of Sigstore that we’ve been developing for nearly a year! This work has been graciously funded by Google’s Open Source Security Team (GOSST), who we’ve also worked with to develop pip-audit and its associated GitHub Actions workflow.

If you aren’t already familiar with Sigstore, we’ve written an explainer, including an explanation of what Sigstore is, how you can use it on your own projects, and how tools like sigstore-python fit into the overall codesigning ecosystem.

If you want to get started, it’s a single pip install away:

$ echo 'hello sigstore' > hello.txt
$ python -m pip install sigstore
$ sigstore sign hello.txt
$ sigstore verify identity hello.txt \
    --cert-identity '[email protected]' \
    --cert-oidc-issuer 'https://example.com'

A usable, reference-quality Sigstore client implementation

Our goals with sigstore-python are two-fold:

  • Usability: sigstore-python should provide an extremely intuitive CLI and API, with 100 percent documentation coverage and practical examples for both.
  • Reference-quality: sigstore-python is just one of many Sigstore clients being developed, including for ecosystems like Go, Ruby, Java, Rust, and JavaScript. We’re not the oldest implementation, but we’re aiming to be one of the most authoritative in terms of succinctly and correctly implementing the intricacies of Sigstore’s security model.

We believe we’ve achieved both of these goals with this release. The rest of this post will show off demonstrate how we did so!

Usability: sigstore-python is for everybody

The sigstore CLI

One of the Sigstore project’s mottos is “Software Signing for Everybody,” and we want to stay true to that with sigstore-python. To that end, we’ve designed a public Python API and sigstore CLI that abstract the murkier cryptographic bits away, leaving the two primitives that nearly every developer is already familiar with: signing and verifying.

To get started, we can install sigstore-python from PyPI, where it’s available as sigstore:

$ python -m pip install sigstore
$ sigstore --version
sigstore 1.0.0

From there, we can create an input to sign, and use sigstore sign to perform the actual signing operation:

$ echo "hello, i'm signing this!" > hello.txt
$ sigstore sign hello.txt

Waiting for browser interaction...
Using ephemeral certificate:
-----BEGIN CERTIFICATE-----
MIICwDCCAkagAwIBAgIUOZ3vPindiCHATxvCRQk/TC5WAd0wCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjMwMTEwMTkzNDI5WhcNMjMwMTEwMTk0NDI5WjAAMHYwEAYH
KoZIzj0CAQYFK4EEACIDYgAETb8dcUgXs31y6tjgsVy8KwfMEzVvhUVs7jlzcwkN
MLICjVvblYtWfFReYMEN8rM8mfglyAwcW+qY/I3klMnMcf/bna/yazzP7Mnnh1g1
dzlOXh14C9iZMDPIV0KHH5u2o4IBSDCCAUQwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud
JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBQdX9zi1TPEHw2uAkqaCE2ecWMLTDAf
BgNVHSMEGDAWgBTf0+nPViQRlvmo2OkoVaLGLhhkPzAjBgNVHREBAf8EGTAXgRV3
aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo
dWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbH
ETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGFnS1KGwAABAMARjBEAiAns85i
YPmlq9RWfJOUwCRN4y5Lwvk3/Y1cWB9wNW4XMwIgBRfib3YbotTgGpB16F/5uf7r
mO2Jc7e0yElimghFFmkwCgYIKoZIzj0EAwMDaAAwZQIxAOh0Ob8Mi2lENgRNjMRe
L8r8rBoVRSi8BzJHcKAe+eTwLsjvsdryJ0yKg5HVHc2erQIwNpdUXD71OPqs3QQ4
Ka+Q2Pjcs+GV5TvaecGzJuQGbm2J5ZW5raPJrXngEGUldt0U
-----END CERTIFICATE-----

Transparency log entry created at index: 10892071
Signature written to hello.txt.sig
Certificate written to hello.txt.crt
Rekor bundle written to hello.txt.rekor

On your desktop this will produce an OAuth2 flow that prompts you for authentication, while on supported CI providers it’ll intelligently select an ambient OpenID Connect identity!

This will produce three outputs:

  • hello.txt.sig: the signature for hello.txt itself
  • hello.txt.crt: a certificate for the signature, containing the public key needed to verify the signature
  • hello.txt.rekor: an optional “offline Rekor bundle” that can be used during verification instead of accessing an online transparency log

Verification looks almost identical to signing, since the sigstore CLI intelligently locates the signature, certificate, and optional Rekor bundle based on the input’s filename. To actually perform the verification, we use the sigstore verify identity subcommand:

$ # finds hello.txt.sig, hello.txt.crt, hello.txt.rekor
$ sigstore verify identity hello.txt \
    --cert-identity [email protected] \
    --cert-oidc-issuer https://github.com/login/oauth
OK: hello.txt

(What’s with the extra flags? Without them, we’d just be verifying the signature and certificate, and anybody can get a valid signature for any public input in Sigstore. To make sure that we’re actually verifying something meaningful, the sigstore CLI forces you to assert which identity the signature is expected to be bound to, which is then checked during certificate verification!)

However, that’s not all! Sigstore is not just for email identities; it also supports URI identities, which can correspond to a particular GitHub Actions workflow run, or some other machine identity. We can do more in-depth verifications of these signatures using the sigstore verify github subcommand, which allows us to check specific attestations made by the GitHub Actions runner environment:

$ # change this to any version!
$ v=0.10.0
$ repo=https://github.com/sigstore/sigstore-python
$ release="${repo}/release/download"
$ sha=66581529803929c3ccc45334632ccd90f06e0de4


$ # download a distribution + certificate and signature
$ wget ${release}/v${v}/sigstore-${v}.tar.gz{,.crt,.sig}

$ # verify extended claims
$ sigstore verify github sigstore-${v}.tar.gz \
    --cert-identity \
      "${repo}/.github/workflows/[email protected]/tags/v${v}" \
    --sha ${sha} \
    --trigger release

This goes well beyond what we can prove with just a bare sigstore verify identity command: we’re now asserting that the signature was created by a release-triggered workflow run against commit 66581529803929c3ccc45334632ccd90f06e0de4, meaning that even if an attacker somehow managed to compromise our repository’s actions and sign for new inputs, they still couldn’t fool us into accepting the wrong signature for this release!

(--sha and --trigger are just a small sample of the claims that can be verified via sigstore verify github: check the README for even more!)

The brand-new sigstore Python APIs

In addition to the CLIs above, we’ve stabilized a public Python API! You can use this API to do everything that the sigstore CLI is capable of, as well as more advanced verification techniques (such as complex logical chains of “policies”).

Using the same signing example above, but with the Python APIs instead:

import io

from sigstore.sign import Signer
from sigstore.oidc import Issuer

contents = io.BytesIO(b"hello, i'm signing this!")

# NOTE: identity_token() performs an interactive OAuth2 flow;
# see other members of `sigstore.oidc` for other credential
# mechanisms.
issuer = Issuer.production()
token = issuer.identity_token()


signer = Signer.production()
result = signer.sign(input_=contents, identity_token=token)
print(result)

And the same identity-based verification:

import base64
from pathlib import Path

from sigstore.verify import Verifier, VerificationMaterials
from sigstore.verify.policy import Identity

artifact = Path("hello.txt").open()
cert = Path("hello.txt.crt").read()
signature = Path("hello.txt.sig").read_bytes()
materials = VerificationMaterials(
    input_=artifact,
    cert_pem=cert,
    signature=base64.b64decode(signature),
    offline_rekor_entry=None,
)

verifier = Verifier.production()

result = verifier.verify(
    materials,
    Identity(
        identity="[email protected]",
        issuer="https://github.com/login/oauth",
    ),
)
print(result)

The Identity policy corresponds to the sigstore verify identity subcommand, and hints at the Python API’s ability to express more complex relationships between claims. For example, here is how we could write the sigstore verify github verification from above:

from sigstore.verify import Verifier
from sigstore.verify.policy import (
    AllOf,
    GitHubWorkflowSHA,
    GitHubWorkflowTrigger,
    Identity
)

materials = ...

verifier = Verifier.production()

result = verifier.verify(
    materials,
    AllOf(
        [
            Identity(identity="...", issuer="..."),
            GitHubWorkflowSHA(
                "66581529803929c3ccc45334632ccd90f06e0de4"
            ),
            GitHubWorkflowTrigger("release"),
        ]
    )
)

…representing a logical AND between all sub-policies.

What’s next?

We’re making a commitment to semantic versioning for sigstore-python’s API and CLI: if you depend on sigstore~=1.0 in your Python project, you can safely assume that we will not make changes that break either without a major version bump.

With that in mind, a stable API enables many of our near-future goals for Sigstore in the Python packaging ecosystem: further integration into PyPI and the client-side packaging toolchain, as well as stabilization of our associated GitHub Action.

Work with us!

Trail of Bits is committed to the long term stability and expansion of the Sigstore ecosystem. If you’re looking to get involved in Sigstore or are working with your company to integrate it into your own systems, get in touch!