Open appsettings.json set the path to the windows sdk signtool.exe and modify the signtool options.
Open powershell and run:
$env:ASPNETCORE_URLS="http://+:5000"; .\TownSuite.CodeSigning.Service.exe
openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj "/C=US/ST=State/L=City/O=YourOrganization/CN=YourName"
openssl pkcs12 -export -out certificate.pfx -inkey server.key -in server.crtuse the cert
$env:ASPNETCORE_URLS="http://+:5000;https://+:5001";$env:ASPNETCORE_Kestrel__Certificates__Default__Password="PLACEHOLDER"; $env:ASPNETCORE_Kestrel__Certificates__Default__Path: "/path/to/placeholder/certificate.pfx" .\TownSuite.CodeSigning.Service.exe
curl --location 'https://localhost:7153/sign' \
--header 'Content-Type: application/x-msdownload' \
--data '@/C:/the/file/to/upload/and/sign.dll' \
-o output-signed-file.dllSee ./TownSuite.CodeSigning.Client.exe -help for more information
Sign all matching files in one folder using -folder and -file.
./TownSuite.CodeSigning.Client -folder "/path/to/folder/with/assemblies" -file "*.dll;*.exe" -timeout 30000 -url "https://localhost:5000" -token "the token"Each -folders entry pairs a folder path with its own file patterns separated by |. Files within are ; separated. Can be specified multiple times.
Duplicate files across folders are detected by SHA-256 hash and only signed once. After signing, the signed copy is distributed to all duplicate locations.
.\TownSuite.CodeSigning.Client.exe -folders "C:\publish\win-x64|*.dll;*.exe" -folders "C:\publish\linux-x64|mylib.dll;*.so" -timeout 30000 -url "https://localhost:5000" -token "the token"Use -rfolder to recursively scan a parent folder and all its subdirectories for matching files. Same folder|patterns syntax as -folders. Can be specified multiple times.
This is useful in CI where multiple publish outputs (e.g. win-x64/, linux-x64/, win-arm64/) live under a common parent directory and may contain duplicate files.
.\TownSuite.CodeSigning.Client.exe -rfolder "C:\publish|*.dll;*.exe" -timeout 30000 -url "https://localhost:5000" -token "the token"Multiple recursive roots with different patterns:
.\TownSuite.CodeSigning.Client.exe -rfolder "C:\publish|*.dll;*.exe" -rfolder "C:\other|mylib.dll" -timeout 30000 -url "https://localhost:5000" -token "the token"All folder modes can be combined. Files from every source are merged, deduplicated by SHA-256, signed once, and the signed copy is copied back to all duplicate locations.
.\TownSuite.CodeSigning.Client.exe -file "extra.dll" -folder "C:\extras" -folders "C:\publish\win-x64|*.dll;*.exe" -rfolder "C:\publish\shared|*.dll" -timeout 30000 -url "https://localhost:5000" -token "the token"You can create a detached PKCS#7 (.sig) for an update package (zip). This is useful when your build server produces an update .zip that should be signed separately from the individual exe/dll code signing.
Client usage example (opt-in detached signing):
.\TownSuite.CodeSigning.Client.exe -file "update-package.zip" -timeout 30000 -url "https://your-codesign-server:5000" -token "the token" -detachedThe client will upload the zip as a batch and poll for result. When detached signing is requested the client will save the returned signature next to the original file as update-package.zip.sig instead of overwriting the zip.
Curl example (upload zip directly and save signature):
Step 1: submit batch signing request (returns an ID in the response)
curl --location 'https://localhost:7153/sign/batch' \
--header 'Content-Type: application/zip' \
--header 'X-Detached: true' \
--header 'X-BatchId: PLACEHOLDER' \
--header 'X-BatchReady: true' \
--data-binary '@./update-package.zip'Step 2: poll for the completed signature using the returned ID
curl --location "https://localhost:7153/sign/batch?id=<id-returned-from-step-1>" \
--header 'Content-Type: application/zip' \
--header 'X-Detached: true' \
--header 'X-BatchId: PLACEHOLDER'Server behavior:
- The service parses your existing
SignToolOptionsto locate the certificate (sha1/subject or referenced PFX). If the configured signer uses a hardware token / CSP that is registered in the certificate store, the detached signer will use that key (no separate PFX required). - The detached signature produced is a PKCS#7 detached signature suitable for verification by standard tools.
Recursive example (sign all zips under a parent folder)
.\TownSuite.CodeSigning.Client.exe -rfolder "C:\builds|*.zip" -timeout 30000 -url "https://your-codesign-server:5000" -token "the token" -detachedNotes:
-rfolder "C:\builds|*.zip"will recursively scanC:\buildsand all subfolders for files matching*.zipand upload them for detached signing.- The client will save a signature file next to each zip as
your-package.zip.sig(it will not overwrite the original zip). - Duplicate zip content across multiple folders is deduplicated by the client; signatures are created once and copied to duplicates.
A browser dashboard is available at /admin/ showing service uptime/version, signing queue depth/in-flight/completed/failed counts, worker health, concurrency slots, configured certificate expiry, and pending batch job folders. It polls GET /admin/status, which returns the same data as JSON for monitoring tools:
curl --location 'https://localhost:7153/admin/status'Anonymous by design: both /admin/ and /admin/status are reachable without a JWT, the same as /healthz and /health/*, even when JWT auth is enabled for /sign*. This is a deliberate trade-off for services running on a trusted internal network — the endpoint discloses machine name, queue counters, certificate subject/thumbprint/expiry, and the batch temp-folder path. If the service is exposed beyond a trusted network, put it behind a reverse proxy that restricts access to /admin/*.
To include certificate expiry in the dashboard, set these under Settings (and Settings:OpenSSL for the detached-signing cert) in appsettings.json — both are optional and skipped when empty:
CertificateWarningDays controls when a certificate's status flips from ok to warning in the dashboard (default 30 days before expiry).
For increased performance add the services working directory to the windows defender exclusion path.
Important Note:
- Security Risks: Be cautious when excluding directories from antivirus scans, as this can potentially expose your system to threats if malicious files are placed in these directories.
Add-MpPreference -ExclusionPath "C:\Users\[USER]\AppData\Local\Temp\1\townsuite\codesigning"| Extension | Purpose | Format/Encoding |
|---|---|---|
| .pem | Generic container for any cryptographic data. | Text-based (Base64). Starts with -----BEGIN.... |
| .crt | Standard "Certificate" file. Common in Linux/Unix. | Usually PEM (text), but can be DER (binary). |
| .cer | Standard "Certificate" file. Common in Windows. | Usually DER (binary), but can be PEM (text). |
| .key | Conventional name for a Private Key. | Usually PEM (text). Should be kept secret. |
Key distinction: .crt and .cer are almost interchangeable; if one doesn't work, renaming the extension often fixes it unless the encoding (binary vs. text) is wrong for the application
.p7s(PKCS#7 Signature)- Standard: The formal extension for a PKCS#7/CMS detached signature.
.sig(Generic Signature)- Standard: No single technical standard; it is a generic naming convention.
sudo apt install -y osslsigncode openssl libengine-pkcs11-openssl gnutls-bin xxd-
https://github.com/mtrojnar/osslsigncode/releases/download/2.13/osslsigncode-2.13-windows-x64-mingw.zip
sha256:c6d3ec8f383a6ed204503a9d4445788f2d3e71da87f3604c42e40167ad9ceb8e
- key is private, keep safe
- crt is for public verification, distribute widely
rsa example
openssl genrsa -aes256 -out server.key 4096
openssl req -x509 -nodes -key server.key -out server.crt -days 365 -subj "/C=US/ST=State/L=City/O=YourOrganization/CN=YourName"mldsa65 example
openssl genpkey -aes256 -algorithm mldsa65 -out server.key
openssl req -x509 -nodes -key server.key -out server.crt -days 365 -nodes -subj "/C=US/ST=State/L=City/O=YourOrganization/CN=YourName"open cms -sign -in "{FilePath}" -signer "/path/to/server.crt" -inkey "/path/to/server.key" -keyform P12 -passin pass:password -out "{FilePath}.sig" -outform DER -md sha256osslsigncode add -t "http://timestamp.digicert.com" -in "{FilePath}.sig" -out "{FilePath}.timestamped.sig"openssl pkcs7 -inform DER -in signature.sig -print_certs -text -nooutopenssl pkcs12 -in "server.pfx" -nocerts -nodes -out "server.key"openssl pkcs12 -in "server.pfx" -nokeys -out "server.cer"openssl cms -verify -binary -inform DER -in archive.zip.sig -content archive.zip -CAfile server.cer > /dev/null