This article was originally posted to my old WordPress blog. The content is still relevant but some details may have changed.

Today’s script is an attempt to bring together several things I have learned about writing good PowerShell scripts. I still have a lot to learn and this is not necessarily a sterling example of best practices. However, it does illustrate some more advanced scripting topics, including:

  • Comment-based help
  • Parameter sets
  • Parameters passing on the pipeline
  • Error handling with try-catch blocks
  • Simple HTTP downloads using System.Net.Webclient
  • Inspecting certificates imported from external sources (http or file)
  • Inspecting certificates in the local store

I rarely use comment-based help in my scripts since I am usually writing scripts for my own use. They tend to be one-off utilities designed to fulfill an immediate need. This script, however, is going to be used by other support technicians outside of my immediate team. So documentation was important. Comment-based help allows you to include documentation in the script (rather than a separate file that can get lost or out of date). And it gives help in a format that users expect for any other PowerShell command.

Parameter handling in PowerShell is extremely versatile. Through the advanced parameter options, you can create parameter sets, specify which parameters are mandatory, perform data validation, define input from the pipeline, and much more. All of this controlled via parameter definition. No need to write code to validate parameters or ensure valid parameter combinations. PowerShell does the heavy lifting for you.

My focus will be on the certificate management portions of the script and to outline the scenario that this script is attempting to support.

The Scenario

