How to Use OpenPubkey with GitHub Actions Workloads | Docker
This post was contributed by Ethan Heilman, CTO at BastionZero.
OpenPubkey is the web’s new technology for adding public keys to standard single sign-on (SSO) interactions with identity providers that speak OpenID Connect (OIDC). OpenPubkey works by essentially turning an identity provider into a certificate authority (CA), which is a trusted entity that issues certificates that cryptographically bind an identity with a cryptographic public key. With OpenPubkey, any OIDC-speaking identity provider can bind public keys to identities today.
OpenPubkey is newly open-sourced through a collaboration of BastionZero, Docker, and the Linux Foundation. We’d love for you to try it out, contribute, and build your own use cases on it. You can check out the OpenPubkey repository on GitHub.
In this article, our goal is to show you how to use OpenPubkey to bind public keys to workload identities. We’ll concentrate on GitHub Actions workloads, because this is what is currently supported by the OpenPubkey open source project. We’ll also briefly cover how Docker is using OpenPubkey with GitHub Actions to sign Docker Official Images and improve supply chain security.
What’s an ID token?
Before we start, let’s review the OpenID Connect protocol. Identity providers that speak OIDC are usually called OpenID Providers, but we will just call them OPs in this article.
OIDC has an important artifact called an ID token. A user obtains an ID token after they complete their single sign-on to their OP. They can then present the ID token to a third-party service to prove that they have properly been authenticated by their OP.
The ID token includes the user’s identity (such as their email address) and is cryptographically signed by the OP. The third-party service can validate the ID token by querying the OP’s JSON Web Key Set (JWKS) endpoint, obtaining the OP’s public key, and then using the OP’s public key to validate the signature on the ID token. The OP’s public key is available by querying a JWKS endpoint hosted by the OP.
How do GitHub Actions obtain ID tokens?
So far, we’ve been talking about human identities (such as email addresses) and how they are used with ID tokens. But, our focus in this article is on workload identities. It turns out that Actions has a nice way to assign ID tokens to GitHub Actions.
Here’s how it works. GitHub runs an OpenID Provider. When a new GitHub Action is spun up, GitHub first assigns it a fresh API key and secret. The GitHub Action can then use its API key and secret to authenticate to GitHub’s OP. GitHub’s OP can validate this API key and secret (because it knows that it was assigned to the new GitHub Action) and then provide the GitHub Action with an OIDC ID token. This GitHub Action can now use this ID token to identify itself to third-party services.
When interacting with GitHub’s OP, Docker uses the job_workflow_ref
claim in the ID token as the workflow’s “identity.” This claim identifies the location of the file that the GitHub Action is built from, so it allows the verifier to identify the file that generated the workflow and thus also understand and check the validity of the workflow itself. Here’s an example of how the claim could be set:
job_workflow_ref = octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main
Other claims in the ID tokens issued by GitHub’s OP can be useful in other use cases. For example, there is a field called Actor
or ActorID
, which is the identity of the person who kicked off the GitHub Action. This could be useful for checking that workload was kicked off by a specific person. (It’s less useful when the workload was started by an automated process.)
GitHub’s OP supports many other useful fields in the ID token. You can learn more about them in the GitHub OIDC documentation.
Creating a PK token for workloads
Now that we’ve seen how to identify workloads using GitHub’s OP, we will see how to bind that workload identity to its public key with OpenPubkey. OpenPubKey does this with a cryptographic object called the PK token.
To understand how this process works, let’s go back and look at how GitHub’s OP implements the OIDC protocol. The ID tokens generated by GitHub’s OP have a field called audience
. Importantly, the audience
field is chosen by the OIDC client that requests the ID token. When GitHub’s OP creates the ID token, it includes the audience
along with the other fields (like job_workflow_ref
and actor
) that the OP signs when it creates the ID token.
So, in OpenPubkey, the GitHub Action workload runs an OpenPubkey client that first generates a new public-private key pair. Then, when the workload authenticates to GitHub’s OP with OIDC, it sets the audience
field equal to the cryptographic hash of the workload’s public key along with some random noise.
Now, the ID token contains the GitHub OP’s signature on the workload’s identity (the job_workflow_ref
field and other relevant fields) and on the hash of the workload’s public key. This is most of what we need to have GitHub’s OP bind the workload’s identity and public key.
In fact, the PK token is a JSON Web Signature (JWS) which roughly consists of:
- The ID token, including the
audience
field, which contains a hash of the workload’s public key. - The workload’s public key.
- The random noise used to compute the hash of the workload’s public key.
- A signature, under the workload’s public key, of all the information in the PK token. (This signature acts as a cryptographic proof that the user has access to the user-held secret signing key that is certified in the PK token.)
The PK token can then be presented to any OpenPubkey verifier, which uses OIDC to obtain the GitHub OP’s public key from its JWKS end. The verifier then verifies the ID token using the GitHub OP public key and then verifies the rest of the other fields in the PK token using the workload’s public key. Now the verifier knows the public key of the workload (as identified by its job_workflow_ref
or other fields in the ID token) and can use this public key for whatever cryptography it wants to do.
Can you use ephemeral keys with OpenPubkey?
Yes! An ephemeral key is a key that is only used once. Ephemeral keys are nice because there is no need to store the private key anywhere, which improves security and reduces operational overhead.
Here’s how to do this with OpenPubkey. You choose a public-private key pair, authenticate to the OP to obtain a PK token for the public key, sign your object using the private key, and finally throw away the private key.
One-time-use PK token
We can take this a step further and ensure the PK token may only be associated with a single signed object. Here’s how it works. To start, we take a hash of the object to be signed. Then, when the workload authenticates to GitHub’s OP, set the audience
claim to equal to the cryptographic hash of the following items:
- The public key
- The hash of the object to be signed
- Some random noise
Finally, OpenPubkey verifier obtains the signed object and its one-time-use PK token, and then validates the PK token by additionally checking that the hash of the signed object is included in the audience
claim. Now, you have a one-time-use PK token. You can learn more about this feature of OpenPubkey in the repo.
How will Docker use OpenPubkey to sign Docker Official Images?
Docker will be using OpenPubkey with GitHub Actions workloads to sign Docker Official Images. Every Docker Official Image will be created using a GitHub Action workload. The workload creates a fresh ephemeral public-private key pair, obtains the PK token for the public key via OpenPubkey, and finally signs the image using the private key.
The private key is then deleted, and the image, its signature, and the PK token will be made available on the Docker Hub container registry. This approach is nice because it doesn’t require the signer to maintain or store the private key.
Docker’s container signing use case also relies heavily on The Update Framework (TUF), another Linux Foundation open source project. Read “Signing Docker Official Images Using OpenPubkey” for more details on how it all works.
What else can you do with OpenPubkey and GitHub Actions workloads?
Check out the following ideas on how to put OpenPubkey and GitHub Actions to work for you.
Signing private artifacts with a one-time key
Consider signing artifacts that will be stored in a private repository. You can use OpenPubkey if you want to have a GitHub Action cryptographically sign an artifact using a one-time-use key. A nice thing about this approach is that it doesn’t require you to expose information in a public repository or transparency log. Instead, you need to post the artifact, its signature, and its PK token in the private repository. This capability is useful for private code repositories or internal build systems where you don’t want to reveal to the world what is being built, by whom, when, or how frequently.
If relevant, you could also consider using the actor
and actor-ID
claim to bind the human who builds a particular artifact to the signed artifact itself.
Authenticating workload-to-workload communication
Suppose you want one workload (call it Bob) to process an artifact created by another workload (call it Alice). If the Alice workload is a GitHub Action, the artifact it creates could be signed using OpenPubkey and passed on to the Bob workload, which uses an OpenPubkey verifier to verify it using the GitHub OP’s public key (which it would obtain from the GitHub OP’s JWKS url). This approach might be useful in a multi-stage CI/CD process.
And other things, too!
These are just strawman ideas. The whole point of this post is for you to try out OpenPubkey, contribute, and build your own use cases on it.
Other technical issues we need to think about
Before we wrap up, we need to discuss a few technical questions.
Aren’t ID tokens supposed to remain private?
You might worry about applications of OpenPubkey where the ID token is broadly exposed to the public inside the PK token. For example, in Docker Official Image signing use case, the PK tokens are made available to the public in the Docker Hub container registry. If the ID token is broadly exposed to the public, there is a risk that the ID token could be replayed and used for unauthorized access to other services.
For this reason, we have a slightly different PK token for applications where the PK token is made broadly available to the public.
For those applications, OpenPubkey strips the OP’s signature from the ID token before including it in the PK token. The OP’s signature is replaced with a Guillou-Quisquater (GQ) non-interactive proof-of-knowledge for an RSA signature (which is also known as a “GQ signature”). Now, the ID token cannot be replayed against other services, because the OP’s signature is removed. An ID token without a signature is useless.
So, in applications where the PK token must be broadly exposed to the public, the PK token is a JSON Web Signature, which consists of:
- The ID token excluding the OP’s signature
- A GQ signature on the ID token
- The user’s public key
- The random noise used to compute the hash of the user’s public key
- A signature, under the user’s public key, of all the information in the PK token
The QC signature allows the client to prove that the ID token was validly signed by the identity provider (IdP), without revealing the OP’s signature. The OpenPubkey client generates the QC signature to cryptographically prove that the client knows the OP’s signature on the ID token, while still keeping the OP’s signature secret. GQ signatures only work with RSA, but this is fine because every OpenID Connect provider is required to support RSA.
Because GC signatures are larger and slower than regular signatures, we recommend using them only for use cases where the PK token must be made broadly available to the public. BastionZero’s infrastructure access use case does not use GQ signatures because it does not require the PK token to be made public. Instead, the user only exposes their PK token to the target (e.g., server, container, cluster, database) that they want to access; this is the same way an ID token is usually exposed with OpenID Connect.
GC signatures might not be necessary when authenticating workload-to-workload communications; if the Alice workload is passing the signed artifact and its PK token to the Bob workload only, there is less of a concern that the PK token is broadly available to the public.
What happens when the OP rotates its OpenID Connect key?
OPs have OpenID Connect signing keys that change over time (e.g., every two weeks). What happens if we need to use a PK token after the OP rotates its OpenID Connect key?
For some use cases, the lifetime of a PK token is typically short. With BastionZero’s infrastructure access use case, for instance, a PK token will not be used for longer than 24 hours. In this use case, these timing problems are solved by (1) having user re-authenticate to the IdP and create a new PK token whenever the IdP rotates it’s key, and (2) having the OpenPubkey verifier check that the client also has a valid OIDC Refresh token along with the PK token whenever the ID token expires.
For some use cases, the PK token has a long life, so we do need to worry about OP rotating their OpenID Connect keys. With Docker’s container signing use case, this problem is solved by having TUF additionally store a historical log of the OP’s signing key. Anyone can keep a historical log of the OP public keys for use after they expire. In fact, we envision a future where OP’s might keep this historical log themselves.
That’s it for now! You can check out the OpenPubkey repo on GitHub. We’d love for you to join the project, contribute, and identify other use cases where OpenPubkey might be useful.