Code signing with HashiCorp Vault and GitHub Actions

Leverage HashiCorp Vault as a trusted certificate authority (CA) to issue short-lived code signing certificates to a GitHub Actions workflow.

Code signing is an essential element of software supply chain security, enabling users of your code to verify that the code they are running is actually the code you released. This helps defend against supply chain attacks such as dependency confusion attacks by supporting end-users’ ability to verify code authorship and enforce code-integrity controls.

At a high level, code signing workflows depend on four components:

  • An artifact that must be signed before being distributed. Artifacts include things like container images, Windows executables and libraries, and Java .jar files.
  • A code signing scheme that establishes how the artifact is signed and how signatures are verified. Examples of code signing schemes include:
  • A code signing key and certificate that establish the identity of the person or system signing the artifact.
  • A trusted certificate authority (CA) that verifies the identity of a person or system and issues a corresponding code-signing certificate attesting that identity. The end user is ultimately responsible for deciding which CAs should be trusted. Software vendors (Microsoft, Apple, Google, Mozilla) provide default lists of trusted CAs, but enterprises typically tweak those lists to add and remove CAs as needed.

This blog post focuses on leveraging HashiCorp Vault as a trusted CA to issue short-lived code signing certificates to a GitHub Actions workflow that signs a PowerShell script using Microsoft Authenticode.

»Workflow and PKI architecture

The diagram below illustrates the workflow of this solution:

Code signing workflow

Code signing workflow

This workflow uses a two-tier public key infrastructure (PKI). We’ll use OpenSSL to operate the root CA and Vault to operate the code signing issuing CA.

Two-tier PKI for short-lived code signing certificates

Two-tier PKI for short-lived code signing certificates

You can see an entire sample code repository of this post’s solution in the code signing with Vault GitHub repository.

»Generating the root CA

Once you have a copy of the sample code repository linked above, navigate to the root-ca directory, then review and execute the generate script (./generate).

This script will generate an Elliptic Curve P-521 key pair and issue a self-signed root certificate. You’ll find the corresponding certificate in root.crt, and the EC parameters and private key will be in root.key.

»Configuring Vault

The sample code in the repository also includes a HashiCorp Terraform module that provisions the required resources in Vault. Once you have a Vault cluster up and running, take the following three steps (Note: the HashiCorp Cloud Platform, also called HCP, makes this step much easier):

  1. Review the Terraform code in the terraform directory.
  2. Create a Terraform variables file and populate it with values appropriate to your environment. Important: Set pki_codesign_cert to null for the time being (more on that later).
  3. Run terraform apply, review the plan, and deploy the changes to your Vault cluster.

At this point, you’ll have these items in your Vault cluster:

  • PKI secrets engine with
    • The code signing CA’s EC key pair and corresponding certificate signing request (CSR)
    • The PKI role for controlling the issuance of code signing certificates
  • JWT authentication backend for authenticating GitHub Actions pipelines into Vault
  • Sample ACL policy to authorize access to the code signing certificate PKI role

»Issuing Vault’s CA certificate

Since the root CA is offline under OpenSSL, the next step is to have the root CA issue a certificate for Vault’s code signing CA.

Retrieve the Vault CA’s certificate signing request from the Terraform outputs and paste it into a file under the root-ca directory. Name that file codesigning-ca.csr. You can inspect the contents of the CSR with OpenSSL:

openssl req -in codesign-ca.csr -noout -text

If you’re satisfied with the parameters of the CSR, run the sign script (./sign) to have the root CA issue the certificate for Vault's CA. The certificate will be stored in codesign-ca.crt.

Next, import Vault’s CA certificate by modifying your Terraform variables file to include both the Vault CA certificate and the root CA certificate:

pki_codesign_cert = <<EOF
-----BEGIN CERTIFICATE-----
Vault CA cert here
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
Root CA cert here
-----END CERTIFICATE-----
EOF

»Running a GitHub pipeline

The repository for this post includes a sample GitHub workflow you can use to test this code signing pipeline concept.

Note that this is not a production-ready workflow. At the very least, you don’t want to download the Vault CLI every time without further security checks.

»Verifying a signed script

Once the hello-world.ps1 script is signed by your pipeline, you can check its Authenticode signature by running:

Get-AuthenticodeSignature -FilePath .\hello-world.ps1 | Format-List

Be sure that your computer trusts the root CA used for this exercise or signature verification will fail. If you’re not using a discardable environment, remember to remove the root CA certificate from the trust store when you’re done with signature verification.

»Why not use a publicly trusted CA?

Code signing traditionally relies on certificates issued by CAs, such as DigiCert and GlobalSign, that are trusted by the major software vendors. These CAs must verify the identity of the individual or organization requesting the certificate, a process that is often manual, lengthy, and expensive.

If you are distributing software broadly, it makes sense to go through that process: you’ll be issued a certificate that is automatically trusted by all your users by virtue of the CA being part of the major vendors’ trusted CA programs.

But what if you’re looking to secure internal distribution of software, e.g. ensuring that a line-of-business application was built by an approved system and not tampered with since? In that case, ask yourself the following questions:

  • Do you buy a certificate for your organization and allow all your developers to use it? What’s your process for authenticating and authorizing individual signing requests?
  • Do you buy individual developer certificates and incur the expense and overhead of managing the certificates’ lifecycles?
  • If your CA requires you to keep private keys in a hardware token, how do you integrate the signature process into automated build pipelines?

The solution outlined in this post gives your business important benefits when internally distributing software:

  • Minimize the risk of compromised signing keys by adopting short-lived certificates — the key is useless an hour after it’s generated.
  • Reduce toil by automating the issuance of certificates in accordance with rules established in advance by your security team.
  • Future-proof your code signing workflow by leveraging Vault’s many sources of identity. If tomorrow you want users to be able to manually sign code, they can authenticate to Vault with OIDC, SAML, or Kerberos and obtain certificates in the same way.

»Next steps

Learn more about how Vault helps organizations run secure and efficient PKI from this HashiConf talk on automating PKI with Vault, and find out how to build your own certificate authority with Vault on HashiCorp Developer.

Sign up for the latest HashiCorp news

By submitting this form, you acknowledge and agree that HashiCorp will process your personal information in accordance with the Privacy Policy.