This is the hands-on companion to our concept chapter on code signing after the CA/B Forum hardware mandate. The goal: a CI pipeline that Authenticode-signs every release build with a key that was generated inside a Thales Luna partition, has never existed outside it, and never will — while the build agent runs unattended and no human presents a credential at release time. The steps assume a Luna Network HSM 7 appliance, Luna HSM Client 10.x on a Windows build agent, and signtool from a current Windows SDK. Command syntax is drawn from Microsoft’s signtool documentation and Thales’ Authenticode integration guidance; version-dependent details are flagged inline, and where a detail could not be verified against current docs, that is said plainly.
Before you start
You will need:
- A Luna application partition dedicated to code signing — not shared with your CA or vault keys. Partition-per-purpose is what makes the lockdown in Step 7 possible.
- The Windows build agent registered as an NTLS client of that partition, with the partition visible as a slot in lunacm.
- The identity of the service account the pipeline agent runs as. Not your admin account — the account that owns the agent process at 2am. This distinction is the single most common cause of “works on my machine, fails in the pipeline” with HSM-backed signing.
- A CA order ready to go (OV or EV — the key handling below is identical), and that CA’s requirements for proving the key is hardware-held.
If the appliance, partition and client trust are not yet in place, work through the Luna HSM design and operational guide first.
Step 1: Generate the key pair in the partition and produce the CSR
The key pair must be born inside the HSM — the CA/B Forum requirement is “generated in”, not “imported into”. On Windows the clean way is certreq with an INF file that names the Luna KSP as the provider, so the key generation call lands in the partition rather than in software:
; codesign.inf
[NewRequest]
Subject = "CN=Example Pty Ltd, O=Example Pty Ltd, C=AU"
KeyAlgorithm = RSA
KeyLength = 3072
Exportable = FALSE
MachineKeySet = TRUE
ProviderName = "SafeNet Key Storage Provider"
KeyUsage = 0x80
HashAlgorithm = sha256
RequestType = PKCS10
[EnhancedKeyUsageExtension]
OID = 1.3.6.1.5.5.7.3.3
certreq -new codesign.inf codesign.csr
Notes on the choices: KeyLength = 3072 is the minimum RSA size the Code Signing Baseline Requirements permit; use 4096 if your CA and tooling are happy with it. Exportable = FALSE is belt-and-braces — the partition policy should already forbid private key export, and for a code signing partition it must. MachineKeySet = TRUE puts the key reference in the machine context so a service account can reach it later. Run the certreq under an account that already has the KSP registered (Step 3 covers registration — for key generation you can do this from an admin session, as long as the machine-level registration exists). The exact subject fields your CA wants come from the CA, not from this INF.
Your CA will also want evidence that the key sits in certified hardware. Luna supports verifiable key attestation: the cmu utility can produce a Public Key Confirmation package — a chain signed back to a Thales manufacturing root proving the key was generated in, and is bound to, a genuine Luna HSM. The exact cmu invocation and package format vary with client version and with what your CA accepts, so we won’t print a flag set here we can’t verify for your combination — check the cmu section of the Thales docs for your client version, and ask your CA which attestation formats they take before you order. Some CAs accept a signed attestation from a company officer for specific HSM models instead.
Step 2: Install the issued certificate on the build agent
Submit the CSR, complete validation, and the CA returns a certificate. Accept it on the same machine where the CSR was generated so Windows can bind the certificate to the HSM-held key:
certreq -accept codesign.cer
certutil -store My
In the certutil -store My output (run elevated for the machine store), find your new certificate and confirm two things: the provider line shows SafeNet Key Storage Provider, and the entry shows a private key is associated. Record the key container name from the same output — signtool needs it in Step 5. You can also enumerate containers held by the Luna KSP directly with certutil -csp "SafeNet Key Storage Provider" -key (elevated; on some client versions the listing requires the partition to be activated first).
Step 3: Register the Luna KSP for the pipeline’s service account
The Luna KSP keeps per-identity registration: each Windows account that will open the partition through CNG must be registered against the slot, with the partition credential, using KspConfig.exe from the Luna client’s KSP directory. Registering your own admin account is what makes interactive testing work — and is exactly the trap, because the pipeline does not run as you. Register the slot for the service account the agent runs as and for NT AUTHORITY\SYSTEM, the same discipline as registering the KSP for an AD CS certificate authority’s service context. We walk the KspConfig screens, the domain-account gotchas and the verification steps in detail in registering the Luna KSP for a service account — the procedure is identical for a build agent; only the account changes.
The quick sanity test once registration is done: open a session as the service account (for example with psexec -u or a scheduled task running whoami-plus-certutil) and confirm certutil -csp "SafeNet Key Storage Provider" -key sees the container. If it works as you and fails as the service account, the KSP registration is the first place to look.
Step 4: Make the partition survive reboots unattended
A CI agent reboots for patching and must come back signing without a human. On a PED-authenticated partition that means activation and auto-activation: policies 22 and 23 enabled, the Crypto Officer’s PED secret cached by a one-time PED login, and thereafter the agent authenticates with the CO challenge secret alone — which lives in your pipeline’s secret store, scoped to the signing jobs, not in a pipeline variable in plain sight. Auto-activation carries the cached secret across reboots and power interruptions up to roughly two hours. The full procedure, including the Remote PED path for reactivation after a longer outage, is its own chapter: configuring Luna partition policies for unattended startup. Do it before you wire the pipeline, and test it by rebooting the appliance, not by assuming.
Step 5: Sign and timestamp from the pipeline
The signing step itself is one signtool invocation. The build agent holds the certificate (public material) and a reference to the key container; the private key operation happens inside the partition over NTLS:
signtool sign /fd SHA256 ^
/csp "SafeNet Key Storage Provider" ^
/kc "<key_container_name>" ^
/f codesign.cer ^
/tr http://timestamp.digicert.com ^
/td SHA256 ^
/d "Release %BUILD_VERSION%" ^
MyApp.exe
What each flag is doing, and the traps:
/csp+/kcname the provider and the key container. The flag says CSP for historical reasons, but it accepts a CNG key storage provider name — passing the Luna KSP here is the documented pattern (Microsoft’s own HSM signing example uses the same construction with the legacy Luna CSP)./fsupplies the certificate file, which deliberately contains no private key. Alternatively, drop/csp /kc /fand select the machine-store certificate with/sm /sha1 <thumbprint>— Windows then finds the Luna key through the certificate’s provider binding. Both work; the explicit form fails more legibly./fd SHA256is effectively mandatory: SDK builds 20236 and later warn (and later versions error) if the file digest is unspecified./truses an RFC 3161 timestamp server (use your CA’s URL), and/td SHA256sets the timestamp digest. A documented signtool quirk:/tdmust come after/tron the command line, or you silently get a SHA-1 timestamp digest.
Do not treat the timestamp as optional. An Authenticode signature without one dies with the certificate: when the cert expires — after at most three years, or 72 hours if you ever migrate to a short-lived cloud service — every unstamped binary you ever shipped starts failing validation. With an RFC 3161 timestamp, the signature is judged against the moment of signing and remains valid indefinitely, which also means revocation-from-a-date can kill a compromised key’s future signatures without invalidating your back catalogue. Timestamping is one flag at signing time and an unwinnable re-signing campaign if deferred; make the pipeline fail the build if the timestamp step fails.
Step 6: Verify in the pipeline, not just on release day
Add a verification stage immediately after signing:
signtool verify /pa /all /tw /v MyApp.exe
/pa applies the standard Authenticode policy (without it, signtool applies the Windows driver policy and will fail non-driver files that are perfectly well signed), /all checks every signature if you dual-sign, /tw raises a warning if the file is not timestamped, and /v prints the chain so the build log shows exactly which certificate signed. Verify on a clean machine periodically too — the build agent trusts things a customer’s machine does not.
Step 7: Lock down which pipelines may sign
An HSM-held key cannot be stolen as a file, so the attack moves up a layer: anyone who can run a job on the signing agent can sign. Malicious code signed by your real key in your real pipeline is a supply-chain incident with your name on it, so the lockdown is where separation of duties earns its keep:
- Dedicated signing agents. Signing happens only on a small, hardened agent pool; only release pipelines for designated repositories may target that pool, enforced by the CI platform’s pool permissions, not convention.
- Dedicated partition, least role. The code signing partition holds nothing else, its clients are only the signing agents, and the pipeline authenticates with the lowest role that can sign — on current Luna firmware the Crypto User role can perform signing with existing keys without the rights to create or delete them; confirm the role capabilities on your firmware and use CU if it fits.
- Protected release path. Signed releases come only from protected branches with enforced review; the challenge secret is scoped so a developer’s feature-branch build cannot reach it.
- Audit both sides. Partition audit logging on, and reconciled: every signing operation in the HSM log should match a pipeline run. A signature with no matching run is your earliest, and possibly only, compromise signal. The governance framing — who authorises a key, who operates it, who audits it — is the key ceremony and separation of duties chapter’s territory.
Where this leaves you
You now have a pipeline that signs unattended from a key no attacker can exfiltrate — which means your remaining risks are operational: the partition’s availability is now release-critical, and a single appliance is a single point of failure for shipping software. That is an HA problem, and the HA chapters of the series cover what replicates and how to prove failover works. The complete series lives at The Luna Field Guide.
Securitribe designs, deploys and operates Thales Luna HSM estates — partitions, HA, key ceremonies and DR that stand up to audit.
Explore Cryptographic HSM SolutionsBook a strategy call →