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 forhello.txt
itselfhello.txt.crt
: a certificate for the signature, containing the public key needed to verify the signaturehello.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!