Skip to content
ecdsa.com
← All articles

Authenticode code signing: keys, timestamps, verification

We signed a small Windows executable with osslsigncode 2.14 and a DigiCert timestamp, then put it through three tests: a copy signed without a timestamp, one byte of code changed, and data hidden in the certificate table. The rules come from the CA/Browser Forum Code Signing Baseline Requirements 3.11.0 and Microsoft's own documentation, checked in October 2026.

Short answer. Authenticode embeds a PKCS #7 SignedData in a table at the end of a Windows PE file; the signature covers a hash of the file that skips the checksum, the pointer to that table and the table itself. Since 1 June 2023 a publicly trusted code signing key must be generated and used in a hardware module (a token, an on-premises HSM or a cloud HSM service with full audit logging), so the familiar .pfx file on a build server no longer gets you a certificate. Certificates issued since 1 March 2026 live at most 460 days, and Microsoft's Artifact Signing issues ones that live 72 hours, which makes the RFC 3161 timestamp the part that keeps a signature valid: always sign with /fd SHA256 /tr <TSA URL> /td SHA256. EV certificates no longer buy instant SmartScreen reputation. Verify with signtool verify /pa /tw on Windows or osslsigncode verify elsewhere.

What gets signed

A PE file (.exe, .dll, .sys) has a data directory in its optional header; entry 4, the Certificate Table, holds the file offset and size of a list of WIN_CERTIFICATE structures appended to the end of the file. For Authenticode the entry has revision 0x0200 and type WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002), and contains a PKCS #7 SignedData (PE format).

The signed content is not the file but an SpcIndirectDataContent holding the file's hash. That hash cannot cover the signature itself, so, in Microsoft's words, it "must exclude certain fields in the PE Image, such as the Checksum and Certificate Table entry in Optional Header Data Directories", and the certificate table. Everything else, headers, sections and data after the last section, is hashed.

Our sample is a 1.6 MB program cross-compiled with Go 1.27.3 (GOOS=windows GOARCH=amd64). A short Python script reads the certificate table entry before and after signing:

python3 pe.py hello.exe show; python3 pe.py hello-signed.exe show
file size 1651712, certificate table at offset 0, size 0
file size 1660952, certificate table at offset 1651712, size 9240
WIN_CERTIFICATE: dwLength 9240, wRevision 0x0200, wCertificateType 0x0002

The 9,240 bytes are the signature, the certificate chain and the timestamp token. Listing the object identifiers inside the extracted signature shows the Authenticode-specific parts next to the usual CMS ones:

openssl asn1parse -inform DER -in sig.der | grep -o 'OBJECT *:[^ ]*' | sort | uniq -c (excerpt)
      1 OBJECT            :1.3.6.1.4.1.311.2.1.11
      1 OBJECT            :1.3.6.1.4.1.311.2.1.12
      1 OBJECT            :1.3.6.1.4.1.311.2.1.15
      2 OBJECT            :1.3.6.1.4.1.311.2.1.4
      1 OBJECT            :1.3.6.1.4.1.311.3.3.1
      2 OBJECT            :id-smime-ct-TSTInfo
      2 OBJECT            :messageDigest
      2 OBJECT            :signingTime

1.3.6.1.4.1.311.2.1.4 is SpcIndirectDataContent, …2.1.15 the PE image data inside it, …2.1.11 and …2.1.12 the statement type and the program name and URL, and 1.3.6.1.4.1.311.3.3.1 the RFC 3161 timestamp token stored as an unsigned attribute, whose content type is id-smime-ct-TSTInfo.

Certificates and keys in 2026

Publicly trusted code signing certificates are governed by the CA/Browser Forum Code Signing Baseline Requirements (CSBR), version 3.11.0 of 16 June 2026. The rules that change how you build:

  • Hardware keys for everyone. "Effective June 1, 2023", the subscriber's private key must be generated, stored and used in a hardware crypto module certified to at least FIPS 140-2 Level 2 or Common Criteria EAL 4+ (§6.2.7.4.1). The allowed options are a hardware module you operate, a cloud key service whose HSM meets that bar and whose subscription is "configured to log all access, operations, and configuration changes", or a signing service. The CA has to verify it, for example by shipping a token with a pre-generated key or by checking a key attestation (§6.2.7.4.2). Before 2023 this applied only to EV certificates.
  • Key sizes. RSA keys of at least 3072 bits, or ECDSA on P-256, P-384 or P-521 (§6.1.5.2).
  • Lifetime. At most 460 days for certificates issued on or after 1 March 2026; 39 months before that (§6.3.2).
  • Timestamp authorities. TSA certificates last at most 135 months and their keys may be used for 15 months after notBefore; SHA-1 timestamp tokens have been banned since April 2022.

