rubrapack Manual←↑→

38 Authenticode signatures

What a program has to write so that Windows accepts a signed PE file (.exe, .dll) or MSI package. Facts are tagged as in Package formats for implementers: [spec] for Microsoft's "Windows Authenticode Portable Executable Signature Format", the PE/COFF specification and RFC 5652 (CMS); [observed] for files signed by Windows' own signer (PowerShell Set-AuthenticodeSignature, which uses mssign32.dll) and checked with Get-AuthenticodeSignature / WinVerifyTrust on Windows 11.

38.1 Where the signature lives in a PE file#

38.2 The PE digest#

In this order, with SHA-256: [spec]

  1. The headers, from 0 to SizeOfHeaders, without CheckSum (4 bytes) and without the certificate table directory entry (8 bytes).
  2. Every section with SizeOfRawData > 0, in increasing PointerToRawData order, SizeOfRawData bytes from PointerToRawData.
  3. The data after them: from SizeOfHeaders + sum(SizeOfRawData) to the end of the file minus the certificate table.

Hashing the file linearly with the two holes gives the same result only when the sections follow each other without gaps; the steps above are the definition.

38.3 The CMS SignedData#

Windows' signer writes, and accepts: [observed]

ContentInfo { signedData (1.2.840.113549.1.7.2), [0] SignedData {
  version 1,
  digestAlgorithms SET { sha256 + NULL },
  encapContentInfo { SPC_INDIRECT_DATA (1.3.6.1.4.1.311.2.1.4),
                     [0] SpcIndirectDataContent }          -- the SEQUENCE itself, no OCTET STRING
  certificates [0] { the signer's certificate, then intermediates; no self-signed root },
  signerInfos SET { SignerInfo {
    version 1, IssuerAndSerialNumber of the signer,
    digestAlgorithm sha256 + NULL,
    signedAttrs [0] { SpcSpOpusInfo, contentType, SpcStatementType, messageDigest },
    signatureAlgorithm rsaEncryption + NULL,
    signature (RSA PKCS#1 v1.5) } } } }

38.4 MSI packages#

Microsoft does not publish how an MSI package is hashed. The rules below were worked out by signing packages with Windows' signer and comparing: rubrapack's signature and Ex streams came out byte-identical to Windows' for the same package and key, and packages whose directory entries were given non-zero state bits, times and CLSIDs told the fields apart. [observed]

38.5 Timestamps#

A signature without a timestamp stops being valid when the signing certificate expires. With an RFC 3161 timestamp, Windows checks the certificate at the stamped time instead. What mssign32!SignerTimeStampEx2 with SIGNER_TIMESTAMP_RFC3161 writes: [observed]

Set-AuthenticodeSignature -TimestampServer writes an older form instead: a PKCS#9 counterSignature attribute (1.2.840.113549.1.9.6) and the server's certificates added to the outer certificates. Windows accepts both. [observed]

38.6 ECDSA#

An ECDSA P-256 signer works for PE files, MSI packages and MSIX packages alike (Windows 11 26100 calls all three Valid with the root trusted). Windows' signer writes the SignerInfo's signatureAlgorithm as id-ecPublicKey (1.2.840.10045.2.1) with NULL parameters - not ecdsa-with-SHA256 - and the signature value as the DER SEQUENCE { INTEGER r, INTEGER s }. rubrapack writes the same, with deterministic nonces (RFC 6979), and reads either OID. [observed]

38.7 What Windows reports#

Get-AuthenticodeSignature distinguishes the cases a signer needs (and names the time-stamping certificate in TimeStamperCertificate when there is a timestamp): Valid (digest, signature and chain to a trusted root), UnknownError with "the certificate chain ... not trusted" (digest and signature are right, the root is not trusted on this machine), HashMismatch (the file changed after signing), NotSigned. So a signature can be checked for correctness before any root is trusted. [observed]

38.8 Worked example: the tutorial's hello.msi#

The digest of the tutorial's first package, computed here from the rules above without any key - a signature made with any key carries these same values. The streams in order of their UTF-16LE names:

#name (UTF-16LE)NameSize
105 00 53 00 75 00 ...\005SummaryInformation336
20b 43 31 41 35 47 ...Binary.RpCa99840
326 41 65 38 be 41 ...cab1.cab6776
440 48 0b 43 31 41 ...Binary4
540 48 0c 46 f6 45 ...CustomAction56
640 48 0d 43 35 42 ...Directory18
740 48 0f 42 e4 45 ...FeatureComponents4
840 48 0f 42 e4 45 ...Feature16
940 48 0f 43 2f 42File20
1040 48 16 42 27 43 ...Media14
1140 48 3f 3b f2 43 ..._Columns648
1240 48 3f 3f 77 45 ..._StringData3706
1340 48 3f 3f 77 45 ..._StringPool684
1440 48 52 44 f6 45 ...InstallExecuteSequence156
1540 48 52 44 f6 45 ...InstallUISequence42
1640 48 59 45 f2 44 ...Property88
1740 48 7f 3f 64 41 ..._Tables36
1840 48 8c 44 f0 44 ...Component12
1940 48 ca 41 30 43 ...AdminExecuteSequence48
2040 48 ca 41 30 43 ...AdminUISequence24
2140 48 ca 41 f9 45 ...AdvtExecuteSequence42
2240 48 de 44 6a 45 ...Upgrade32
2340 48 ff 3f e4 43 ..._Validation1944

The prehash is the root's CLSID and state bits (20 bytes) and, per stream, its name, 8-byte size and two zero times: 908 bytes in all. Its SHA-256, the \005MsiDigitalSignatureEx value:

be d7 c5 7b 1d aa 0f fd 10 b6 95 f5 e3 eb cc b7 11 91 28 12 d8 8a b2 85 76 29 5a 7a 94 ca 68 9d

The digest in SpcIndirectDataContent - SHA-256 over that value, every stream's bytes in the same order, and the root CLSID:

fe a3 de 0c f6 25 b2 1d ad fb c5 37 ea a5 05 30 49 88 e9 b9 80 8b 6d 1f e5 a0 11 c8 64 a0 06 91