Parse openssl certificate date output into .NET DateTime objects

From Svendsen Tech PowerShell Wiki
Jump to: navigation, search

Here's what you need to, as of 2018-03-26, parse the Linux utility openssl's output about a certificate's expiration date (not after / notAfter) and "not before" date, and turn it into .NET DateTime objects in PowerShell.

I use my own SSHSessions module, in version 2.1.3, for access to a Linux server, in the examples. That module requires PowerShell version 3, but you can use the SSH-Sessions v2 module and then in the code use New-Object -TypeName PSObject -Property @{ ComputerName = $LinuxServer; ... } ... etc. instead of [PSCustomObject], to make it compatible with PowerShell version 2. It's time to move on from v2 in most situations, I guess.

There could also be openssl floating around for Windows somewhere, but for now I went with the "old one", on the Linux platform, and the output that tool produces.

Adapt the code as necessary if your needs are different. Possibly your output could be in another format, but my code should/could still be a good help to guide you on how to parse it into proper DateTime objects.



Example screenshot

Parse-openssl-certificate-date-output-into-.net-datetime-objects.png

Example openssl output about an expiring certificate

Here I connect to the Linux computer www.svendsentech.no and check the certificate for this site, www.powershelladmin.com, using a command I googled a while back (lost reference, sorry). It supports SNI as well with the -servername parameter (this was not in the original reference, I found out later). This can be used against load balancers too.

PS C:\temp> Import-Module SSHSessions
PS C:\temp> New-SshSession -ComputerName $LinuxServer -Credential $SSHCredentials

PS C:\temp> $SSHOutput = Invoke-SSHCommand -ComputerName $LinuxServer -Quiet `
    -Command "echo | openssl s_client -connect $($TargetCertDNS):443 -servername $($TargetCertDNS):443 2> /dev/null | openssl x509 -noout -dates"

PS C:\temp> $SSHOutput[0].Result
notBefore=Aug 30 01:01:00 2017 GMT
notAfter=Aug 30 01:01:00 2019 GMT

Then we need to parse that output to get nicely formatted .NET DateTime objects, which is not 100 % straight-forward, but I did the dirty work for us.

Complete code example

This is not a pre-written function or cmdlet, but code you can review and adapt yourself.

$LinuxServer = "www.svendsentech.no"
$TargetCertDNS = "www.powershelladmin.com"

#$SSHCredentials = Import-Clixml C:\Temp\testcreds.xml # protected by DPAPI...
#$SSHCredentials = Get-Credential root # set earlier

# Assuming SSHSessions module version above 1.9,
# otherwise don't index into the ".Result" property
# and there won't be an ".Error" property...
Import-Module SSHSessions -ErrorAction Stop
New-SshSession -ComputerName $LinuxServer -ErrorAction Stop -Credential $SSHCredentials

$SSHOutput = Invoke-SSHCommand -ComputerName $LinuxServer -Quiet `
    -Command "echo | openssl s_client -connect $($TargetCertDNS):443 -servername $($TargetCertDNS):443 2> /dev/null | openssl x509 -noout -dates"
if ($SSHOutput.Error) {
    # handle error
}
elseif ($SSHOutput[0].Result -match '^(?ms)\s*notBefore\s*=\s*(.+)notAfter\s*=\s*(.+)$') {
    Write-Verbose -Verbose "Regex matched."
    Write-Verbose -Verbose "`$Matches[1] (notBefore) is: $($Matches[1].TrimEnd())"
    Write-Verbose -Verbose "`$Matches[2] (notAfter) is:  $($Matches[2].TrimEnd())"
    $NotAfter, $NotBefore = "Unset", "Unset"
    $ErrorActionPreference = "Stop"
    try {
        $NotAfter = [DateTime]::ParseExact(
            [regex]::Replace(
                ($Matches[2] -replace '\s*GMT\s*$' -replace '(.+)\s+([\d:]+)\s+(\d{4})', '$1 $3 $2'), '(\w+)\s+(\d?\d)\s+(.+)', {
                    $args[0].Groups[1].Value + " " + ("{0:D2}" -f [int] $args[0].Groups[2].Value) + " " + $args[0].Groups[3].Value
                }
            ), 'MMM dd yyyy HH:mm:ss', [CultureInfo]::InvariantCulture)
    }
    catch {
        $NotAfter = "Parse error"
    }
    try {
        $NotBefore = [DateTime]::ParseExact(
            [regex]::Replace(
                ($Matches[1] -replace '\s+GMT\s*' -replace '^(.+)\s+([\d:]+)\s+(\d{4})$', '$1 $3 $2'), '(\w+)\s+(\d?\d)\s+(.+)', {
                    $args[0].Groups[1].Value + " " + ("{0:D2}" -f [int] $args[0].Groups[2].Value) + " " + $args[0].Groups[3].Value
                }
            ), 'MMM dd yyyy HH:mm:ss', [CultureInfo]::InvariantCulture)
    }
    catch {
            $NotBefore = "Parse error"
    }
    $ErrorActionPreference = "Continue"
}
else {
    $NotAfter = "Parse error (no match)"
    $NotBefore = "Parse error (no match)"
}

[PSCustomObject] @{
    ComputerName = $LinuxServer
    TargetCertificateDNS = $TargetCertDNS
    NotAfter = $NotAfter
    NotBefore = $NotBefore
}

How many days until the certificate expires?

Now let's find out how many days are left until this certificate expires. This could be used to trigger warnings, etc.

PS C:\temp> $CertTemp = .\openssl-cert-temp.ps1
VERBOSE: Regex matched.
VERBOSE: $Matches[1] (notBefore) is: Aug 30 01:01:00 2017 GMT
VERBOSE: $Matches[2] (notAfter) is:  Aug 30 01:01:00 2019 GMT

PS C:\temp> $DaysLeft = [Math]::Round(($CertTemp.NotAfter - (Get-Date)).TotalDays, 1)

PS C:\temp> $DaysLeft
521.1

And we see that there are now 521.1 days left until the certificate expires. A negative number means the certificate has already expired.