sgcSign Server: Self-Hosted Code-Signing and Document-Signing over REST

· Components
sgcSign Server: Self-Hosted Code-Signing and Document-Signing over REST

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:

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:

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:

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.