Code signing has a key-management problem. The usual setup copies a .pfx file, or plugs a USB token, onto every build agent that needs to sign a release. The private key ends up on machines that run untrusted build scripts, there is no record of what was signed or by whom, and rotating the certificate means touching every agent. sgcSign 2026.6 answers this with a new signing server: a self-hosted daemon that exposes signing over a TLS REST API, so your pipelines and developers sign remotely while the signing key stays in exactly one place.
The key never leaves the server. Better still, the server can front a hardware token or a cloud KMS, so the key never exists as a file at all. Every request is authenticated with an API key, rate-limited, and written to a tamper-evident audit log. This post covers what the server can sign, how to configure it, a five-minute quick start, and the two ways to call it: plain curl and the bundled sgcsign command-line client.
What it signs
One server, one API, eight signature formats. The same endpoint shape (POST /api/v1/sign/<format>) covers both executables and documents:
- Authenticode — Windows PE files:
.exe,.dll,.sys,.msi,.cab,.ocx. Optional dual SHA-1 + SHA-256 signing for legacy Windows. - PAdES — PDF documents, with reason, location, contact and signer name.
- XAdES — XML, enveloped, enveloping or detached, with country e-invoicing profiles such as eIDAS, FacturaE, FatturaPA, KSeF, Peppol, VeriFactu and TicketBAI.
- CAdES — detached PKCS#7 (
.p7s) over any payload. - ClickOnce — application and deployment manifests.
- NuGet —
.nupkgpackage signing. - VSIX — Visual Studio extension packages.
- PowerShell —
.ps1/.psm1/.psd1scripts (Authenticode SIP).
Every format accepts an optional RFC 3161 timestamp URL, so signatures stay valid after the signing certificate expires. A companion POST /api/v1/verify endpoint checks an existing signature and returns the signer subject, validity and timestamp.
Pluggable key providers
A provider is a named handle to a signing key. The caller names a provider on each request; it never sees the key. You can register as many as you need, and the same server can mix a local PFX for internal tools with a cloud-backed EV certificate for public releases. Nine provider types are supported:
- Windows Certificate Store (
WinCertStore) — selects a certificate by thumbprint or subject, ideal for EV certificates enrolled on a token. - PFX and PEM — certificate and key files on disk.
- PKCS#11 / HSM (
PKCS11) — hardware tokens such as YubiKey, or a network HSM. - AWS KMS, Azure Trusted Signing, Google Cloud KMS, HashiCorp Vault and Certum SimplySign — the private key lives in the cloud service and never reaches the server.
Secrets stay out of the configuration file. Any parameter whose name ends in _env is read from an environment variable, so the PFX password, the token PIN or the cloud secret is supplied by the service environment, not committed to disk.
Configuring the server
The whole server is driven by one JSON file, sgcSignServer.conf.json (a documented sample ships as sgcSignServer.conf.sample.json). The top-level shape is a handful of blocks:
{
"server": {
"listen": "0.0.0.0",
"port": 8443,
"tls": {
"enabled": true,
"cert_file": "certs/server.crt",
"key_file": "certs/server.key"
},
"max_upload_mb": 512,
"firewall": {
"enabled": true,
"whitelist": ["10.0.0.0/8"],
"brute_force": { "enabled": true, "max_attempts": 5, "ban_duration_sec": 900 },
"rate_limit": { "enabled": true, "max_connections_per_ip": 30, "time_window_sec": 60 }
}
},
"storage": { "sqlite_path": "data/sgcsignserver.db" },
"admin": {
"initial_user": "admin",
"initial_password_env": "SGCSIGN_ADMIN_INIT_PW",
"session_timeout_minutes": 60
},
"audit": { "retention_days": 365 },
"providers": [
{
"name": "pfx-build",
"type": "PFX",
"params": { "file": "certs/build.pfx", "password_env": "SGCSIGN_PFX_PW" }
},
{
"name": "ev-release",
"type": "AzureTS",
"params": {
"tenant_id": "00000000-0000-0000-0000-000000000000",
"client_id": "00000000-0000-0000-0000-000000000000",
"client_secret_env": "SGCSIGN_AZURE_TS_SECRET",
"account": "your-account",
"certificate_profile": "your-profile",
"endpoint": "https://eus.codesigning.azure.net/"
}
}
]
}
The blocks map directly to features. server sets the bind address, port (8443 by default), TLS certificate and the built-in firewall (IP allow and deny lists, brute-force lockout, connection rate limiting, path-traversal and payload guards). storage points at the SQLite database that holds users, API keys, the audit log and the webhook queue. admin defines the first user and how long sessions last. audit sets the retention window. providers is the list above.
The bootstrap admin password is never stored in the file. On the very first start, when the user table is empty, the server reads the environment variable named by initial_password_env (default SGCSIGN_ADMIN_INIT_PW) and creates the admin account. On every later start that variable is ignored, so it only ever seeds the first login.
Quick start in five minutes
Install the server (the setup wizard registers the Windows service, or run sgcSignServer.exe --install), then:
1. Create a test code-signing certificate and export it as a PFX. For a real deployment you import your CA-issued certificate, or skip this and point a provider at your token or cloud KMS instead.
$cert = New-SelfSignedCertificate -Type CodeSigningCert `
-Subject "CN=sgcSign Quickstart" -KeyAlgorithm RSA -KeyLength 3072 `
-CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(1)
$pw = ConvertTo-SecureString "QuickStartPFX!" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -Password $pw `
-FilePath "C:\Program Files\sgcSign Server\certs\quickstart.pfx"
2. Set the secrets as machine environment variables so the service account can read them:
setx /M SGCSIGN_ADMIN_INIT_PW "ChangeMeNow!"
setx /M SGCSIGN_PFX_PW "QuickStartPFX!"
3. Write a minimal config with one PFX provider (TLS off for a localhost test only):
{
"server": { "listen": "127.0.0.1", "port": 8443, "tls": { "enabled": false } },
"storage": { "sqlite_path": "data/sgcsignserver.db" },
"admin": { "initial_user": "admin", "initial_password_env": "SGCSIGN_ADMIN_INIT_PW" },
"audit": { "retention_days": 365 },
"providers": [
{ "name": "pfx-quickstart", "type": "PFX",
"params": { "file": "certs/quickstart.pfx", "password_env": "SGCSIGN_PFX_PW" } }
]
}
4. Start the service and confirm it is listening:
sc start sgcSignServer
sc query sgcSignServer
If it stops on start, run it in the foreground with sgcSignServer.exe --console --config <path> to see the error live (a missing SGCSIGN_ADMIN_INIT_PW or an unreadable PFX are the usual causes). The flag --selftest-providers loads the config, initialises every provider and exits, so you can validate keys before going live.
5. Log in and create an API key. Open http://localhost:8443/admin, sign in as admin with the bootstrap password, and create an API key. The plaintext key is shown once at creation; copy it immediately. Keys carry the prefix sgcsk_.
6. Sign a test executable with curl:
curl -X POST http://localhost:8443/api/v1/sign/authenticode ^
-H "X-API-Key: sgcsk_..." ^
-F file=@app.exe ^
-F provider=pfx-quickstart ^
-F hash=sha256 ^
--output app-signed.exe
That is the whole loop: configure a provider, issue a key, sign. From here you swap the self-signed PFX for a real certificate, turn TLS on, and lock the firewall down to your CI subnet.
Signing over REST
Every signing request is a POST with the file and a few form fields. Authentication is one header, either X-API-Key: sgcsk_... or Authorization: Bearer sgcsk_.... The response is the signed artifact as application/octet-stream, with the signer subject and signing duration returned in the X-Sgcsign-Signer-Subject and X-Sgcsign-Duration-Ms headers.
Signing a PDF, with a visible signature reason and location:
curl -X POST https://sign.acme.local:8443/api/v1/sign/pades \
-H "X-API-Key: sgcsk_..." \
-F file=@invoice.pdf \
-F provider=qualified-eu \
-F tsa_url=http://timestamp.digicert.com \
-F reason=Approved \
-F location="Madrid, Spain" \
-F signer_name="Acme Billing" \
-o invoice-signed.pdf
Signing an XML invoice with an eIDAS XAdES profile:
curl -X POST https://sign.acme.local:8443/api/v1/sign/xades \
-H "X-API-Key: sgcsk_..." \
-F file=@invoice.xml \
-F provider=qualified-eu \
-F profile=eidas \
-F xades_type=enveloped \
-o invoice-signed.xml
Verifying a signature returns JSON rather than a file:
curl -X POST https://sign.acme.local:8443/api/v1/verify \
-H "X-API-Key: sgcsk_..." \
-F file=@app-signed.exe \
-F format=authenticode
{ "valid": true, "status": "valid",
"subject": "CN=Acme Inc., O=Acme, C=US",
"has_timestamp": true, "timestamp_time": "2026-06-15T13:01:17.000Z" }
The sgcsign command-line client
The server ships with sgcsign, a small cross-format CLI with four verbs: sign, verify, keys and health. The server URL, API key and provider can come from flags or from the environment variables SGCSIGN_SERVER, SGCSIGN_APIKEY and SGCSIGN_PROVIDER, which keeps secrets off the command line in CI:
set SGCSIGN_SERVER=https://sign.acme.local:8443
set SGCSIGN_APIKEY=sgcsk_...
sgcsign sign -f authenticode -p pfx-build -o app-signed.exe app.exe ^
--tsa http://timestamp.digicert.com --desc "Acme Installer"
sgcsign verify -f authenticode app-signed.exe
For Authenticode there is a bandwidth-saving trick. With pre-hash mode (--prehash, on by default) the CLI computes the PE hash locally and uploads only the hash, a few hundred bytes instead of a multi-megabyte binary. The server signs the hash and returns a detached PKCS#7 blob, which the client embeds into the file. On a bandwidth-constrained CI agent signing a large installer, that turns a multi-megabyte upload into a tiny one.
Security and operations
The server is built to run unattended in front of a real signing key, so the operational surface matters as much as the crypto:
- Admin console at
/admin: dashboard, API keys, providers, users, projects and the audit trail, all in the browser. - API keys with per-key rate limits and daily quotas. A key that hits its limit gets
429with aRetry-Afterheader. - Multi-tenant projects, so a key scoped to one team cannot use another team's providers.
- Approval workflow (optional): a two-step flow where a signing request is held until an operator approves it, for high-value release certificates.
- Tamper-evident audit log: every sign, verify, login and configuration change is chained with a hash, so the record cannot be altered after the fact.
- Prometheus metrics at
/api/v1/metricsand liveness/readiness probes at/api/v1/health, ready for your monitoring stack. - Outbound webhooks that POST a JSON event on every signing operation, to wire signing into Slack, an SIEM or a release dashboard.
- OpenAPI 3.1 spec at
/api/v1/openapi.jsonwith an interactive Swagger UI at/api/v1/docs. - TLS 1.2+ with optional ACME / Let's Encrypt challenge serving, plus the built-in firewall for IP filtering and abuse protection.
Why self-host
The point of the server is to collapse a fleet of risky, key-bearing build agents down to a single hardened endpoint. The signing key lives only on the server, or in the HSM or cloud KMS the server fronts, never on the machines that run your build scripts. Access is gated by per-key API tokens with quotas, scoped to projects, and every signature is recorded in an audit log you control. Rotating a certificate, revoking a compromised agent's key, or proving who signed what all become one place to look instead of many.
Availability
The sgcSign Server is part of sgcSign 2026.6.0. It runs as a Windows service or a console application, is configured by the single JSON file shown above, and is driven from curl, the bundled sgcsign CLI, or any HTTP client through its OpenAPI description.
Questions, feedback or help getting it set up? Get in touch. You will get a reply from the people who wrote the code.