We have a set of devices that require a device-specific certificate to be installed. We have a scripted process for creating, publishing, and installing these certificates. The certificates are created in bulk for a large number of devices. These certificates are then exported to PFX files copied to a folder shared by a web server. The device can then download the PFX file and import it into local certificate store on the device. The devices and the certificates have a standardized naming scheme (e.g. DEV###). This makes it easy to identify which certificate belongs to which device.

The certificate lifecycle an unmanaged process. There is no policy mechanism to ensure that the device has installed the proper certificate or that the certificate installed is correct and valid. Occasionally we can have problems where the installed certificate is not working properly or the PFX file published to the web server does not match the certificated issued by the CA. To troubleshoot these issues we need to be able to verify the certificates on the device in PFX files published on the web server.

The solution

This script looks for certificates in one of three locations: the certificate broker (web server), the local certificate store, or PFX files stored in the file system. In all cases, the output is the same for each certificate found. The script displays some basic information about the certificate and then checks that each certificate in the validity chain is still valid.

Example 1 - check the published PFX file for a device

This was the first scenario I needed to solve for. The script takes the specified device name and attempts to download the matching PFX file from the certificate broker.

PS C:\> .\check-devicecert.ps1 -devices DEV101

You can specify one or more device IDs as an array of strings.

Example 2 - search for the device certificate in the local store

The script takes the specified device name and searches for a certificate with a matching Subject name in the local certificate store.

PS C:\> .\check-devicecert.ps1 -devices DEV101 -local

You can specify one or more device IDs as an array of strings.

Example 3 - load a PFX file from disk

The script loads the specified PFX file(s) from disk.

PS C:\> .\check-devicecert.ps1 -pfxfilename .\DEV113.pfx
PS C:\> dir *.pfx | .\check-devicecert.ps1

You can specify one or more PFX filenames as an array of strings. You can also pass an array of files on the pipeline.

For examples #1 and #3 we are working with PFX files. The first step is to obtain the contents of the PFX file as an array of bytes so that we can create an X.509 certificate object. To download the PFX file from the certificate broker we do the following:

$client = New-Object -TypeName System.Net.WebClient
$url = "https://certbroker.contoso.com/pfxshare/$($num).pfx"
$pfxBytes = $client.DownloadData($url)

The DownloadData() method of System.Net.WebClient does this nicely for us.

To load a PFX file from disk I use the Get-Content cmdlet and specify that I want Byte encoding.

$pfxBytes = Get-Content -path $file -encoding Byte -ErrorAction:SilentlyContinue
if ($error.count -ne 0) {throw $error}

Also, note the ErrorAction parameter. For some reason, exceptions occurring inside of Get-Content were not being caught by my Try-Catch block. I had to override the ErrorAction to force Get-Content to continue silently, check to see if an error occurred, then re-throw the exception so that it would get caught by my Try/Catch block.

Once I had the Byte array containing the PFX-formatted data blob I needed to import it into an X.509 certificate object.

function import-pfxbytes {
  param($pfxBytes)
  ## Import cert into a new object. No need to import it into a certificate device.
  $pfxPass = 'pFxP@$5w0rd'
  $X509Cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
  $X509Cert.Import([byte[]]$pfxBytes, $pfxPass,"Exportable,PersistKeySet")
  return $X509Cert
}

The import-pfxbytes function creates an empty X.509 certificate and imports the data using a static password and returns a certificate object. In this case, I have hard-coded the password. For better security, you should prompt the user to enter a password (for example, using Read-Host -AsSecureString).

For example #2 I am using PowerShell’s built-in provider to access the local certificate store. With this access method, you receive a certificate object, not a PFX-formatted data blob. Once I have an X.509 certificate object I pass it to show-certinfo to inspect the important properties and verify the validity of the trust chain.

# title="Check-DeviceCert.ps1"]
<#
.SYNOPSIS
Checks a device certificate for validity.

.DESCRIPTION
The script downloads a device certificate PFX file from the cert broker or reads an existing PFX file then checks for the validity.

.PARAMETER devices
An array of device numbers .PARAMETER local Indicates that you want to search the local certificate device.

.PARAMETER pfxfiles
An array of pathnames to PFX files deviced on disk. .INPUTS You can provide an array of PFX file names in the pipeline.

.EXAMPLE
PS C:\> .\check-devicecert.ps1 -devices DEV101
    ==================================================
    Downloading DEV101.pfx

    Subject      : CN=DEV101@contoso.com
    Issuer       : CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    NotBefore    : 5/2/2013 12:39:58 PM
    NotAfter     : 5/1/2017 12:39:58 PM
    SerialNumber : 27DC85E200060000B6D2

    Validating certficate chain...

    Valid   Certificate
    -----   -----------
    True    CN=DEV101@contoso.com
    True    CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    True    CN=Contoso Corporate Root CA, O=CONTOSO
    ==================================================

The example above illustrates downloading the PFX file from the certificate broker and check the validity.
.EXAMPLE
PS C:\> .\check-devicecert.ps1 -devices DEV369,DEV123
    ==================================================
    Downloading DEV369.pfx

    Subject      : CN=DEV369@contoso.com
    Issuer       : CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    NotBefore    : 5/2/2013 3:37:14 PM
    NotAfter     : 5/1/2017 3:37:14 PM
    SerialNumber : 287ED09B00060000CD63

    Validating certficate chain...

    Valid   Certificate
    -----   -----------
    True    CN=DEV369@contoso.com
    True    CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    True    CN=Contoso Corporate Root CA, O=CONTOSO
    ==================================================
    Downloading DEV123.pfx
    Error downloading S123456 - The remote server returned an error: (404) Not Found.

The example above illustrates downloading multiple PFX files from the certificate broker and check their validity.
.EXAMPLE
PS C:\temp> .\check-devicecert.ps1 -devices DEV101 -local
    ==================================================
    Reading Cert:LocalMachine\My\584C772D4E9EAA9F5858742B2AE4F3E9A0D602C7

    Subject      : CN=DEV101@contoso.com
    Issuer       : CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    NotBefore    : 5/2/2013 12:39:58 PM
    NotAfter     : 5/1/2017 12:39:58 PM
    SerialNumber : 27DC85E200060000B6D2

    Validating certficate chain...

    Valid   Certificate
    -----   -----------
    True    CN=DEV101@contoso.com
    True    CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    True    CN=Contoso Corporate Root CA, O=CONTOSO
    ==================================================

The example above searches for a certificate in the local certificate device and test the validity.
.EXAMPLE
PS C:\temp> .\check-devicecert.ps1 -pfxfilename .\DEV113.pfx
    ==================================================
    Reading .\S10113.pfx

    Subject      : CN=DEV113@contoso.com
    Issuer       : CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    NotBefore    : 5/2/2013 3:30:35 PM
    NotAfter     : 5/1/2017 3:30:35 PM
    SerialNumber : 2878BAEA00060000CCAD

    Validating certficate chain...

    Valid   Certificate
    -----   -----------
    True    CN=DEV113@contoso.com
    True    CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    True    CN=Contoso Corporate Root CA, O=CONTOSO
    ==================================================

The example above checks the validity of an existing, locally-deviced PFX file.
.EXAMPLE
PS C:\temp> dir *.pfx | .\check-devicecert.ps1
    ==================================================
    Reading C:\temp\DEV113.pfx

    Subject      : CN=DEV113@contoso.com
    Issuer       : CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    NotBefore    : 5/2/2013 3:30:35 PM
    NotAfter     : 5/1/2017 3:30:35 PM
    SerialNumber : 2878BAEA00060000CCAD

    Validating certficate chain...

    Valid   Certificate
    -----   -----------
    True    CN=DEV113@contoso.com
    True    CN=Contoso Corporate Enterprise CA 02, DC=contoso, DC=com
    True    CN=Contoso Corporate Root CA, O=CONTOSO
    ==================================================

The example above checks the validity of all the PFX files deviced in folder.
#>

[CmdletBinding(DefaultParametersetName="devices")]
param (
       [parameter(ParameterSetName="names",Position=0,Mandatory=$true,
        ValueFromPipeline=$false,HelpMessage="Enter device Number, Ex S12345")]
       [string[]]$devices,
       [parameter(ParameterSetName="names",Position=1,Mandatory=$false,
        ValueFromPipeline=$false,HelpMessage="Look for certificate in local device.")]
       [switch]$local,
       [parameter(ParameterSetName="files",Position=0,Mandatory=$true,
        ValueFromPipeline=$true,HelpMessage="Enter PFX file name, Ex C:\folder\DEV123.pfx")]
       [string[]]$pfxfiles
      )

function import-pfxbytes {
   param($pfxBytes)
   ## Import cert into a new object. No need to import it into a certificate device.
   $pfxPass = 'pFxP@$5w0rd'
   $X509Cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
   $X509Cert.Import([byte[]]$pfxBytes, $pfxPass,"Exportable,PersistKeySet")
   return $X509Cert
}
function show-certinfo {
   param($cert)
   $cert | Select-Object -property Subject,Issuer,NotBefore,NotAfter,SerialNumber

   $certChain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain
   $result = $certChain.Build($cert)
   $certChain.ChainPolicy.RevocationFlag = "EntireChain"
   $certChain.ChainPolicy.RevocationMode = "Online"
   Write-Host -Object "Validating certficate chain..." -foreground black -background yellow
   "`r`nValid`tCertificate"
   "-----`t-----------"
   foreach ($element in $certChain.ChainElements) {
       "{0}`t{1}" -f $element.Certificate.Verify(),$element.Certificate.Subject
   }
}

$Error.Clear()
("=" * 50)

try {
    switch ($PsCmdlet.ParameterSetName) {
        "names" {
            $client = New-Object  -TypeName System.Net.WebClient
            foreach ($num in $devices) {
                if ($local) {
                    $certs = Get-ChildItem -Recurse -Path Cert: | Where-Object { $_.Subject -like "CN=$num" }
                    if ($certs.count -eq 0) {
                        "No matching certificates found in the local device."
                        return ''
                    }
                    foreach ($cert in $certs) {
                        $certpath = $cert.pspath -replace 'Microsoft.PowerShell.Security\\Certificate::',"Cert:"
                        Write-host -Object "Reading $certpath"  -foreground black -background yellow
                        show-certinfo($cert)
                        ("=" * 50)
                    }
                }
                else {
                    $url = "https://certbroker.contoso.com/pfxshare/$($num).pfx"
                    Write-host -Object "Downloading $num.pfx" -foreground black -background yellow
                    $pfxBytes = $client.DownloadData($url)
                    $cert = import-pfxbytes($pfxBytes)
                    show-certinfo($cert)
                    ("=" * 50)
                }
            }
        }

        "files" {
            foreach ($file in $pfxfiles) {
                Write-host -Object "Reading $file" -foreground black -background yellow
                $pfxBytes = Get-Content -path $file -encoding Byte -ErrorAction:SilentlyContinue
                if ($error.count -ne 0) {throw $error}
                $cert = import-pfxbytes($pfxBytes)
                show-certinfo($cert)
                ("=" * 50)
            }
        }
    }
}
catch {
    $_.Exception.Message
    $_.InvocationInfo.PositionMessage
}