How to create Certificate Trust List (CTL) using PowerShell
In this post, I will explain how to create custom certificate trust list (CTL) using PowerShell PKI (PSPKI) module.
What is CTL?
In short, CTL is a Microsoft open format of portable certificate container based on PKCS#7 format. Although, PKCS#7 already is a simple container for certificate, CTL provides several useful features:
- Name each list for better discoverability;
- Provide versioning;
- Add X.509 attributes to each certificate;
- Set custom validity for entire list;
- Restrict usages (purposes) for all certificates in list;
- Digitally sign the list.
Using CTLs, you can logically group and distribute collections of certificates. Although, CTL is *trust* list, CTL can store arbitrary certificates, root, intermediate and cross certificates. Depending on store where CTL is installed, certificates in CTL are trusted or distrusted by operating system or application.
Uses of CTL
As said, CTL is often used to logically group multiple certificates for specified purposes and trust/distrust for specified duration. Instead of managing multiple separate certificates, you manage only single container. Microsoft uses CTL to distribute a list of trusted root certificates which are members of Microsoft Root Program, explicitly distrusted (compromised) certificates:
Create new list
Sysadmins.PKI.dll library (part of PSPKI module) provides a set of APIs to generate arbitrary CTL. X509CertificateTrustListBuilder is a base class to build CTL. Use available properties to configure the list. Actual certificates are added to Entries collection in a form of X509CertificateTrustListEntry objects. Here is an example script that creates a trust list which is valid for Server and Client Authentication purposes, and valid for 5 years:
Import-Module PSPKI # instantiate trust list using default constructor $builder = New-Object SysadminsLV.PKI.Cryptography.X509Certificates.X509CertificateTrustListBuilder # provide version and name. Both are optional $builder.SequenceNumber = 5 $builder.ListIdentifier = "My custom trust list" # restrict list to client and server authentication [void]$builder.SubjectUsages.Add("1.3.6.1.5.5.7.3.1") # server authentication [void]$builder.SubjectUsages.Add("1.3.6.1.5.5.7.3.2") # client authentication # set validity for 5 years: $builder.NextUpdate = [datetime]::Now.AddYears(5) # add certificate entries in the list # for this example purposes we will add 10 certificates from CA store: $certs = Get-ChildItem cert:\currentuser\ca | select -First 10 # convert X509Certificate2 objects to X509CertificateTrustListEntry $certs | ForEach-Object { # use SHA1 to reference certificate in the list $entry = New-Object SysadminsLV.PKI.Cryptography.X509Certificates.X509CertificateTrustListEntry $_, "sha1" $builder.Entries.Add($entry) }
if we run this piece of code, we will get this in $builder variable:
PS C:\> $builder ListIdentifier : My custom trust list SequenceNumber : 5 SubjectUsages : {Server Authentication, Client Authentication} Entries : {E12D2E8D47B64F469F518802DFBD99C0D86D3C6A, C7ED7BF076120309F682577FE7B29A7593E9889C, AD898AC73DF333EB6 0AC1F5FC6C4B2219DDB79B7, 94C95DA1E850BD85209A4A2AF3E1FB1604F9BB66...} HashAlgorithm : System.Security.Cryptography.Oid ThisUpdate : 09.01.2020 14:27:45 NextUpdate : 09.01.2025 16:27:45 PS C:\>
Pretty simple! We can check created entries:
PS C:\> $builder.Entries Thumbprint Attributes Certificate ---------- ---------- ----------- E12D2E8D47B64F469F518802DFBD99C0D86D3C6A {} [Subject]... C7ED7BF076120309F682577FE7B29A7593E9889C {} [Subject]... AD898AC73DF333EB60AC1F5FC6C4B2219DDB79B7 {} [Subject]... 94C95DA1E850BD85209A4A2AF3E1FB1604F9BB66 {} [Subject]... 93E067E2FC976D8A53AA20F1A4E818D0F52779B4 {} [Subject]... 92C1588E85AF2201CE7915E8538B492F605B80C6 {} [Subject]... 8BFE3107712B3C886B1C96AAEC89984914DC9B6B {} [Subject]... 83DA05A9886F7658BE73ACF0A4930C0F99B92F01 {} [Subject]... 67090E77B40C7C40C1AB7733036B2EE4BCD178F0 {} [Subject]... 56EE7C270683162D83BAEACC790E22471ADAABE8 {} [Subject]... PS C:\> $builder.Entries[0] | fl * Thumbprint : E12D2E8D47B64F469F518802DFBD99C0D86D3C6A Attributes : {} Certificate : [Subject] CN=DigiCert SHA2 Assured ID CA, OU=www.digicert.com, O=DigiCert Inc, C=US [Issuer] CN=DigiCert Assured ID Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US [Serial Number] 04AE79606666901AB9C57FA66C5BDCCD [Not Before] 05.11.2013 14:00:00 [Not After] 05.11.2028 14:00:00 [Thumbprint] E12D2E8D47B64F469F518802DFBD99C0D86D3C6A PS C:\>
CTL Signing
In order to allow Windows to trust the list signature, it must be signed using a certificate that is intended for “Microsoft Trust List Signing” (1.3.6.1.4.1.311.10.3.1) purpose. Now, we will sign the CTL using certificate:
# in my case, certificate with Thumbprint=40BE51BF3FCE811ADC714D2AEBD86A85A5EBDF24 is CTL signing cert $cert = Get-Item Cert:\CurrentUser\My\40BE51BF3FCE811ADC714D2AEBD86A85A5EBDF24 # construct signer object from certificate and default hash algorithm $signer = New-Object SysadminsLV.PKI.Tools.MessageOperations.MessageSigner $cert # sign CTL # last parameter is an optional collection of signing certificate chain $ctl = $builder.Sign($signer,$null)
And voilà:
PS C:\> $ctl Version : 1 SubjectUsage : {Server Authentication, Client Authentication} ListIdentifier : My custom trust list SequenceNumber : 05 ThisUpdate : 09.01.2020 16:27:45 NextUpdate : 09.01.2025 16:27:45 SubjectAlgorithm : System.Security.Cryptography.Oid Entries : {E12D2E8D47B64F469F518802DFBD99C0D86D3C6A, C7ED7BF076120309F682577FE7B29A7593E9889C, AD898AC73DF333E B60AC1F5FC6C4B2219DDB79B7, 94C95DA1E850BD85209A4A2AF3E1FB1604F9BB66...} Extensions : {} RawData : {48, 130, 57, 30...} PS C:\>
if we call $ctl.ShowUI()
, we will see CTL object in Windows UI:
One more step is left. Last image shows that CTL is signed and not time-stamped. This means, that CTL will expire as soon as expires signing certificate. So, we add timestamp to signature:
$ctl.Addtimestamp("", "sha256")
call $ctl.ShowUI()
again to show changes in signature:
now, CTL is signed and timestampted and won’t expire after signing certificate expiration.
Note: you can timestamp only signed CTL.
Edit existing CTL
Another neat feature in CTL builder is that you can edit existing CTL object. In order to do this, use the following builder constructor:
PS C:\> $builder2 = New-Object SysadminsLV.PKI.Cryptography.X509Certificates.X509CertificateTrustListBuilder $ctl PS C:\> $builder2 ListIdentifier : My custom trust list SequenceNumber : 5 SubjectUsages : {Server Authentication, Client Authentication} Entries : {E12D2E8D47B64F469F518802DFBD99C0D86D3C6A, C7ED7BF076120309F682577FE7B29A7593E9889C, AD898AC73DF333EB6 0AC1F5FC6C4B2219DDB79B7, 94C95DA1E850BD85209A4A2AF3E1FB1604F9BB66...} HashAlgorithm : System.Security.Cryptography.Oid ThisUpdate : 09.01.2020 14:27:45 NextUpdate : 09.01.2025 16:27:45 PS C:\>
When you pass existing CTL to builder, all data (except signature) is copied from existing CTL to builder. Modify CTL contents as necessary and sign it again.
Final words
As you see, CTL building was never that simple in PowerShell. Given that APIs are built using .NET, same behavior is used in any other .NET language (C#/VB.NET/others).
Great entry Vadim. How does Windows behave when an entry of CTL has no matching certificate in a store? If I would like to manually switch between PKI hierarchies using CTL should I deploy the CTL as well as certificates files?
yes, you should include certificates in CTLs. It is always a recommended practice.