Code signing is an important part of testing and distributing your desktop and mobile applications. It ensures that the end user’s system can verify the legitimacy of your application. Because of the need for security around signed certificates, they are stored locally and not uploaded to the cloud. This constraint could prevent your team from fully automating your CI/CD pipeline. Fortunately, using self-hosted runners, your team can keep certificates secure on machines you control, while still allowing CircleCI jobs to sign your package.

What is code signing?

Code signing is familiar in the mobile application space, where the Google Play Store and the App Store require it for every published app. Code signing guarantees users that applications they download will be trusted by the devices they want to install them on.

Code signing is available for desktop binaries, but it is less common. When you run an unsigned binary on Windows, you may get a prompt asking if you trust the publisher. The OS is alerting you that this is an unsigned app. The application may or may not be trustworthy, so you can use the prompt as a warning flag.

There are two methods of managing code signing for your application:

  • Embed the signature within the binary so it can be verified by the operating system; this is used by macOS, Windows, and mobile operating systems.
  • Provide a signature of the hashes of the binaries; used for Linux.

Either method can be used to verify that the downloaded file is correct.

Keeping private certificates secure

Best practice says to keep signed certificates as secure as possible, and to never put them online or in the cloud. Therefore, it is not a good idea to store a private certificate within CircleCI, either in secure contexts or in environment variables. Uploading the certificate or private GPG key to CircleCI is not recommended.

There is an exception, though, when it comes to testing. Locally created certificates are okay to be used for uploading and testing during the CI/CD process, so long as the certificate is not distributed and trusted by other machines. This exception makes it possible to perform end-to-end (E2E) testing of your CI/CD pipeline when you use the runner to keep certificates secure on machines that your team controls.

Setting up a runner on your machine

Getting a runner set up on your own machine is straightforward and is currently supported on Ubuntu, macOS, and Windows. You can find instructions for multiple platforms in the CircleCI runner overview.

You will also need to install the private certificate to the location required by the platform you are using to sign. Here are instructions:

With these steps completed, you are ready to start signing your applications using the CircleCI runner.

Signing on Linux using signed hashes

Linux does not use the concept of signed binaries. Instead, users rely on file hashes and signatures.

The first step is to calculate the file hash for the file you want to distribute. You can use SHA sums of various strengths, including SHA1 (not recommended), SHA256, or SHA512 hashes. For this tutorial, we will use SHA512.

$ sha512sum xyz.exe
cf83e...927da3e  xyz.exe

Save the result as SHA512SUMS. If you have multiple files you want to include in a single signature, just pass their filenames to the sha512sum command. Each file’s hash will be printed to a separate line. Do not edit the output, or you will not be able to verify the hashes later.

Now you can sign. If you have already generated a private GPG key, signing is this easy:

$ gpg --sign SHA512SUMS --output SHA512SUMS.sig

This code outputs the file SHA512SUMS.sig, which has been signed using your key. Distribute this .sig file with your binary and instruct users to validate the output after installing your public key. You can review instructions here.

$ gpg --decrypt SHA512SUMS.sig > SHA512SUMS
gpg: Signature made Tue 22 Jun 13:53:56 2021 BST
gpg:                using RSA key 1234XYZ
gpg: Good signature from "CircleCI <contact@circleci.com>" [ultimate]
$ sha512sum --check SHA512SUMS
xyz.exe: OK

Code signing on macOS

Like Linux, macOS can also use file hashes and signatures. It is a bit tedious compared to the alternative, which is signing your team’s macOS or iOS application files. In turn, macOS will verify them automatically.

Make sure all the certificates and profiles needed to build your application are installed on the local runner machine. These certificates and profiles must be installed under the same user configured for the runner daemon. The easiest way to do this is to build the project once in Xcode on the machine. This allows Xcode to create and install the certs and profiles it needs.

The runner launch daemon needs an additional option added so that the keychains can be accessed via the job:

<key>SessionCreate</key>
<true/>

Another key step is making sure that the new Apple Worldwide Developer Relations certificate is available on the runner machine. As the runner user, you can download it here

Construct your workflow in a way that builds the app on CircleCI but does not create a signed .ipa file. Instead, it should create an .xcarchive file that can be used later. Trying to build an .ipa file will fail because there are no code signing assets in the cloud job.

This archive is saved to the workspace that is pulled in on the runner job. The runner job takes this archive, skips building the project again, and runs it through gym, which exports it as a signed .ipa file using the code signing assets installed in the runner machine. This keeps the code signing assets safely on a machine that your team controls.

Code signing on Windows

If you have Visual Studio installed, you have access to a special signing tool from Microsoft called SignTool.exe. Signing files requires only a valid certificate (it can be self-signed) and executing SignTool.exe:

SignTool.exe sign /a /fd SHA256 xyz.exe

No other files are created, unlike in the Linux process. Instead, the signature is embedded within the file itself.

If you want to sign files for wider distribution, make sure that the certificate used to sign the file is widely trusted. Either purchase a certificate that has been signed by a trusted authority, or generate your own and distribute it yourself. However, self-signing is not recommended except for internal use. As I described earlier in the tutorial, Windows flags the execution of unsigned binaries with a prompt to proceed with an untrusted file. If a valid signature is found, the OS displays the details so the user can review them.

Using self-hosted runners to add signing to your CircleCI configuration

You know how to sign the binary, or provide a trusted way to validate that the binary is correct. Now you can roll this into your CircleCI configuration. Step one is verifying that the runner is correctly installed on the machine with the certificate or private GPG key.

In this example, we will be using a Windows Server 2019 virtual machine with a self-hosted runner to create an executable through CircleCI. You can adapt the process for other use cases if you want to.

Here is the example configuration:

version: 2.1
orbs:
  msix: circleci/microsoft-msix@1.1
jobs:
  build:
  	docker:
  		- image: cimg/go:stable
  	steps:
  		- checkout
  		- run:
  			command: GOOS=windows GOARCH=amd64 go build -o your-package.exe
  			name: Build application for Windows
  		- persist_to_workspace:
  			root: .
  			paths: your-package.exe
  sign:
	shell: powershell
    machine: true
    resource_class: your-namespace/windows-vm
    steps:
      - attach_workspace:
      		at: .
      - msix/sign:
          package-name: your-package
workflows:
  demo:
    jobs:
      - build
      - sign:
      		requires: ["build"]

The first step in this workflow is performing the intensive build process on CircleCI. That is saved to a workspace to transfer the binary file to the runner machine. This method keeps your own infrastructure small, but powerful enough to control your signing keys or certificates.

Conclusion

Code signing is becoming more and more important to distributing applications. Some systems refuse to execute binaries without being signed by a trusted party. Thanks to self-hosted runners, it is easy to combine the speed of CircleCI Cloud with the safety of running sensitive workloads on your own controlled machines. I hope you have found this tutorial helpful, and I hope you will be inspired to lead your team through more applications of code signing with CircleCI self-hosted runners.