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:

image image

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:

image image image

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:

image

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).

Leave a Comment





This site uses Akismet to reduce spam. Learn how your comment data is processed.