powershellcertificatex509certificatex509certificate2powershell-7.3

How to get the full certificate path/chain of this file in PowerShell


I have a signed file that for some reason can't get its root certificate in PowerShell using the code below

$FilePath = '.\NordPassSetup_x86.exe'

# Get the certificate from the file path
$Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $FilePath

# Build the certificate chain
$Chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
[void]$Chain.Build($Cert)

$Chain.ChainElements.count

foreach ($Element in $Chain.ChainElements) {
    $Element.Certificate | ft -AutoSize
}

Uploaded the file here: https://ufile.io/1j5pleow

The output is 3 items instead of 4 items. The file has 1 leaf, 1 root and 2 intermediate certificates.

I've tried skipping check for root cert and setting the check to offline but didn't help

[System.Security.Cryptography.X509Certificates.X509RevocationMode]::Offline

[System.Security.Cryptography.X509Certificates.X509RevocationFlag]::ExcludeRoot

The highlighted certificate doesn't show up in command line

enter image description here


Solution

  • I finally figured out a way to do this and it works beautifully. Leaf certificate, Root certificate, Intermediate certificate(s) and nested certificates are all detected and processed. The code is part of my module and available here:

    https://github.com/HotCakeX/Harden-Windows-Security/blob/main/WDACConfig/Invoke-WDACSimulation.psm1

    First I made this function to get the certificate collection of the signed file

    function Get-SignedFileCertificates {
        param (
            # Define two sets of parameters, one for the FilePath and one for the CertObject
            [Parameter()]
            [string]$FilePath,
            [Parameter(ValueFromPipeline = $true)]       
            [System.Security.Cryptography.X509Certificates.X509Certificate2]$X509Certificate2
        )
    
        # Create an X509Certificate2Collection object
        $CertCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
    
        # Check which parameter set is used
        if ($FilePath) {
            # If the FilePath parameter is used, import all the certificates from the file
            $CertCollection.Import($FilePath, $null, 'DefaultKeySet')
        }
        elseif ($X509Certificate2) {
            # If the CertObject parameter is used, add the certificate object to the collection
            $CertCollection.Add($X509Certificate2)
        }
    
        # Return the collection
        return $CertCollection
    }
    

    Then I modified one of my previous functions accordingly to handle the new type of data

    function Get-CertificateDetails {
        param (
            [Parameter(ParameterSetName = 'Based on File Path', Mandatory = $true)]
            [System.String]$FilePath,
    
            [Parameter(ParameterSetName = 'Based on Certificate', Mandatory = $true)]
            $X509Certificate2,    
    
            [Parameter(ParameterSetName = 'Based on Certificate')]    
            [System.String]$LeafCNOfTheNestedCertificate, # This is used only for when -X509Certificate2 parameter is used, so that we can filter out the Leaf certificate and only get the Intermediate certificates at the end of this function     
            
            [Parameter(ParameterSetName = 'Based on File Path')]
            [Parameter(ParameterSetName = 'Based on Certificate')]
            [switch]$IntermediateOnly,
    
            [Parameter(ParameterSetName = 'Based on File Path')]
            [Parameter(ParameterSetName = 'Based on Certificate')]
            [switch]$LeafCertificate
        )
    
        # An array to hold objects
        [System.Object[]]$Obj = @()
    
        if ($FilePath) {
            # Get all the certificates from the file path using the Get-SignedFileCertificates function
            $CertCollection = Get-SignedFileCertificates -FilePath $FilePath | Where-Object { $_.EnhancedKeyUsageList.FriendlyName -ne 'Time Stamping' }
        }
        else {
            # The "| Where-Object {$_ -ne 0}" part is used to filter the output coming from Get-AuthenticodeSignatureEx function that gets nested certificate
            $CertCollection = Get-SignedFileCertificates -X509Certificate2 $X509Certificate2 | Where-Object { $_.EnhancedKeyUsageList.FriendlyName -ne 'Time Stamping' } | Where-Object { $_ -ne 0 }
        }
    
        # Loop through each certificate in the collection and call this function recursively with the certificate object as an input
        foreach ($Cert in $CertCollection) {
                          
            # Build the certificate chain
            $Chain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain
    
            # Set the chain policy properties
            $chain.ChainPolicy.RevocationMode = 'NoCheck'
            $chain.ChainPolicy.RevocationFlag = 'EndCertificateOnly'
            $chain.ChainPolicy.VerificationFlags = 'NoFlag'
    
            [void]$Chain.Build($Cert)      
            
            # If AllCertificates is present, loop through all chain elements and display all certificates
            foreach ($Element in $Chain.ChainElements) {
                # Create a custom object with the certificate properties
    
                # Extract the data after CN= in the subject and issuer properties
                # When a common name contains a comma ',' then it will automatically be wrapped around double quotes. E.g., "Skylum Software USA, Inc."
                # The methods below are conditional regex. Different patterns are used based on the availability of at least one double quote in the CN field, indicating that it had comma in it so it had been enclosed with double quotes by system
    
                $Element.Certificate.Subject -match 'CN=(?<InitialRegexTest2>.*?),.*' | Out-Null
                $SubjectCN = $matches['InitialRegexTest2'] -like '*"*' ? ($Element.Certificate.Subject -split 'CN="(.+?)"')[1] : $matches['InitialRegexTest2']
                
                $Element.Certificate.Issuer -match 'CN=(?<InitialRegexTest3>.*?),.*' | Out-Null
                $IssuerCN = $matches['InitialRegexTest3'] -like '*"*' ? ($Element.Certificate.Issuer -split 'CN="(.+?)"')[1] : $matches['InitialRegexTest3']
                
                # Get the TBS value of the certificate using the Get-TBSCertificate function
                $TbsValue = Get-TBSCertificate -cert $Element.Certificate
                # Create a custom object with the extracted properties and the TBS value
                $Obj += [pscustomobject]@{
                    SubjectCN = $SubjectCN
                    IssuerCN  = $IssuerCN
                    NotAfter  = $element.Certificate.NotAfter
                    TBSValue  = $TbsValue                
                }           
            }  
        }
    
        if ($FilePath) {
    
            # The reason the commented code below is not used is because some files such as C:\Windows\System32\xcopy.exe or d3dcompiler_47.dll that are signed by Microsoft report a different Leaf certificate common name when queried using Get-AuthenticodeSignature
            # (Get-AuthenticodeSignature -FilePath $FilePath).SignerCertificate.Subject -match 'CN=(?<InitialRegexTest4>.*?),.*' | Out-Null
    
            $CertificateUsingAlternativeMethod = [System.Security.Cryptography.X509Certificates.X509Certificate]::CreateFromSignedFile($FilePath)
            $CertificateUsingAlternativeMethod.Subject -match 'CN=(?<InitialRegexTest4>.*?),.*' | Out-Null
    
            
            [string]$TestAgainst = $matches['InitialRegexTest4'] -like '*"*' ? ((Get-AuthenticodeSignature -FilePath $FilePath).SignerCertificate.Subject -split 'CN="(.+?)"')[1] : $matches['InitialRegexTest4']
             
    
            if ($IntermediateOnly) {
    
                $FinalObj = $Obj | 
                Where-Object { $_.SubjectCN -ne $_.IssuerCN } | # To omit Root certificate from the result
                Where-Object { $_.SubjectCN -ne $TestAgainst } | # To omit the Leaf certificate
                Group-Object -Property TBSValue | ForEach-Object { $_.Group[0] } # To make sure the output values are unique based on TBSValue property
    
                return $FinalObj
    
            }
            elseif ($LeafCertificate) {
        
                $FinalObj = $Obj | 
                Where-Object { $_.SubjectCN -ne $_.IssuerCN } | # To omit Root certificate from the result
                Where-Object { $_.SubjectCN -eq $TestAgainst } | # To get the Leaf certificate
                Group-Object -Property TBSValue | ForEach-Object { $_.Group[0] } # To make sure the output values are unique based on TBSValue property
    
                return $FinalObj
            }
    
        } 
        # If nested certificate is being processed and X509Certificate2 object is passed
        elseif ($X509Certificate2) {
        
            if ($IntermediateOnly) {
    
                $FinalObj = $Obj | 
                Where-Object { $_.SubjectCN -ne $_.IssuerCN } | # To omit Root certificate from the result            
                Where-Object { $_.SubjectCN -ne $LeafCNOfTheNestedCertificate } | # To omit the Leaf certificate
                Group-Object -Property TBSValue | ForEach-Object { $_.Group[0] } # To make sure the output values are unique based on TBSValue property
    
                return $FinalObj
    
            }  
            elseif ($LeafCertificate) {
    
                $FinalObj = $Obj | 
                Where-Object { $_.SubjectCN -ne $_.IssuerCN } | # To omit Root certificate from the result
                Where-Object { $_.SubjectCN -eq $LeafCNOfTheNestedCertificate } | # To get the Leaf certificate
                Group-Object -Property TBSValue | ForEach-Object { $_.Group[0] } # To make sure the output values are unique based on TBSValue property
    
                return $FinalObj
    
            }        
        }
    }