Microsoft adds its own layers on top. Kernel-mode drivers must be signed by Microsoft through the Windows Hardware Dev Center since Windows 10 version 1607, so a commercial certificate alone does not load a driver (driver signing policy). And EV no longer means "no SmartScreen warning": Microsoft's distribution feature status page says that "no certificate type (OV or EV) grants an immediate bypass" since the 2024 Trusted Root Program change; reputation accumulates per file hash and per signing identity.

Microsoft's managed service, Artifact Signing (renamed from Trusted Signing), keeps keys in Microsoft-operated HSMs and issues certificates that "are renewed daily and are valid for only 72 hours" (certificate management). Only the digest leaves your build machine. With certificates that short, a signature without a timestamp is useless three days later. Two details from the same page: because the certificate changes every day, each certificate profile gets a durable custom EKU under 1.3.6.1.4.1.311.97. to pin trust to (for example in application control policies), and its CAs are being removed from the CCADB and are no longer in the Microsoft Trusted Root Program, but "remain on the Windows Platform Trust List", so Windows keeps trusting them while other platforms may not.

Signing and verifying with osslsigncode

osslsigncode implements Authenticode on Linux and macOS. The lab (Debian package 2.14 in debian:forky-20261005-slim) created a private test hierarchy, a root, an issuing CA and an RSA 3072 code signing certificate with EKU codeSigning, signed the program and asked DigiCert's public TSA for a timestamp:

lab.sh (excerpt): sign with an RFC 3161 timestamp, verify
osslsigncode sign -certs chain.pem -key codesign.key -h sha256 -n "Hello" -i "https://example.test/hello" \
  -ts http://timestamp.digicert.com -in hello.exe -out hello-signed.exe
osslsigncode verify -CAfile root.pem -TSA-CAfile /etc/ssl/certs/ca-certificates.crt -in hello-signed.exe

Only a hash of the signature goes to the TSA. The verification, with the repeated chain listings shortened:

osslsigncode verify (shortened)
Message digest algorithm  : SHA256
Current message digest    : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Calculated message digest : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 

Signer's certificate:
	------------------
	Signer #0:
		Subject: CN=Example Software GmbH,O=Example Software GmbH,C=DE
		Issuer : CN=Example Test Code Signing CA,O=Example Test
		Serial : 20ADC33D7219F573885B44765077E96312F72F6D
		Certificate expiration date:
			notBefore : Oct 22 06:32:28 2026 GMT
			notAfter : Nov 21 06:32:28 2026 GMT

Authenticated attributes:
	Signing time: Oct 22 06:32:28 2026 GMT
	Microsoft Individual Code Signing purpose
	Message digest: DE669CE86A064FAEB4DFE8E43E14090CE5912FA036C527C13D5558B0BF110BB7 
	URL description: https://example.test/hello
	Text description: Hello

Countersignatures:
	Timestamp time: Oct 22 06:32:29 2026 GMT
	Signing time: Oct 22 06:32:29 2026 GMT
	Hash Algorithm: sha256
	Issuer: CN=DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1,O=DigiCert\, Inc.,C=US
	Serial: 084FDC334F7E454EDBC30F8FF9921835
...
Timestamp Server Signature CRL verification: ok
Timestamp Server Signature verification: ok
Signature verification time: Oct 22 06:32:29 2026 GMT
...
Signature verification: ok

Number of verified signatures: 1
Succeeded

"Current message digest" is the hash stored in the signature, "Calculated" the one recomputed from the file. The signer's own "Signing time" is just a claim by the signer's clock; the "Timestamp time" is signed by the TSA and is the one verifiers rely on. osslsigncode then validated the signature at the timestamp time, which is the point of the whole construction.

Why the timestamp matters

To see it, the lab signed the same program a second time without -ts, then verified both files as if it were 60 days later, when the 30-day test certificate had expired (osslsigncode verify -time):

Verification at now + 60 days (lines filtered, OpenSSL error location shortened)
hello-signed.exe   Signature verification time: Oct 22 06:32:29 2026 GMT Signature verification: ok Succeeded
hello-nots.exe     ...:PKCS7_verify:certificate verify error:...:Verify error: certificate has expired Signature verification time: Dec 21 06:32:29 2026 GMT Error: certificate has expired ... Signature verification: failed Failed

