My goal is to sign an unsigned executable file on Windows using a certificate. From my general knowledge I know that I need a public and a private key pair for a digital signature. I have also installed the Windows SDK, which provides signtool.exe and makecert.exe.
I have already obtained a certificate online, comprising a CER, a PEM, and a CRT file.
My question now is how I have to use these tools and the certificate files in order to sign an executable. According to here, the CRT file is the private key. From what I've learned so far, the CER and the PEM file are basically the same but with different encodings. What are the they for? Are they the public key? And how do I sign my executable?
EDIT: I've tried installing the CRT file to a certificate store and then signing using that certificate:
"C:\Program Files (x86)\Windows Kits\8.1\bin\x64\signtool.exe" sign /debug /fd SHA256 /a /n "<Issued_To>" /t http://timestamp.comodoca.com/authenticode <Filename>
Here <Issued_To> was replaced with the data from the certificate and <Filename> is the name of the file I wanted to sign. The output I get from signtool is the following:
The following certificates were considered:
...
Issued to: ...
Issued by: Certum Code Signing CA SHA2
Expires: Thu Oct 12 14:37:04 2017
SHA1 hash: BA081A67D3F2DDDC9268121DCBA04F43D6CD37FB
...
After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Subject Name filter, 1 certs were left.
After Private Key filter, 0 certs were left.
SignTool Error: No certificates were found that met all the given criteria.
For what it's worth, I purchased Certum Cloud Signing for open source projects and here is how I got it working. (It took me 5 business days of mails, trying and error and using google translate on polish documents, so maybe I save a little time to someone)
You first provide your personal information, Once you receive the activation token, you only have 24 hours to activate it. (even if this info is not specified, dont let time pass as I did), so:
Use the "Secret for regaining access to the SimplySign service" in the link that looks like: https://cloudsign.webnotarius.pl/arc/app/resetseed?token=...
There you will get a new code that you should use on the SimplySign mobile app (Reset button, I believe).
I wasnt able to find SimplySign mobile app on google play. So I used a desktop browser to find the App (which said "This app is not compatible with your device", and/or country...) and downloaded the APK using a chrome extension, then installed manually on my phone.
Once you have the mobile app setup, it should be generating 6 digit tokens every minute or so.
Now install SimplySign Desktop on Windows. Log in using your email and the 6 digits token from your mobile. Once it says "Status: Connected" it has installed a virtual smartcard and your certificate. SimplySign must stay connected for the certificate to work.
signtool.exe sign /n "Open Source Developer, Your Name" /fd SHA256 YourApp.exe
If you don't use /fd SHA256 you will get:
SignTool Error: SignedCode::Sign returned error: 0x80090027
The parameter is incorrect.
SignTool Error: An error occurred while attempting to sign: YourApp.exe
If you don't login into SimplySign, you get:
After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Subject Name filter, 1 certs were left.
After Private Key filter, 0 certs were left.
SignTool Error: No certificates were found that met all the given criteria.
Note: There is no need to install proCertum SmartSign app.
This solved my question: http://www.anse.de/programming/code-signing-for-open-source-executable
I exported the certificate as a PK2 file using Firefox. Then I installed this certificate in the "Personal" certificate store on Windows. Afterwards I could use the aforementioned command to sign my executable:
signtool sign /fd SHA256 /a /n "<Issued_To>" /t http://timestamp.comodoca.com/authenticode <Filename>
Here <Issued_To> matches the value in the certificate and <Filename> is the name of the file to be signed. Afterwards the executable file is signed.
Related
I'm trying to sign an executable using Microsoft's signtool.exe
The private key lives in an HSM and can not be taken out. So, i had to get the "public" part of the certificate in a .cer file and used below command to sign
signtool.exe sign /v /f .\SigningCert.pem /csp "HSM Key Storage Provider" /k "KEYID" /tr http://timestamp.digicert.com /fd sha256 /td sha256 .\App.exe
Since the machine i used for signing did not have the intermediate certificate added to Windows trust store, the signed executable could not be verified.
SignTool Error: WinVerifyTrust returned error: 0x800B010A
A certificate chain could not be built to a trusted root authority.
That being said, I used below command to add the intermediate certificate using /ac switch
signtool.exe sign /v /f .\SigningCert.pem /csp "HSM Key Storage Provider" /k "KEYID" /ac .\Intermediate.pem /tr http://timestamp.digicert.com /fd sha256 /td sha256 .\App.exe
The executable could be verified properly. So far so good.
Now, if my certificate chain has more than one intermediate certs, how could i add them to signatures?
Example chain:
Leaf Cert -> Intermediate 1 -> Intermediate 2 -> Root Cert
I tried merging all the intermediate certs into a single pem file and using it with /ac switch. Apparently, Signtool takes only the first certificate from the pem file and ignores the rest.
Also, If i want to add cross certificates from Microsoft apart from my intermediate certificates, how would i add them?
As far as i read, I can put all the certs into a pfx file and use it with signtool. But, as i mentioned, i don't have access to the private key. I do not think building a pfx file is an option for me.
I'm setting up a new development laptop, and have installed a self-issued code signing certificate. I can see it in certmgr under my Current Users's Personal Certificates.
When I try to build from the Developer Command Prompt For Visual Studio 2017 I get:
error : SignTool Error: No certificates were found that met all the given criteria.
This always worked fine on my old laptop.
I have found if I run the same build from the command prompt after starting it as admin that signtool succeeds and can find the cert.
This has happened to 3/4 colleagues when we've set up new laptops. One guy is ok and can sign without running as admin. On our old laptops we never had to run as admin.
I've tried googling to find what could be the cause because I wasn't aware that running as admin or not should have any affect over this. I haven't found any reference to this problem.
How can we use signtool.exe without running it as admin?
When not running as admin it appears to be at the Private Key filter step where the cert I'm expecting to be selected gets filtered out:
**********************************************************************
** Visual Studio 2017 Developer Command Prompt v15.9.12
** Copyright (c) 2017 Microsoft Corporation
**********************************************************************
C:\>signtool sign /v /debug /ph /i "<issuedby>" /fd sha256 /td sha256 "C:\TestSign.dll"
The following certificates were considered:
Issued to: Scott Langham
Issued by: <issuedby>
Expires: Sun Sep 25 09:54:55 2022
SHA1 hash: <a_hash>
Issued to: Scott Langham
Issued by: <issuedby_somethingelse>
Expires: Wed May 13 15:51:14 2020
SHA1 hash: <b_hash>
After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Issuer Name filter, 1 certs were left.
After Private Key filter, 0 certs were left.
SignTool Error: No certificates were found that met all the given criteria.
I've ensured the version of signtool.exe I'm using is the same as the one that my colleague who has this working is using (10.0.18362.1). I've been able to spot any other differences between our systems.
I ran into this today and here is how I am now able to run signtool.exe via command line without elevating to admin.
Run 'mmc' and add the 'Certificates' snap-in
Select the correct key store location
(mine is in Local Computer so I select 'Computer account' here)
Find and select the certificate
Right click on the certificate, select All Tasks > Manage Private Keys...
In the 'Permissions for private keys' dialog, Add your user account and then give yourself 'Full Control'. You will now be able to sign using a normal command prompt.
Note: If you use a build machine, do the above steps for the account that performs the builds.
Similar to what #Baget said, I'd compare the certificates on your machine to that of your colleague who can successfully run the SignTool.exe command without the elevation token. Here's a chunk of PowerShell to assist you:
get-childitem -Path Cert:\ | foreach-object ({
$location = $_.Location
foreach($store in $_.StoreNames.Keys) {
get-childitem -Path "Cert:\$location\$store" | foreach-object ({
$thumb = $($_.ThumbPrint)
$issuer = $($_.Issuer)
if ($issuer -eq "CN=EXAMPLE, DC=EXAMPLE, DC=EXAMPLE, DC=EXAMPLE") {
write-host "$location $store $issuer"
}
})
}
})
Bear in mind that the output of the above may differ slightly if you run as a normal user and 'run as admin'.
Finally, do you and your colleague have the same UAC settings?
I found myself in a similar situation with signtool, it refused to work with an admin user but does work as actual Administrator.
In my case, I am not actually importing the certificate into the certificate store, but using a .pkcs12 file exported from a comodo certificate in firefox, so this makes things even stranger, as no permissions on any keys in the store are involved.
I tried granting myself permissions on some server key, but that did nothing.
I hope someone finds a solution to this problem.
In the meantime, I am signing my exe with osslsigncode instead, which works perfectly.
I extracted the exe and dependent dlls from the msys2 mingw64 build, here is a zip of everything in case it is of use to anyone, just extract it to a directory and put it in your PATH.
http://cachemiss.com/files/osslsigncode.zip
To extract this program yourself from an msys2 installation, you can use this command:
pacman --noconfirm -S mingw-w64-x86_64-osslsigncode
cd /mingw64/bin
mkdir ~/osslsigncode
cp osslsigncode.exe $(ldd osslsigncode.exe | sed -n 's,^.*\(/mingw64/[^ ]*\).*,\1,p' | sort -u) ~/osslsigncode/
cd
zip -9r osslsigncode.zip osslsigncode
I am using this in Visual Studio cmake builds with no issue.
There are two Certificate Store in Windows, User Store, and a machine store, you probably installed the certificate to the local machine, or you installed it when you run as elevated user.
Firstly you are getting such an error and you are using visual studio then See the signing tab in project properties. You will see a previously assigned signature (strong assembly)
If you enter the correct password here:
If your environment is the same as the environment in which the project is written, you will skip this error. Look the post on about the error
The reasons behind this error are:
1- Visual Studio needs some features when using certificate file to sign strong assembly.
ClickOnce Manifest Signing and Strong-Name Assembly Signing Using Visual Studio Project Designer's Signing Page
You can try this code to determine
signtool sign /debug /f mypfxfile.pfx /p <password> (mydllexectuable).exe
2- These settings can vary from machine to machine, and windows does not write these settings to the environment setting by default. check the config path
This part is just a deep note! not interested with the question you should add your private keystore file to request header and those all for this. My favorite steps about the creation of a certificate on this post
How can I enable access to key in HSM when signing as sha256?
When I sign as sha1 sign tool properly pulls the cert key out of the HSM but if I change to "/fd sha256" the key can't be found within the container. I'm sure signtool can access the container, but some policy must be blocking this in the sha256 case.
My Error:
SignTool Error: The specified private key container was not found.
Sha256 command:
signtool.exe sign /f mycert.crt
/csp "Luna Cryptographic Services for Microsoft Windows"
/kc mycontainer /tr http://timestamp.digicert.com /td sha256
/fd sha256 signed-file.exe
Sha256 command, which works:
signtool.exe sign /f mycert.crt
/csp "Luna Cryptographic Services for Microsoft Windows"
/kc mycontainer /tr http://timestamp.digicert.com /td sha256
/fd sha1 signed-file.exe
/debug and /v options offer no additional information.
If I use makecert to generate a new self signed cert it the command generates a container which CSP can use for sha1 or sha256. Luna's CSP\keymap.exe tool allows me to manage containers and keys. I can create new ones for signing or exchange and then associate keys with them.
Generate Cert and upload kes to HSM
# Create Cert and store keys on HSM in a container called "noi1-501706key"
makecert -sk noi1-501706key -sp "Luna Cryptographic Services for Microsoft Windows" -r
-n "CN=noi1- 501706" -ss TestStore noi1-501706.cer
# make self signed
Cert2Spc noi1-501706.cer noi1-501706.spc
Use Program Files\Safenet\Luna Client\CSP\keymap
create new container
associate pub/private with new container
using new container sha1 works and sha256 fails. All attempts to view the two containers show them as identical.
Check the KeyContainer,public and private key objects labels (cmu.exe list or keymap.exe Browse Objects).Public and private key labels should be in following format:
Container name: ContainerName
Private key: S-ContainerName
Public key: S-ContainerName
Use cmu.exe setattribute to change the label.
According to the chapter 7 "Integrating Microsoft HCK (Windows Server 2012) with Luna HSM" I noticed the following which makes me think the creation of the CSR via an HSM tool on linux instead of makecert we are blocked from KSP/CNG.
So, we will never get Luna client to find the key for the cert. We need to either get a new cert or migrate to a new HSM and get a new cert. This is because AWS CloudHSM Classic uses hardware configured to block export of keys regardless of policy. No change to escrow the keys here.
In order to integrate the Luna SA Hardware Security Module with Microsoft HCK, the Luna CSP “Luna Cryptographic Services for Microsoft Windows” must be used to generate the certificate. The
certificate must be signed and the signer certificate must be present in the “Trusted Root Certificate Authority”. You can use the CA signed certificate of self-signed certificate both.
While trying to sign some installer created by the company I am working for I encountered an error, which I have not been able to solve. I am using the same certificate which has been used on another machine (Win7) successfully in the same way for signing quasi the same installer. Anyway, on our Windows Server 2008 which is running CruiseControl.net I tried to sign an installer with signtool.exe and it fails with the following error:
The following certificates were considered:
Issued to: <our company>
Issued by: <some ca>
Expires: <is valid>
SHA1 hash: <...>
Issued to: <...>
Issued by: <...>
Expires: <...>
SHA1 hash: <...>
After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Subject Name filter, 1 certs were left.
After Private Key filter, 0 certs were left.
SignTool Error: No certificates were found that met all the given criteria.
I tried installing the certificate to different certificate stores, tried different versions of signtool.exe and tried to use the .cer file directly, but it made no difference. I am receiving the error mentioned above in all of the cases. I tried the following command line commands
signtool.exe sign /debug /n "MyCompany" C:\my\installer.exe
signtool.exe sign /debug /f C:\path\to\my\certificate.cer C:\my\installer.exe
but I left the /debug away in some cases. Is there anything I am doing wrong or missing?
I had the same symptom, but an altogether different cause. As many developers do, I have a bunch of different tool chains installed on my system. I just surveyed them to show how this can look; scroll to the bottom of this answer for the full list.
I had installed my code-signing certificate from VeriSign to the system certificate store (requires /sm with signtool.exe) as usual, using certutil -importPFX cert.pfx from an elevated command prompt.
First tests looked promising, but then suddenly the signing started to fail.
To debug the issue I first started using signtool.exe sign /debug /v /a /sm ... in order to see what goes wrong. The output looked like this (also see question):
The following certificates were considered:
Issued to: localhost
Issued by: localhost
Expires: Tue Dec 26 00:00:00 2017
SHA1 hash: <...>
Issued to: <...>
Issued by: Symantec Class 3 SHA256 Code Signing CA
Expires: <...>
SHA1 hash: <...>
After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Root Name filter, 1 certs were left.
After Private Key filter, 0 certs were left.
SignTool Error: No certificates were found that met all the given criteria.
I could rule out the missing private key, as the certificate store indicated clearly I have a matching private key:
Now I remember that there were some recent patches that allow Windows 7 to accept signatures made with a certificate that has a SHA256 hash. Although, of course, most older articles will state that Windows 7 cannot handle SHA-2 hashes at all.
So this already gave me a nudge into the direction of "it's got to be an old version of something involved in signing".
I still decided to remove the certificate plus key and reimport it using the invocation shown before.
Then, after surveying my system (see at the bottom of the answer), I found a whopping five different versions of signtool.exe. So I started by trying the newest one (6.3.9600.17298, from the Windows 8.1 SDK) and it worked immediately:
signtool.exe sign /debug /v /a /sm /r VeriSign /ac MSCV-VSClass3.cer /ph /t "http://timestamp.verisign.com/scripts/timstamp.dll" *.exe
The following certificates were considered:
Issued to: localhost
Issued by: localhost
Expires: Tue Dec 26 00:00:00 2017
SHA1 hash: <...>
Issued to: <...>
Issued by: Symantec Class 3 SHA256 Code Signing CA
Expires: <...>
SHA1 hash: <...>
After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Root Name filter, 1 certs were left.
After Private Key filter, 1 certs were left.
The following certificate was selected:
Issued to: <...>
Issued by: Symantec Class 3 SHA256 Code Signing CA
Expires: <...>
SHA1 hash: <...>
Cross certificate chain (using machine store):
Issued to: Microsoft Code Verification Root
Issued by: Microsoft Code Verification Root
Expires: Sat Nov 01 13:54:03 2025
SHA1 hash: 8FBE4D070EF8AB1BCCAF2A9D5CCAE7282A2C66B3
Issued to: VeriSign Class 3 Public Primary Certification Authority - G5
Issued by: Microsoft Code Verification Root
Expires: Mon Feb 22 19:35:17 2021
SHA1 hash: 57534CCC33914C41F70E2CBB2103A1DB18817D8B
Issued to: Symantec Class 3 SHA256 Code Signing CA
Issued by: VeriSign Class 3 Public Primary Certification Authority - G5
Expires: Sat Dec 09 23:59:59 2023
SHA1 hash: 007790F6561DAD89B0BCD85585762495E358F8A5
Issued to: <...>
Issued by: Symantec Class 3 SHA256 Code Signing CA
Expires: <...>
SHA1 hash: <...>
The following additional certificates will be attached:
Issued to: VeriSign Class 3 Public Primary Certification Authority - G5
Issued by: Microsoft Code Verification Root
Expires: Mon Feb 22 19:35:17 2021
SHA1 hash: 57534CCC33914C41F70E2CBB2103A1DB18817D8B
Issued to: Symantec Class 3 SHA256 Code Signing CA
Issued by: VeriSign Class 3 Public Primary Certification Authority - G5
Expires: Sat Dec 09 23:59:59 2023
SHA1 hash: 007790F6561DAD89B0BCD85585762495E358F8A5
Done Adding Additional Store
Successfully signed: <...>.exe
Number of files successfully Signed: 1
Number of warnings: 0
Number of errors: 0
Tracking this down further I thought I had found the issue. However, it turned out that the error I got is not what I would have gotten to see with the older signtool.exe versions. Instead older versions would have complained about /ac, /fd and /ph being unrecognized command line options, respectively.
So I needed to dig a little deeper and it turned out that my (alternative) file manager was the culprit. I usually start my command prompts in the respective folder using that file manager and a handy keyboard shortcut. It turns out that it sometimes does not pass the environment variables - essentially the file manager "forgets" the environment variables. This turned out to be the root cause. A command prompt opened using Win+R and then cmd Enter would not expose this behavior despite executing signtool.exe from the same folder.
My best guess from this is that due to a messed up PATH variable or similar, signtool.exe ended up picking the wrong DLL. Notably mssign32.dll and wintrust.dll accompany signtool.exe in the same folder for the Windows SDK 8.0 and 8.1, but not for any of the earlier versions of signtool.exe which will pick the "global" system-wide DLLs, whatever they turn out to be.
On my system I had five different versions of signtool.exe.
signtool.exe 5.2.3790.1830
Doesn't even understand the /ac and /ph arguments I was using (also not /fd). But strangely enough worked without those two arguments.
C:\Program Files (x86)\Microsoft Visual Studio 8\SDK\v2.0\Bin\signtool.exe
signtool.exe 6.0.4002.0
Doesn't even understand the /ac and /ph arguments I was using (also not /fd). But strangely enough worked without those two arguments.
C:\Program Files (x86)\Microsoft Visual Studio 8\Common7\Tools\Bin\signtool.exe
signtool.exe 6.1.7600.16385
First version to understand /fd sha256.
C:\WINDDK\7600.16385.1\bin\amd64\SignTool.exe
C:\WINDDK\7600.16385.1\bin\x86\SignTool.exe
C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\signtool.exe
C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\signtool.exe
C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe
signtool.exe 6.2.9200.20789
C:\Program Files (x86)\Windows Kits\8.0\bin\x64\signtool.exe
C:\Program Files (x86)\Windows Kits\8.0\bin\x86\signtool.exe
signtool.exe 6.3.9600.17298
C:\Program Files (x86)\Windows Kits\8.1\bin\arm\signtool.exe
C:\Program Files (x86)\Windows Kits\8.1\bin\x64\signtool.exe
C:\Program Files (x86)\Windows Kits\8.1\bin\x86\signtool.exe
In order to sign a file you need to have the certificate's private key, which is not included in the *.cer file you copied from the Windows 7 machine. To export the certificate with its private key you can follow the instructions supplied here.
Do note that you'll only be able to export the private key if the certificate was set to allow exporting it when it was created (by passing -pe to makecert)
Just to be sure...
You need an special "code sign" certificate to be able to sign your code...
I have wasted a few hours by trying to sign our app with our domain certificate, what is not possible.
I had the same issue on Win7 machine and tried everything that good people suggested in this post with no luck. Then, even my machine has only one account and it has admin privileges, I opened command prompt window by "Run as administrator" and then all versions of signtool.exe that I have installed start working.
I had same problem as mentioned. This is what I did - assuming you have setup the rest of the pre-requisites for code signing.
Closed the solution and closed the VS IDE
Launched the Visual Studio IDE with Run as Administrator privileges
Open the solution and rebuild it
This time the signing process was successful.
I presume you have the right cert file and private key for signing and also imported the cert to the Trusted store.
If not, you can continue to read more.
Import the pfx file - "xyz.pfx" :: Open command prompt (launch - Run
as Administrator privileges) and run the following command
certutil.exe -p mypassword -importpfx xyz.pfx
An output from the command is shown below as example:
Certificate "CN=Name of the Cert, O=BLAH, OU=BLAH, E=john.doe#yourorg.com" added to
store.
CertUtil: -importPFX command completed successfully.
Add the certificate to the Trusted Root Certification Authorities by using the following command. There are many ways to do but I used this. Open command prompt (launch - Run as Administrator privileges) and run the following command
certmgr.exe -add -c mycertificate.cer -s -r localMachine root
You will see an output message like this
CertMgr Succeeded
Sign the assembly. Simple process from Visual Studio IDE.
Right click the Project in your solution and open its properties.
Open Signing section and click on Sign the Assembly check box (Enable it)
Choose the pfx file using Browse option and enter the password
you can also create a new one but you need a cert file that match to this pfx file, otherwise your signing will fail.
Save the changes
Now, Click on Build Events section and click Edit Post-Build to open the editor for post build events. Enter the following command.
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe" sign /v /sm "$(TargetPath)"
Path of the signtool.exe depends on where/whether you have installed the Windows SDK.
Now build the solution, your solution should build successfully and your binaries signed. Ensure to run the VS IDE with Administrator privileges.
I also had to sign a file, using a certificate which I received from another source (similar to you). For me, the issue was that I only installed the certificate on my PC with the "Current User" option. Once I installed it, using the "Local Machine" option, it worked.
How can i include the entire certification path when signing code using signtool?
Older versions of signtool would include the entire certification path in a digital signature. As it is now if i sign an executable with signtool:
signtool.exe" sign /v /f avatar.pfx -t "http://timestamp.verisign.com/scripts/timstamp.dll" app.exe
the signature is not valid:
This is because there is no certification path:
Binaries signed with the older version of signtool worked fine:
How do i tell signcode to include the entire certification path when signing?
What is the proper way to sign a binary?
Update: SignTool version 6.1.7600.16385:
See also
How can I sign an ActiveX control with a code signing certificate and be a verified publisher?
Signing WinForms ClickOnce app with Certificate Chain
ClickOnce: Certificate cannot be validated
Use /ac and pass the filename of the .cer in which your certificate is rooted (for Verisign it was called MSCV-VSClass3.cer last time I checked when signing kernel code or other special code).
signtool.exe sign /v /f "Avatar.pfx"
/ac "Thawte Code Signing CA - G2.cer"
-t "http://timestamp.verisign.com/scripts/timstamp.dll" app.exe
This should be given by your CA. Usually MS offers bundles for the various CAs it accepts within Windows.
See:
Windows root certificate program members 🕗
Cross-Certificates for Kernel Mode Code Signing 🕗
Either way, to my knowledge this is only required for kernel code and very specific other things (e.g. Windows Security Center).
If you use Thawte then download their primaryca.cer.
Download to file primaryca.cer and sign your file with:
signtool sign /f certificate.pfx /p PASSWORD /ac primaryca.cer APP.exe.
Should work.
The documentation for authenticode signing
Windows Authenticode Portable Executable Signature Format (.docx 🕗)
says that the PKCS #7 SignedData structure...
...contains the signer certificate and any intermediate certificates, but typically does not contain the root certificate.
However, as I discovered in a bit of a 'DOH!' moment, signtool.exe must be able to find the certificates to include them.
The leaf certificate is provided on the command line. But the identification of the remaining certificates up the chain does not include where to find the certificates. signtool does check the system certificate store, so if they are found there, they are added to the binary. If they are not found, signtool only puts the leaf certificate into the signed binary.
Note that if the intermediate certificates are not in the signed binary, but are in the system certificate store of the system checking the signature, the binary will still pass verification, because the chain can be resolved.
Also note that the exclusion of the root from the signed binary makes sense, given that the root must independently be on the system checking the signature for it to be trusted, so it would be ignored anyway. (The only real benefit to including the root in the binary would be if someone wanted to import the root cert manually, which is almost always a very bad idea.)