Handtokening Code Signing Server
You acquired a code signing certificate with a hardware token; you've signed a couple .EXEs and .DLLs, but now you want to sign code remotely, for example, as part of CI builds in GitHub Actions or GitLab CI. What to do?
Well, you need a networked service that accepts programs and signs them with your token.
There are some options out there, but they're expensive, the free options have unacceptable limitations, and some don't run on your operating system.
To be fair, it's unusual to want to sign Windows program while using Linux, but that's the situation I found myself in, so I ended up building my own solution.
Goals
I want my solution to be simple, run on Linux, and help me follow code-signing best practices, including:
- Test signing
- Logging
- Virus scans (ClamAV and optionally VirusTotal)
- To protect against malicious code that made its way into the program,
- and for discovering AV false positives early.
- PIN entry for production signatures (PIN is not stored).
Result
I ended up with a self-hosted HTTP API that invokes the osslsigncode
program.
The name of the code signing server is Handtokening. It's a portmanteau of handtekening — which means signature in Dutch — and (hardware) token, which is where the private keys of code signing certificates are often stored.
It uses several technologies that I'm excited about:
- Python + Django with Gunicorn as the WSGI server: well known and reliable.
- systemd: pretty much unavoidable on Linux. Socket activation and its built-in security hardening options are great.
- SQLite: a great database option, especially for low traffic site like this.
- Ansible: To reliably set up the application in a secure manner.
Data Model
Handtokening's data model is fairly simple. There are a handful of tables related to configuration, authentication, and logging.
Signing Profiles
Everything is centered around the "Signing Profile". Among other things, it describes the certificates that can be used, what timestamping servers are available, and which clients have access to the profile.
Usually you'll have two profiles, each with their own certificate:
- A test signing profile that's constantly used in development builds.
- This makes your dev builds more representative of the production ones, without unnecessarily exposing your production certificate to prerelease code that may have vulnerabilities, unaudited dependencies, bugs, or other quality issues.
- A test signing certificate isn't stored on a hardware token meaning you don't have to enter a PIN to sign things.
- A production profile that's used for official releases.
- Access to this profile is much more restricted.
- A PIN-code must be entered for every file you sign.
Clients
Clients are used to grant access to signing profiles. Authentication is handled using randomly generated secrets with 160 bits of entropy. The lifetime of these secrets is configurable.
I have one client for non-production builds that only has access to the test signing profile. Secrets generated for this client will expire long after civilisation has ended (900+ days).
For production builds, each project has its own client with access granted to the production profile. The secrets for these clients are valid for only a short amount of time: one to four hours depending on the project.
Signing Logs
It's important to track what you've signed. If your code signing certificate is abused somehow, you want as much information as possible to evaluate the impact and investigate how it could've happened.
This is what the signing logs table is for. Every signing operation results in a new entry.
The log contains many attributes:
- When the operation started and ended
- Which client requested the signature
- The IP address and user agent header of the client
- The selected signing profile
- What certificate was used
- Hashes of the program before and after signing
- Where the files are stored on the signing server
- VirusTotal analysis results
- Whether the signing operation succeeded
Here's what the detail view looks like, with some fields removed and condensed:
API
The signing API is very simple. A file is POST
-ed to /api/sign
along with the client credentials using basic auth and some additional info via query parameters.
You can easily invoke the API with curl
:
curl http://localhost/api/sign \
--max-time 300 \
--user "$HT_USER:$HT_SECRET" \
--header 'Content-Disposition: attachment; filename="minidump.dll"' \
--url-query description="Noita Crash Dumper" \
--url-query url="https://github.com/dextercd/Noita-Minidump" \
--url-query signing-profile="test-signing" \
--data-binary @minidump.dll \
--output "minidump-signed.dll"
Using curl's @
syntax, the contents of minidump.dll is sent to Handtokening.
If everything goes well, it responds with a signed program that's saved to minidump-signed.dll.
The request blocks until the signing operation is complete, so the client and server need to be prepared for that, i.e., large timeout values must be configured on the client and server. That's one complication in an otherwise simple interface.
Microsoft's Authenticode signing system is based around PKCS #7 files.
Instead of responding with the signed program,
it's possible to make Handtokening respond with a PKCS #7 file using --url-query response-type=pkcs7
.
The resulting file can be attached to the submitted program to make it signed.
This means the entire signed program doesn't have to be transferred back to the client, which saves you the time it takes to transfer hunderds or even thousands of megabytes.
When a program is submitted, Handtokening does the following:
- Authenticates the request
- Checks if the client has access to the signing profile
- Picks a valid certificate in the profile
- Stores the incoming file
- Scans it with ClamAV and optionally VirusTotal
- Waits to receive a PIN-code if applicable
- Signs the program using
osslsigncode
and returns the result - Keeps a log of how it all went
CMake
I'm using CMake for all the projects I want to sign, so I wrote a module for this that hooks into CMake's packaging step.
Here's how the module is used; just before include(CPack)
add:
FetchContent_Declare(
Handtokening_CMake
GIT_REPOSITORY https://github.com/dextercd/Handtokening-CMake.git
GIT_TAG [Version hash here]
)
FetchContent_MakeAvailable(Handtokening_CMake)
set(HT_SIGN_PATTERNS
"Debug/noita_dear_imgui\\.dll$" "description=Noita Dear ImGui module (Debug Build)"
"noita_dear_imgui\\.dll$" "description=Noita Dear ImGui module"
"native_test\\.dll$" "description=Noita Dear ImGui example native mod module"
)
include(Handtokening)
The HT_SIGN_PATTERNS
variable defines what you want to sign.
You can provide description
and url
attributes after a pattern which will then be included in the Authenticode signature data.
The module requires the following cache variables before it attempts to sign programs:
HT_ENDPOINT
: URL to submit signing requests to. This normally ends in /api/sign.HT_USER
: Client name to authenticate as.HT_SECRET
: API authentication secret.HT_SIGNING_PROFILE
: What signing profile to use.
It's expected that these are defined outside the code base as part of the CI setup.
The module checks if osslsigncode
or signtool.exe
is available locally.
If one's available, and it's a new enough version, it'll request a PKCS #7 response from Handtokening and attach it to the program, instead of downloading the full signed program from Handtokening.
Installation
Ansible is used to perform the installation steps:
- Making a dedicated user to run the service under.
- Creating the virtualenv where the code and its dependencies are installed.
- Setting up the systemd service with socket activation.
- Adding a polkit rule so the service can access hardware tokens via pcscd.
- Writing the configuration files.
- Everything's set up with proper file permissions, so secrets are only readable by the dedicated service user.
systemd
systemd contains numerous settings that you can use to remove privileges from a service.
systemd-analyze security [service name]
lists these options along with an "exposure" rating, letting you prioritise the most important ones.
I ended up enabling most suggested security options.
I'm not signing code every day. systemd's socket activation feature means that the server is not running until I begin using it, at which point it's started automatically.
Fin?
That's about it. The code is available on GitHub dextercd/Handtokening. Give it a try if it seems useful to you.
Feel free to open an issue if you run into problems or have questions.
Currently the service only supports Authenticode via osslsigncode
.
In the future I may add other signing options,
although I personally don't have much of a need for this at the moment.