With a timestamp the check happens at the proven signing time, so the signature outlives the signing certificate (DigiCert's timestamp responder certificate in this run is itself valid until November 2037). Without one, verification uses the current time and fails the day the certificate expires, the same certificate has expired failure as any TLS client. Microsoft goes further: if the timestamp falls inside the validity periods of both the signing and the TSA certificate, the signature is valid, and "It's valid long after the signing certificate and the TSA certificate expire (unless either are revoked)."

Revocation is time-bound too. The CSBR let a CA set the revocationDate of a code signing certificate to the date it was first considered invalid, for key compromise "the earliest date of suspected compromise", and "software should continue to treat objects containing a timestamp dated before the revocation date as valid" (§4.9.6). A leaked key therefore invalidates the signatures timestamped after the compromise, not every release you ever shipped.

What breaks a signature, and what does not

Two modifications, verified with the same command:

One byte of code changed (offset 4096), then 64 bytes appended inside the certificate table
### one byte of code changed
Signature verification: failed
Message digest algorithm  : SHA256
Current message digest    : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Calculated message digest : E754D0A7C1AB0CA1716866C67046BA54D2C195CDCBA4A182D56DD5C57FCD37B6     MISMATCH!!!
Failed
### extra bytes inside the certificate table
appended 64 bytes inside the certificate table: size 9240 -> 9304
Warning: invalid PE checksum
Calculated message digest : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Timestamp Server Signature verification: ok
Signature verification: ok
Succeeded

The second file grew the certificate table entry and the WIN_CERTIFICATE length by 64 bytes and put data there. Because the table is excluded from the hash, the signature still verifies; only the PE checksum, also excluded, is wrong. That is the layout behind CVE-2013-3900: installers that read configuration from the end of their own file could be given an attacker's payload without invalidating the signature. Microsoft fixed it in 2013 as an opt-in registry setting, EnableCertPaddingCheck, and its 2022 republication says it "does not plan to enforce the stricter verification behavior as a default". CISA added the CVE to its Known Exploited Vulnerabilities catalog in January 2022. The lesson for developers: never read data from the certificate table area of your own executable, and set EnableCertPaddingCheck on machines you manage.

On Windows: signtool

SignTool from the Windows SDK is the reference implementation. Current versions require the digest algorithms explicitly: "The SignTool sign command requires the file digest algorithm option (/fd) and the time stamp digest algorithm option (/td)" (SignTool). With the certificate in the Windows store and the key on a token, the usual commands are:

signtool (from Microsoft's documentation; not run here)
signtool sign /n "Example Software GmbH" /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /d "Hello" MyApp.exe
signtool verify /pa /tw /v MyApp.exe
  • /n or /sha1 selects the certificate; the key stays in the token or HSM behind its CSP or KSP (/csp and /kc name them explicitly).
  • /tr asks an RFC 3161 server; the older /t uses the legacy Authenticode timestamp protocol.
  • verify /pa uses the default Authenticode policy; without it, SignTool applies the driver policy and rejects ordinary signatures. /tw warns when a signature has no timestamp, which belongs in every release pipeline.
  • /dlib and /dmdf plug in a library that signs the digest remotely; Artifact Signing ships one, and the open-source Jsign (Apache-2.0, version 7.6) does the same from any OS with AWS KMS, Google Cloud KMS, Azure Key Vault, PKCS #11 tokens and YubiKeys.

On the verifying side, PowerShell's Get-AuthenticodeSignature returns the status and signer, and Windows Explorer's "Digital Signatures" tab shows the timestamp.

Signing in a build pipeline

With hardware-bound keys, the build server no longer holds a key file. Three setups work:

  1. A cloud key service: generate the key in AWS KMS, Google Cloud HSM or Azure Key Vault Premium, get the certificate from a CA that accepts that service's key attestation or audit report, and sign the digest remotely with Jsign or a vendor tool. The audit log requirement is part of the CSBR; signing with cloud KMS shows which logs are off by default.
  2. A managed signing service such as Artifact Signing or a CA's own: the service holds key and certificate, you send digests.
  3. A token on a dedicated signing host, reachable only from the release job. Simple, but a single machine is now on your critical path.

In every case, sign only release artifacts, from a protected branch, with the timestamp, and record the file hash and signing identity in the release notes so users and EDR tools can check what you shipped.

Common mistakes

  • No timestamp. Everything works until the certificate expires, then every installed copy shows an invalid signature. With 72-hour or 460-day certificates that day comes soon.
  • SHA-1 anywhere. Omitting /fd used to default to SHA-1; current SignTool refuses, older scripts copied from forums still contain /fd sha1 or /t.
  • Wrong verification policy. signtool verify without /pa reports a valid commercial signature as untrusted.
  • A certificate without the codeSigning EKU, or a TLS certificate pressed into service. Verifiers reject it, just as OpenSSL reports unsuitable certificate purpose.
  • Missing intermediate. Include the full chain in the signature (-certs chain.pem, or the CA's intermediate installed on the signing host), or verification depends on what the user's machine happens to have.
  • Expecting EV to silence SmartScreen. Reputation now builds over time; sign every release with the same identity so it carries forward.
  • Reading data from the end of your own signed file (installers, self-extractors): that area is not covered by the signature.

Sources