DEV Community

Katz Sakai
Katz Sakai

Posted on

How to Set Up Code Signing for Windows Apps in GitHub Actions CI/CD Pipelines

Overview

When distributing Windows applications via installers, it is standard practice to code sign the binaries before distribution. Code signing proves that the binary has not been tampered with and verifies the identity of the publisher.

With that in mind, I investigated what kind of architecture is needed to code sign Windows apps within a CI/CD pipeline on GitHub Actions.

The Big Picture

The overall architecture of the system built on GitHub Actions looks roughly like the diagram below.

System Diagram

When integrating a code signing process into a CI/CD pipeline on GitHub Actions, the code signing private key must be stored in a cloud-based HSM (Hardware Security Module) that is accessible from the Windows machine running on GitHub Actions.1

There are two types of cloud-based HSMs: those provided by a Certificate Authority (CA), and those running in your own cloud environment. With the former, you may be charged based on the annual number of code signing operations, and those costs can be quite high. So, if managing your own infrastructure is not a burden, using your own cloud HSM is the recommended approach.

It is common practice to attach a timestamp when code signing. Without a timestamp, the date and time of signing is self-reported by the signer, meaning third parties cannot verify whether the certificate was valid at the time of signing (since one could set the machine's clock to a past date and sign). As a result, once the certificate expires, the code signature is also considered invalid.

In contrast, if a trusted third-party timestamp is attached, the signing date and time become verifiable, allowing third parties to confirm that the certificate had not been revoked at the time of signing. This means the code signature remains valid even after the certificate itself expires.

The code signing certificate issued by the CA does not contain sensitive data such as private keys. Therefore, it can be placed on the GitHub Actions Runner as a file.

General Steps for Obtaining a Code Signing Certificate

First and foremost, you need to have a code signing certificate issued.
Here are the key points for this process:

  • The code signing private key must be generated on a FIPS 140-2 Level 2 compliant HSM and stored within the HSM (the private key must not be exportable from the HSM).
    • This is an industry requirement for how private keys must be handled. If this requirement is not met, the CA will not issue a code signing certificate.1
    • While it is technically possible under the requirements to store the private key on a FIPS 140-2 Level 2 compliant USB security token, GitHub Actions Runners cannot access USB devices, so this option is not viable for this architecture.
  • The CA issues the code signing certificate against the public key corresponding to the private key.
    • In other words, the CA is certifying that "this public key indeed belongs to organization XXX."
  • Once a private key is generated, it does not need to be regenerated as long as its security is not compromised. This means you can continue using the same key when renewing the certificate without creating a new one.
  • Certificates have an expiration date, so they must be reissued each time the expiration arrives.

If you generate the private key on your own cloud HSM, the general steps are as follows:

  1. Generate the code signing private key on a FIPS 140-2 Level 2 compliant HSM and store it within the HSM.
    • As long as the key's security has not been compromised, there is no need to regenerate it. This means that when reissuing the code signing certificate in subsequent years, you can start from step 2.
  2. Obtain the Attestation for the private key.
    • A private key Attestation is data that allows a third party (in this case, the CA) to verify that the private key was generated in a trusted environment and has not been tampered with.
    • The Attestation is downloaded from the HSM.
  3. Create a CSR (Certificate Signing Request) using the private key.
    • The CSR contains the public key and a signature created with the private key, so the certificate issued by the CA is linked to the key pair through this CSR.
  4. Submit both the CSR and the Attestation to the CA to have the code signing certificate issued.
    • The CA verifies the integrity of the private key based on the Attestation and then issues the code signing certificate based on the CSR.
  5. Receive the certificate file from the CA and store it somewhere safe.

Step 3, creating the CSR, is typically done on a local Linux machine using an OpenSSL command like the following.
For information on how to use OpenSSL with cloud HSMs, please refer to What is the OpenSSL Engine API that enables integration between OpenSSL and cloud HSMs or YubiKey.

export PKCS11_MODULE_PATH=/tmp/libkmsp11-1.6-linux-amd64-fips/libkmsp11.so
export KMS_PKCS11_CONFIG=/tmp/pkcs11-config.yaml

openssl req -new -subj '/CN=example.com/' -sha256 \
  -key pub.pem -engine pkcs11 -keyform engine \
  -key pkcs11:object=sign_key_name > cert-request.csr
Enter fullscreen mode Exit fullscreen mode

If generating the private key and creating the CSR yourself seems too cumbersome, using an HSM provided by the CA may allow some of these steps to be semi-automated.

Code Signing Procedure

Once the certificate has been issued, you need to set up the code signing environment on the Windows machine running on the GitHub Actions Runner.
Here are the key points for setting up the environment:

  • Code signing is performed using SignTool.exe, which is included in the Windows SDK (it is pre-installed on GitHub-hosted Windows machines).2
  • You need to install a library on the GitHub Actions Windows Runner that allows SignTool.exe to delegate its signing operations to the cloud-based HSM.
  • Simply installing the library is not enough — you also need to configure which credentials to use for cloud access and which key to use, following the setup instructions for each library.
    • For example, with the Google Cloud CNG Provider, ADC (Application Default Credentials) is used for authenticating with Google Cloud, and the key to use is defined in a YAML configuration file.
    • When connecting to proprietary HSM environments such as DigiCert or SSL.com, refer to the documentation published by each company.

Once the environment is set up, all that remains is to call SignTool.exe to perform the code signing.
The following is an example command for signing with a Sectigo certificate:

signtool.exe sign /v /fd sha256 /t http://timestamp.sectigo.com /f path/to/mysigncscertificate.crt /csp "Google Cloud KMS Provider" /kc projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME/cryptoKeyVersions/1 path/to/file-tobe-signed.exe
Enter fullscreen mode Exit fullscreen mode

  1. Prior to 2023, it was possible to store private keys in PEM files. However, due to ongoing private key leakage incidents — exemplified by the NVIDIA code signing private key leak — the industry now requires that private keys be stored on an HSM when issuing code signing certificates. 

  2. You can view the list of software pre-installed on Runners at https://github.com/actions/runner-images

Top comments (0)