English | 简体中文
An HTTP wrapper around Microsoft SignTool.exe that lets you sign Windows binaries from any machine on your LAN — including Linux/macOS CI runners that cannot access the hardware UKey directly.
Warning
This service exposes a hardware-backed code-signing certificate over HTTP. Do not expose it to the public internet. Run it only on a trusted intranet, and protect it with the built-in HTTP Basic Auth (see Configure Authentication).
- HTTP API for signing files with a hardware UKey on a remote Windows host
- Content-addressed cache to skip re-uploading unchanged binaries
- Chunked upload for large files — bypass gateway size limits and retry failed chunks
- HTTP Basic Auth with multi-account support
- Built-in Web UI for manual signing and connectivity testing
- Drop-in
sign.jsfor Electron Builder
- Windows host with the code-signing UKey physically connected
- Bun
- SignTool.exe (ships with the Windows SDK)
-
Install the UKey driver (e.g. SafeNet).
- In the driver client settings, enable "Enable single logon".
-
Install the certificate (e.g. via DigiCertHardwareCertificateInstaller).
- Make sure the certificate is also installed into the local certificate store.
-
Verify the setup in PowerShell:
gci -Recurse Cert: -CodeSigningCert
The certificates installed in steps 1 and 2 should appear. If not, confirm the UKey is connected and try again.
-
Install Bun.
-
Install SignTool.exe from the Windows SDK installer.
Note
Some of the steps above cannot be completed over Windows Remote Desktop.
Create a .token file (already in .gitignore) before starting the service:
cp .token.example .token
# edit .token, fill in username:passwordFile format. Each non-empty, non-comment line is parsed as a username:password pair. Any matching pair grants access; multiple accounts are supported. All endpoints — including the Web UI — require authentication.
# .token
admin:s3cret
ci-bot:another-secret
Create a .config file (already in .gitignore):
cp .config.example .config
# edit .config, fill in signtool path and certificate thumbprintFile format. key=value per line; lines starting with # are comments. Both keys are optional — leave a value empty to fall back to auto-detection (signtool searches the standard Windows SDK install paths; thumbprint is only required when the machine has more than one code-signing certificate).
# .config
signtool=C:/Program Files (x86)/Windows Kits/10/bin/x64/signtool.exe
thumbprint=ABCDEF0123456789ABCDEF0123456789ABCDEF01
git clone https://github.com/netless-io/sign-server
cd sign-server
bun start
# serving http://192.0.2.10:3000Take note of the URL printed above — that's your SIGN_SERVER_URL. The host can be either an IP address or a DNS name (e.g. http://signer.intranet:3000); you'll need it when integrating with Electron Builder (see Section 5).
If bun start reports an error, see the table below.
| Error | Resolution |
|---|---|
Not found .token file |
Create a .token file in the project root following the format of .token.example. |
Invalid .token file |
Ensure each entry in .token follows the username:password format. |
Not found .config file |
Create a .config file in the project root following the format of .config.example. |
Not found SignTool.exe |
Set signtool in .config to the absolute path of SignTool.exe. |
Not found certificate |
Re-check Section 1 and confirm the UKey is connected. |
Found multiple certificates |
Set thumbprint in .config to the 40-char thumbprint of the certificate to use. Run gci -Recurse Cert: -CodeSigningCert | Select Subject,Thumbprint to list candidates. |
Visit the URL printed at startup in a browser to access the built-in Web UI, which provides a minimal upload-and-sign interface. The browser will prompt for the credentials defined in .token. Use it to verify the end-to-end signing flow before integrating with your build pipeline.
Refer to the official documentation on custom signing for context.
sign.example.js is a drop-in custom sign script. Wire it up in your electron-builder config:
{
"win": { "sign": "./sign.example.js" }
}The script is fully configured via environment variables — no code changes required:
SIGN_SERVER_URL=http://signer.intranet:3000 \
SIGN_SERVER_USER=admin \
SIGN_SERVER_PASS=secret \
electron-builder ...SIGN_SERVER_URL accepts either an IP address or a DNS name.
Note
The sample script uses the native fetch() API, so electron-builder must run under Node.js 18 or newer. On older versions, import { fetch, FormData } from undici.
All endpoints require HTTP Basic Auth.
Check whether a file with the given content hash is already cached on the server.
- Body — raw text: the file's content hash
- Response — JSON
true|false
Sign a file and return the signed bytes.
- Body —
multipart/form-data:file— either a hash string (cache hit) or the file blobhash—"sha1"or"sha256"isNest—"1"for nested signatures,""otherwise
- Response —
application/octet-stream: the signed file
For large files: split the payload into smaller pieces, upload them independently to bypass gateway request-size limits, and retry per-chunk on failure. Once all chunks are uploaded, call /sign with the file hash — no re-upload needed.
Pending chunks are cleared when the server restarts; resumable upload only works within a single server lifetime.
sign.example.js enables chunked upload automatically for files larger than 16 MiB, with 4 MiB chunks. Override via the SIGN_SERVER_CHUNK_THRESHOLD / SIGN_SERVER_CHUNK_SIZE environment variables.
Query which chunks have been received for the given file hash, for resuming an interrupted upload.
- Body — raw text: the final file's MD5 hash
- Response — JSON
{ "received": number[], "total": number | null, "name": string | null }
Upload a single chunk.
- Query parameters:
hash— final file's MD5 hashindex— zero-based chunk indextotal— total number of chunksname— original file name (URL-encoded)
- Body —
application/octet-stream: the chunk bytes - Response — JSON
{ "received": number[] }
Assemble all uploaded chunks into the final file and verify its MD5 matches the supplied hash. On success the file is added to the cache; subsequent calls to /sign can reference it by hash.
- Body — raw text: the final file's MD5 hash
- Response —
200 { "ok": true };400with an error message if any chunk is missing or the hash does not match
Discard the in-progress chunked upload and delete any chunks already received.
- Body — raw text: the final file's MD5 hash
- Response — JSON
{ "ok": true }
signtool sign
/debug /td sha256 /tr http://timestamp.digicert.com /as
/fd {hash} /sha1 {thumbprint} /s {store} /sm
{file.exe}