docs/documentation/platform/pki/code-signing/windows-ksp.mdx
Code signing adds a tamper-proof seal to your software, so Windows and your users can trust that it really came from you and has not been changed.
The Infisical Windows KSP lets Microsoft's standard signing tool, signtool, add that seal to your .exe, .dll, and .msi files, using a signing key that stays locked inside Infisical. It is a small plug-in for Windows' built-in cryptography system, called Cryptography API: Next Generation (CNG). "KSP" is short for Key Storage Provider, which is just Windows' name for a plug-in that supplies signing keys.
Here is what happens when you sign a file: signtool hands the plug-in a short fingerprint of the file, the plug-in sends that fingerprint to Infisical, Infisical signs it and sends the signature back, and signtool attaches the signature to your file. Your private signing key never leaves Infisical.
This is the Windows version of the PKCS#11 module. Both use the same Infisical Signers behind the scenes. Use the PKCS#11 module for cross-platform tools like jarsigner, osslsigncode, and cosign. Use the Windows KSP when you sign with signtool.
You authenticate as a member of the Signer, using either a Machine Identity or your own Infisical access token. To use a Machine Identity, set it up once in Infisical:
signtool, Microsoft's signing tool, which comes with the Windows SDK. Most setups use the 64-bit signtool; there is also a 32-bit build, with a matching plug-in. The two plug-in files are infisical-ksp.dll (64-bit) and infisical-ksp-x86.dll (32-bit). Use the one that matches the signtool you run.Download the plug-in file from the releases page into the folder you are working in. Most people use the 64-bit signtool, so get infisical-ksp.dll:
Invoke-WebRequest `
-Uri "https://github.com/Infisical/infisical-ksp/releases/latest/download/infisical-ksp.dll" `
-OutFile "infisical-ksp.dll"
Registering means telling Windows the plug-in exists, so signtool can find it. This copies the file into a Windows system folder and adds a few entries to the Windows registry (the database of system settings). Open PowerShell as Administrator, go to the folder that holds infisical-ksp.dll, and run:
$prov = "Infisical Key Storage Provider"
$base = "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography"
Copy-Item .\infisical-ksp.dll "$env:windir\System32\infisical-ksp.dll" -Force
New-Item -Path "$base\Providers\$prov\UM\00010001" -Force | Out-Null
New-ItemProperty -Path "$base\Providers\$prov\UM" -Name Image -PropertyType String -Value "infisical-ksp.dll" -Force | Out-Null
Set-ItemProperty -Path "$base\Providers\$prov\UM\00010001" -Name "(default)" -Value "CRYPT_KEY_STORAGE_INTERFACE"
New-ItemProperty -Path "$base\Providers\$prov\UM\00010001" -Name Flags -PropertyType DWord -Value 0x10000 -Force | Out-Null
New-ItemProperty -Path "$base\Providers\$prov\UM\00010001" -Name Functions -PropertyType MultiString -Value @("KEY_STORAGE") -Force | Out-Null
$iface = "$base\Configuration\Local\Default\00010001\KEY_STORAGE"
$cur = @((Get-ItemProperty $iface).Providers)
if ($cur -notcontains $prov) { Set-ItemProperty -Path $iface -Name Providers -Value ($cur + $prov) }
The plug-in needs your Infisical address and the login it should use. The simplest setup is environment variables only, with no config file. This is the machine identity your admin added to the Signer:
$env:INFISICAL_SERVER_URL = "https://app.infisical.com"
$env:INFISICAL_UNIVERSAL_AUTH_CLIENT_ID = "<machine-identity-client-id>"
$env:INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET = "<machine-identity-client-secret>"
If you prefer a config file, create one at %ProgramData%\Infisical\config.json (or point INFISICAL_CONFIG at a different location) with at least your Infisical address, and keep credentials in environment variables:
{
"server_url": "https://app.infisical.com",
"log_level": "info"
}
Instead of a machine identity, you can hand the plug-in an Infisical access token directly, either your own or a machine identity's. Set INFISICAL_TOKEN and the plug-in uses it as-is:
$env:INFISICAL_SERVER_URL = "https://app.infisical.com"
$env:INFISICAL_TOKEN = "<your-access-token>"
| Field | Required | Default | Description |
|---|---|---|---|
server_url | Yes | None | Your Infisical address. Must start with http:// (local testing) or https://. |
auth.method | No | inferred | How to log in: universal-auth (machine identity) or token (an access token used directly). Inferred from the credentials when unset (a token means token, otherwise universal-auth). |
auth.client_id | No | None | The machine identity's client ID, for universal-auth. Prefer the environment variable. |
auth.client_secret | No | None | The machine identity's secret, for universal-auth. Prefer the environment variable. |
auth.token | No | None | An Infisical access token, for token auth. Prefer the environment variable. |
tls.ca_cert_path | No | None | Path to a custom CA file, for self-hosted Infisical that uses a private CA. |
tls.skip_verify | No | false | Skip checking the server's TLS certificate. For local testing only, never in production. |
cache.token_ttl_seconds | No | 300 | How long to reuse a login token before logging in again. |
cache.cert_ttl_seconds | No | 3600 | How long to reuse a downloaded certificate. |
cache.signer_ttl_seconds | No | 300 | How long to reuse the list of Signers. |
approval.signing_duration | No | None | Ask for an access window of this length when signing is denied for want of approved access (30m, 8h, 2d). The provider accepts 1m to 30d as a sanity check; the real limit is the Signer's approval policy. The window starts when the request is approved, so time spent waiting for an approver does not eat into it. |
approval.signing_count | No | None | Ask for access good for this many signing operations. |
approval.exclude_scope_fields | No | None | Signing parameters to leave out of the requests the provider opens, so one approval covers any value of them. Any of command, signing_application, signing_application_hash, hostname, os_username, ip_address, data_hash. An unknown name is rejected at startup. |
approval.ip_address | No | None | Pin the requests the provider opens to this address instead of the one Infisical sees them arrive from, which is what it uses when this is unset. It does not have to be this machine's, so you can name a build agent's egress address. |
log_level | No | info | How much detail to log: trace, debug, info, warn, or error. |
log_file | No | %ProgramData%\Infisical\ksp.log | Where to write the log. signtool has no window to print to, so the plug-in writes here by default. |
| Variable | Description |
|---|---|
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID | The machine identity's client ID. Wins over the config file. |
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET | The machine identity's secret. Wins over the config file. |
INFISICAL_TOKEN | An Infisical access token (a user or machine identity token). Selects token auth; used instead of the client ID/secret. |
INFISICAL_SERVER_URL | The Infisical instance URL. Sets server_url (and overrides the config file). |
INFISICAL_CONFIG | Path to the config file (default %ProgramData%\Infisical\config.json). |
First, put the Signer's certificate on the computer. A certificate is the public half of your signing identity, so it is safe to share. In Infisical, open the Signer and use Export certificate to download it, or paste it in:
$certPem = @"
-----BEGIN CERTIFICATE-----
...paste the certificate here...
-----END CERTIFICATE-----
"@
Set-Content -Path "$env:ProgramData\Infisical\demo-signer.cer" -Value $certPem -Encoding ascii
Then sign your file. /kc picks the Signer by name, and /f points to the certificate file you just saved:
signtool sign /fd SHA256 `
/f "$env:ProgramData\Infisical\demo-signer.cer" `
/csp "Infisical Key Storage Provider" /kc "demo-signer" `
MyApp.exe
Check that the file is signed:
signtool verify /pa /v MyApp.exe
When a Signer has an approval policy, signtool fails until someone has active signing access. The provider can open that request for you on the first denied signing attempt. Add an approval block to %ProgramData%\Infisical\config.json:
{
"server_url": "https://app.infisical.com",
"approval": {
"signing_duration": "8h",
"signing_count": 10
}
}
signing_duration asks for an access window of this length (30m, 8h, 2d).signing_count asks for access good for this many signing operations.Set one or both, depending on what the Signer's policy expects. Both are capped by the policy's own limits.
The request is scoped to what the provider saw: the command, the host process, the machine, the Windows account, and the artifact. Approvers review the real signing situation, and the approval only covers that situation.
<Warning> Values following credential flags (`/p`, `-storepass`, `-keypass`, `-pass`, `--password`, and MSBuild's `/p:Password=`) are redacted before the command line leaves the machine, because it is stored on the approval record where approvers and auditors can read it. Everything else in the command line is sent as-is, so review your commands if they carry other sensitive arguments. </Warning> <Note> The first signing attempt **still fails**, because an approver has to act on the request. Once approved, run the same command again and it succeeds.The approval covers that exact command and artifact, so a different artifact, a changed argument, or reordered flags needs a new request. A request's parameters are fixed once it is open, so when they are wrong the request is rejected and reopened rather than edited. To have one approval cover a batch, leave the parameters that vary out of the request from the start. </Note>
Every parameter on a request has to match at signing time, so a request that names the artifact hash covers exactly one artifact. To have one approval cover a series of builds, name the parameters that vary in approval.exclude_scope_fields and the provider leaves them off every request it opens:
{
"approval": {
"signing_duration": "8h",
"signing_count": 10,
"exclude_scope_fields": ["data_hash"]
}
}
data_hash is the usual one. A timestamped signature changes the digest between runs even for the same file, so pinning it means a fresh approval for every build. The other names are command, signing_application, signing_application_hash, hostname, os_username and ip_address.
Leave out only what genuinely varies, and be aware that dropping a parameter widens the approval further than the machine you configured it on. An approval belongs to the identity that asked for it, not to a host, so once hostname is excluded the same approval covers that identity signing from anywhere its credentials are installed.
An excluded parameter is still recorded on the signing operation, so the command, machine, account and artifact hash behind every signature stay available to auditors even when they no longer constrain the approval.
The address is the one parameter the provider cannot observe, because what counts is the address Infisical receives the sign call from, after any NAT or proxy in between. Infisical fills it in from the request itself, the same value it records in the audit log, so requests are scoped by address without any configuration.
Two settings change that:
{
"approval": {
"ip_address": "203.0.113.10",
"exclude_scope_fields": ["ip_address"]
}
}
ip_address pins a different address than the request came from, which is how you tie an approval to a build agent's egress address from elsewhere. Excluding ip_address leaves signing unrestricted by address, which is what a fleet behind a rotating NAT pool needs, since a pinned address would stop matching between runs. Setting both leaves the address unscoped, because excluding wins.
Infisical always compares against the address it sees, never one a caller reports, so neither setting can widen access beyond what the approval already allows.
signtool sign ... /csp "Infisical Key Storage Provider" /kc <signer>.approval block and opens a scoped signing request.For fully unattended CI, have an Administrator pre-approve signing before the pipeline runs.
The plug-in writes a log to %ProgramData%\Infisical\ksp.log. signtool itself only shows a short error code, so this log is the best place to see what actually went wrong and how to fix it. For more detail, set log_level to debug.
To remove the provider, run this in an Administrator PowerShell, then restart the computer (Windows keeps the provider in its cached list until a reboot):
$prov = "Infisical Key Storage Provider"
$base = "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography"
Remove-Item "$base\Providers\$prov" -Recurse -Force -ErrorAction SilentlyContinue
$iface = "$base\Configuration\Local\Default\00010001\KEY_STORAGE"
$cur = @((Get-ItemProperty $iface -ErrorAction SilentlyContinue).Providers) | Where-Object { $_ -ne $prov }
Set-ItemProperty -Path $iface -Name Providers -Value $cur
Remove-Item "$env:windir\System32\infisical-ksp.dll" -Force -ErrorAction SilentlyContinue
Remove-Item "$env:windir\SysWOW64\infisical-ksp.dll" -Force -ErrorAction SilentlyContinue
This deletes the registry entries and removes the DLL from System32. Your %ProgramData%\Infisical config and log files are left in place; delete that folder too if you no longer need them.