Code Signing: Certificates, Timestamping and Key Custody
How code signing actually runs: the signing pipeline, RFC 3161 timestamping, HSM key custody, stolen signing keys in real incidents, and platform rules.
Every operating system you use makes a trust decision about executable code dozens of times a day. Windows decides whether a driver loads. macOS decides whether an app opens on first launch. A package manager decides whether an update came from the distribution it claims. All of those decisions run on code signing.
The cryptography underneath is ordinary. A hash of the file gets signed with a private key, and a verifier checks that signature against a certificate. That primitive is covered in How Digital Signatures Work, and the certificate chain that makes the signer's key believable is covered in What Is PKI. This guide is about everything built on top: how a real signing pipeline runs, why timestamping decides whether your releases survive certificate expiry, where the key is supposed to live, what happened when signing keys were stolen, and how Authenticode, macOS notarization and sigstore differ in practice.
What a code signing certificate is
A code signing certificate is an X.509 certificate with the code signing Extended Key Usage OID (1.3.6.1.5.5.7.3.3) in it. That EKU is the whole distinction. A TLS certificate for your website cannot sign software, because verifiers check the EKU and reject a certificate presented for the wrong purpose.
Two other properties matter more than most teams expect.
Validity is capped. Publicly trusted code signing certificates run up to 39 months. That cap is why timestamping is mandatory: your software will outlive the certificate that signed it.
The key must be in hardware. Since 1 June 2023 the CA/Browser Forum code signing Baseline Requirements have required subscriber private keys to be generated and stored in a cryptographic module meeting FIPS 140-2 Level 2, FIPS 140-3 Level 2, or Common Criteria EAL 4+. Before that date, an organization validated (OV) certificate could arrive as a .pfx file in an email attachment, and a great many of them ended up on build servers and developer laptops. Extended validation (EV) certificates always required a hardware token. The 2023 change made hardware the floor for everyone.
An EV code signing certificate uses the same algorithms as an OV one. What you buy is a heavier identity check on the organization and, on Windows, faster accumulation of SmartScreen reputation for new binaries. The signature itself is not mathematically stronger.
The signing pipeline, in order
A production signing flow has six steps, and each one is a place where things go wrong.
- Build the artifact and freeze it. Whatever you sign is what ships. Any post-signing step that rewrites the file (a packer, a resource editor, a CI script that stamps a version) breaks the signature.
- Compute the digest. The signer hashes the file with SHA-256. For a Windows PE file this is not a plain hash of the bytes on disk, which matters and is covered below.
- Sign the digest. The private key produces a signature over the digest, wrapped in a CMS SignedData structure (RFC 5652).
- Embed the signature. Most formats have a dedicated slot for it, so the signed file is still a single runnable file.
- Timestamp the signature. The signature blob is sent to a time-stamping authority, which returns a countersigned token that is stored alongside the signature.
- Verify what you are about to ship. Verify on a clean machine that does not have your signing chain manually installed, because a machine that trusts your test root will happily accept a signature real users will reject.
Why a PE hash is not a file hash
Authenticode cannot hash the whole file, because embedding the signature would change the file and break the hash it just computed. So the Authenticode digest deliberately skips three regions:
| Skipped region | Size | Why |
|---|---|---|
CheckSum field in the optional header | 4 bytes | Recalculated after the signature is embedded |
| Certificate Table entry in the data directory (index 4) | 8 bytes | Records the offset and size of the signature that does not exist yet |
| The attribute certificate table itself | variable | This is where the signature blob goes |
The resulting digest goes into a SpcIndirectDataContent structure, which becomes the content of the PKCS#7 SignedData blob. That blob is appended to the file in a WIN_CERTIFICATE structure, and the data directory entry is updated to point at it. Verification recomputes the digest with the same exclusions and compares.
macOS does something structurally similar with code directories and hash slots per page, and Android's APK Signature Scheme v2 signs the whole APK contents while leaving the signature block out of its own coverage. Every format solves the same self-reference problem.
A worked signing command
On Windows, with a certificate whose key is on a hardware token. This is cmd.exe syntax, where ^ continues a line; in PowerShell the continuation character is a backtick and this would break as written.
signtool sign /fd SHA256 /a ^
/tr http://timestamp.digicert.com ^
/td SHA256 ^
/d "Acme Updater" ^
AcmeUpdater.exe
signtool verify /pa /v AcmeUpdater.exe
/fd SHA256 sets the file digest algorithm. /tr is an RFC 3161 timestamp URL and /td SHA256 sets the timestamp digest. /pa on verify means "use the default authenticode policy", which is what a normal user's machine does, rather than the driver-signing policy that signtool verify uses by default and which will pass or fail for different reasons.
Timestamping, and why it decides whether your releases survive
A signature is verified against a certificate, and a certificate has a validity window. Without extra information, a verifier checking a signature has only one clock to work with: now. If the certificate expired last month, the verifier cannot tell whether the signature was made legitimately two years ago or forged yesterday with a compromised key. The safe answer is to reject it.
RFC 3161 solves this. The signer sends a hash of the signature value to a time-stamping authority (TSA). The TSA returns a TimeStampToken: a CMS structure containing that hash plus the TSA's own trusted time, signed by the TSA's key. That token is stored back into the file as an unsigned attribute on the signer info, an attached countersignature.
Now the verifier has two clocks. It checks the TSA's signature to establish when the signing happened, then checks the signing certificate's validity against that time rather than the present. Concretely:
| Signing certificate valid | Binary signed | Verified on 2026-08-25 | Result | |
|---|---|---|---|---|
| No timestamp | 2023-01-15 to 2026-01-15 | 2025-06-01 | Certificate expired 7 months ago | Fails or warns |
| RFC 3161 timestamp | 2023-01-15 to 2026-01-15 | 2025-06-01 | Signed inside the validity window | Valid |
The practical rule: an untimestamped release has a fuse on it, and the fuse burns out on the certificate's expiry date. Teams discover this when a three-year-old installer that thousands of customers still deploy suddenly starts throwing publisher warnings, and the fix requires re-signing and re-shipping every affected artifact.
signtool verify /pa /v prints the timestamp block when one is present. If your release checklist only confirms "signature valid", it passes today and fails in three years. Assert on the presence of the countersignature in CI, and treat a TSA outage as a build failure rather than something to retry without the /tr flag.
Revocation is date-scoped
Timestamping also changes what revocation means. When a code signing certificate is revoked, the revocation entry can carry an invalidity or effective date. Windows Authenticode applies it as a cutoff: signatures timestamped before that date remain valid, signatures timestamped after it fail. Verifier behaviour differs by platform, so confirm what your target actually does before relying on the cutoff.
That is deliberate. A vendor whose key is stolen in March should not have every release since 2019 stop working. It also means the accuracy of your compromise timeline becomes a security control. Set the effective date too late and the attacker's signed malware stays trusted. Set it too early and you invalidate your own shipping releases. The general mechanics of revocation, CRLs and OCSP are covered in What Is PKI.
Key custody: the control that actually matters
Everything above is arithmetic. The security of code signing is a key custody problem, because an attacker who obtains signing capability inherits your entire installed base's trust.
Custody models, roughly in ascending order of what they resist:
| Model | Key location | What an attacker needs | Main weakness |
|---|---|---|---|
| PFX on a build agent | Disk, often in a repo secret | Read access to one machine | Key is exportable, theft is silent |
| Hardware token at a desk | USB token, PIN protected | Physical access plus the PIN | Manual, does not scale, tokens get left plugged in |
| Network or cloud HSM | HSM, unexportable | Credentials to call the signing API | Signing access becomes the target |
| Managed signing service | Provider HSM | Authorized pipeline identity | Trust shifts to the provider and its authz |
| Keyless (sigstore) | Ephemeral, minutes | Ability to impersonate the OIDC identity | Identity provider compromise, log monitoring required |
Note what moves as you go down the table. The key stops being stealable and the permission to sign becomes the asset. That is a better position, because permissions can be scoped, logged, rate limited and revoked, and a key on a disk cannot be any of those things. Design the signing service so that the pipeline identity is narrow and the human path requires a second person, an application of least privilege with unusually high stakes.
Practical controls worth having:
- Separate the release key from the test key. Nightly builds and internal tooling get an internal CA, never the publicly trusted certificate. This alone eliminates most of the pressure to put the real key somewhere convenient.
- Sign from a dedicated service. The build submits an artifact and a justification; the service signs and logs.
- Log every signature with the artifact hash. When an incident starts, the first question is "what did we sign, and when". A signing log answers it in minutes. Without one, you are guessing at the revocation effective date.
- Alert on signatures you did not request. Correlate the signing log against your release records, and watch for your publisher name appearing on binaries you never built.
- Rehearse revocation. Know who calls the CA, what evidence they need, and how long re-issuance takes. Most teams find out during the incident that it takes days.
What stolen signing keys did
The reason to care about custody is documented in incidents. Each of these produced malware that verified correctly on a fully patched machine.
Stuxnet, 2010. Its kernel drivers were signed with certificates stolen from Realtek Semiconductor and, later, JMicron Technology, two companies with offices in the same Taiwanese science park. Signed drivers loaded without the warning an unsigned driver would trigger. From here on, stolen code signing keys were a standard tool.
Adobe, 2012. An attacker compromised a build server with access to Adobe's code signing infrastructure and had two malicious utilities signed with a legitimate Adobe certificate. Adobe revoked the certificate and re-issued. Its own disclosure scoped the impact to three affected products signed after 10 July 2012, which were re-signed and re-released.
Plead malware, 2018. The BlackTech group signed its payloads with certificates stolen from D-Link and from another Taiwanese firm, Changing Information Technology. Both certificates were revoked, and signed samples kept circulating afterwards.
ASUS ShadowHammer, 2019. The ASUS Live Update utility was backdoored and distributed from official ASUS update servers, signed with legitimate ASUS certificates. Users receiving it had no signal available to them: the vendor, the channel and the signature were all genuine.
SolarWinds Orion, 2020. The SUNBURST backdoor was inserted into the build process, so SolarWinds' own pipeline signed it with SolarWinds' own valid certificate. No key was stolen. The attacker simply moved upstream of the signature, which is the defining pattern of modern supply chain compromise and a large part of why APT operations target build systems.
NVIDIA, 2022. The LAPSUS$ group leaked NVIDIA code signing certificates. Although expired, they remained usable for signing Windows drivers under certain policy conditions, and samples signed with them appeared quickly. The response ran through driver blocklist policy as well as revocation.
3CX, 2023. A trojanized 3CX desktop client was distributed through the vendor's normal channel, signed with 3CX's certificate. As with ASUS, the compromise was upstream of signing.
Two patterns run through the list. Early incidents stole keys. Recent incidents compromise the build and let the legitimate signing process do the work, which is untouched by any amount of key protection. The countermeasure to the second pattern is build integrity and provenance, which is why reproducible builds and attestations get so much attention now.
Every incident above produced binaries that passed verification. Treating a valid signature as a reason to skip analysis is the exact assumption these campaigns were built to exploit. A signature answers "who" and "unchanged since", and those are the only questions it answers.
Platform specifics
The mechanism is shared. The policy wrapped around it differs sharply by platform.
| Platform | Format | Trust anchor | Distinctive rule |
|---|---|---|---|
| Windows (Authenticode) | PKCS#7 in the PE attribute certificate table | Microsoft Trusted Root Program | Kernel drivers need an additional Microsoft attestation signature; SmartScreen weighs publisher reputation on top of validity |
| macOS | Code directory hashes plus CMS in the Mach-O | Apple Developer ID | Notarization is a separate Apple scan with a stapled ticket; hardened runtime is required for notarization; arm64 binaries must carry at least an ad hoc signature to run at all |
| Android | APK Signature Scheme v2 / v3 / v4 | The app's own key, pinned on first install | Trust is key continuity: an update must be signed by the same key (or a v3 rotation proof) or it will not install |
| Linux distributions | OpenPGP signatures over repository metadata (Debian) or over each package (RPM) | Distribution keyring shipped with the OS | Debian packages are trusted transitively through a signed index of hashes |
| Container images and OSS artifacts | sigstore (cosign) | Fulcio CA plus the Rekor transparency log | Keyless: a short-lived certificate bound to an OIDC identity, with the signature publicly logged |
Three of those deserve a note.
macOS separates identity from inspection. A Developer ID signature answers who built it. Notarization is Apple running an automated malware scan on the uploaded build and issuing a ticket if it passes. Gatekeeper wants both. xcrun stapler staple attaches the ticket to the app so first launch works offline; skip stapling and the first launch on a machine without connectivity behaves unpredictably.
Android inverts the CA model. There is no authority vouching for the developer. The system records the signing key at first install and refuses any update signed by a different key. That gives strong continuity and makes key loss catastrophic, which is why Google Play App Signing exists: Google holds the app signing key and you hold an upload key that can be reset.
Sigstore removes the long-lived key. Cosign requests a certificate from Fulcio, proving an OIDC identity such as a specific GitHub Actions workflow. The certificate is valid for about ten minutes. The signature and certificate are written to Rekor, an append-only transparency log built on the same idea as Certificate Transparency. Verification checks the identity that signed and the log inclusion proof. There is no key to steal, and the trade is that your CI identity provider and your log monitoring become the things that must hold.
How a package manager actually verifies an update
Repository signing works one level up from the artifact, which is why a mirror can be untrusted and the update still safe.
On Debian and Ubuntu, the repository publishes a Release file listing the size and SHA-256 of every index (Packages, Sources, translations). That file is signed with the archive key, either as a detached Release.gpg or as InRelease, which carries the signature inline. Apt fetches InRelease, checks it against a keyring installed with the OS or pinned per source with signed-by=, then checks each downloaded index against the hash in the signed Release, then checks each .deb against the hash in the signed Packages. Individual .deb files carry no signature of their own. The chain is signature over index hashes, index hashes over packages, so a hostile mirror can serve the bytes and change nothing.
RPM distributions sign each package directly. The RPM header carries an OpenPGP signature checked against keys imported with rpm --import, and repomd.xml is signed as well, so both the metadata path and the package path are covered independently.
Two failure modes follow from the shape. A repository added without its key, or with [trusted=yes], skips the whole chain. And an expired archive key stops updates entirely until the new keyring lands, which is why distributions ship key rotations well ahead of expiry.
Failure modes
Ranked by how often they show up in real environments.
- No timestamp. The single most common defect, and its cost is deferred by years, which is exactly why it survives review.
- The key on a build agent. Now also a compliance failure for publicly trusted certificates. If your CI secret store holds a
.pfx, that is the finding. - Signing the wrong thing. Signing an artifact that was never tested, or signing before a post-processing step that rewrites the binary and silently invalidates the signature.
- One certificate for everything. Nightly builds, internal tools, customer releases and driver packages all under one certificate means one compromise revokes all of them at once.
- Verifying on a machine that trusts your test root. The signature passes for you and fails for every customer. Verify on a clean image.
- Assuming revocation is fast or complete. Revocation propagation depends on CRL and OCSP behavior that varies by platform and by cached state, and offline machines may never learn about it.
- Deprecated algorithms. SHA-1 Authenticode signatures are no longer trusted for current use. Anything still shipping with a SHA-1 file digest needs re-signing with SHA-256.
- Treating a checksum page as a signature. A SHA-256 hash published on the same website that serves the download proves only that the file matches what that website says. See How Hashing Works for what a hash does and does not establish. A signature carries an identity that survives the website being compromised.
- Certificates obtained by shell companies. Commodity malware routinely ships with valid, freshly issued certificates registered to plausible-sounding front companies, which is how signed infostealer builds reach users with fewer warnings.
Verification and detection for defenders
If you defend endpoints rather than build software, code signing gives you telemetry that is worth using carefully.
Signature status on process creation (Sysmon Event ID 1 records signature, signer and signature status) is a useful ranking signal. A signed binary from a known publisher is lower priority than an unsigned one from a temp directory. Treat it as ranking only, because the incidents above all appeared as signed binaries from known publishers.
The higher-value signals are anomalies in the signature itself:
- A publisher name you have never seen in the environment, on a binary in a user-writable path.
- A certificate issued days before the binary appeared, with no other history.
- A signature that is present but invalid, which usually means the file was modified after signing.
- A driver signed by a vendor with no business on that host, the precondition for the vulnerable-driver technique described in What Is a Rootkit.
- Your own publisher name on a build your signing log does not account for.
On Windows, application control (WDAC, or AppLocker publisher rules) turns signature identity into an enforcement decision, which is where signing pays off defensively. And whatever your policy, keep the analysis step: a signed sample still goes through malware analysis when the behavior is anomalous.
Verdict
Code signing does two jobs well. It makes post-build tampering detectable, and it attaches an accountable identity to a release. Those are real and worth the operational cost.
The attacks moved from stealing keys to compromising the build, which defeats key protection entirely and is answered only by build integrity, provenance attestation and reproducibility.
For a team shipping software today, the priorities are unambiguous. Timestamp everything. Put the key in hardware you cannot export it from, and put a service in front of it so signing becomes a scoped permission. Log every signature against an artifact hash so an incident starts with facts. Separate internal signing from release signing. Then spend the remaining effort upstream of the signature, on the build system, because that is where the last decade of serious compromises actually happened.
Frequently asked questions
Where should a code signing private key be stored? In hardware that cannot export it: a FIPS 140-2 Level 2 or higher token, a network HSM, or a managed cloud signing service. Build agents should hold credentials that request a signature, never the key itself.
How is macOS notarization different from signing? Signing with a Developer ID certificate proves who built the app. Notarization is a separate Apple scan that issues a ticket if the build passes. Gatekeeper wants both, and stapling attaches the ticket so a first launch works offline.
Does code signing prevent supply chain attacks? It narrows them. Signing stops tampering after the build and makes an unauthorized publisher obvious. It does nothing about compromise before the signature, which is where modern supply chain attacks operate.
Related guides
Sources & further reading
- Security Considerations for Code Signing (NIST Cybersecurity White Paper) (NIST)
- Baseline Requirements for the Issuance and Management of Publicly-Trusted Code Signing Certificates (CA/Browser Forum)
- RFC 3161: Internet X.509 PKI Time-Stamp Protocol (TSP) (IETF)
- RFC 5652: Cryptographic Message Syntax (CMS) (IETF)
- Windows Authenticode Portable Executable Signature Format (Microsoft)
- Notarizing macOS Software Before Distribution (Apple)
- Application Signing (Android Open Source Project) (Android Open Source Project)
- Securing the Software Supply Chain: Recommended Practices for Developers (CISA / NSA / ODNI)