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:

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:

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:

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:

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:

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:

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:

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.