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

558 Views Asked by At

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

2

There are 2 best solutions below

0
SpyNet On BEST ANSWER

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

        }        
    }
}
6
Crypt32 On

This seems to be expected and by design.

Certificate chaining engine in signature validation context uses certificates stored in signature as much as possible. It will look into external stores only when matching certificate is not found.

What happens here: both chains share same intermediate CA certificate (GlobalSign Extended Validation CodeSigning CA - SHA256 - G3). Then, signature contains another intermediate CA certificate (GlobalSign) which is the issuer of EV code signing CA, so signature validator uses it. And the issuer is GlobalSign Root CA R1. This effectively produces a chain of 4 elements (from root to leaf):

  • GlobalSign Root CA - R1
  • GlobalSign
  • GlobalSign Extended Validation CodeSigning CA - SHA256 - G3
  • TEFINCOM S.A.

The presence of 2nd certificate in signature forces the signature validator to use it, thus extending the chain by one element.

The X509Chain doesn't stick to this requirement and builds as many chains as possible and then select the best one, which is then returned to the caller.

Underneath, X509Chain will produce these two chains, one that ends up with GlobalSign Root CA - R1 and another one that ends up with GlobalSign Root CA - R3. Both chains are valid, but 2nd chain is shorter (better) and certificate chaining engine returns it. Both, GlobalSign Root CA - R1 and GlobalSign Root CA - R3 share the same public key, thus both can be used to validate the chain.

If that extra GlobalSign certificate would absent in signature, then both, signature UI viewer and X509Chain will return identical chains.