Working with SafeNet Tokens to sign code in Gitlab CI
At work we’ve recently needed to sign our software, so that our customers will not get ugly and scary warnings when installing it via the provided MSI packages. I’ve never dabbled with code signing before, but I’ve quickly learned that it’s not as simple as getting the cert and private key, putting both on a server and using it from the command line using signtool.
Apparently, since February 2017, Microsoft requires the private key to be stored securely on a USB token, from which it is not exportable. This requires the user to use the software provided by their chosen CA (GlobalSign in our case) to verify ownership during the actual signing process.
Once you have your token (and its password), you can run signtool as normal to sign a file:
signtool.exe sign /a /v /t http://timestamp.verisign.com/scripts/timestamp.dll MY_MSI_FILE.msi
/aselects the best certificate automatically
/vmakes the process verbose
/tdefines the timestamping URL, which is also required since at least 2017
Now the only question is: how does this integrate in your Gitlab CI workflow?
The Build Server
We’re using a virtual machine in our office as a Gitlab CI runner, which has exclusive hardware access to the USB port and the USB token. What we’ve learned is that when using RDP to connect to the server, Windows would disconnect the token, rendering it useless. Sometimes this required us to reboot the VM, sometimes it would reconnect automatically once the RDP connection was closed.
Therefore, when working with the virtual machine, use VNC.
Gitlab CI Runner
Originally, we’ve had the runner setup according to documentation, with it running as Windows service. This had a couple of issues though:
Running as the SYSTEM user, the runner process (and the signtool it would spawn) could not see the certificate, which we’ve imported into the local user’s cert store. If you’re getting
SignTool Error: No certificates were found that met all the given criteria.
make sure the runner is using the correct user account.
After changing the service’s user account, signtool would find the certificate, but still fail to actually sign the binary:
Error information: "Error: SignerSign() failed." (-2147023673/0x800704c7)
SignTool Error: An unexpected internal error has occurred.
As it turns out, Windows services are not allowed to spawn GUI elements.
We found no way to both run the service under a local user account and disable the session isolation,
so in the end we opted to simply run the runner process manually (with a primitive shortcut to
gitlab-runner.exe run). From there on, the runner could spawn the signtool, which would in turn
find the correct certificate and pop up the SafeNet software, asking for the token password.
This password prompt is the only part of our build pipeline that we cannot automate. There are probably ways around this, but it seems that this is against the spirit of USB tokens and we’re not Windows developery by heart. We build installers rarely, so it works fine to just have a VNC window open and supply the password when our CI pipeline is running.
Single Token Logon
James from RTDS Technologies Inc. was so nice to share a trick that makes singing (lots of) files much easier: Apparently, the SafeNet client has an option to require only a single logon per session. It’s hidden in the advanced section of the Client Settings and is disabled by default:
After enabling the option and saving, you have to enter your token password only on the next
signtool invocation and then only after rebooting the machine. Hooray!
This could mean that running the Gitlab CI runner as a (non-SYSTEM) Windows service is an option now, as long as you manually logon to the token before running your first build job, but we’ve opted to stay with the desktop shortcut variant.