Importing and Removing Certificates

We knew it had to come to this.  All these posts about examining .cer files, scanning for certificates being served on :443, and auditing the LocalComputer\My certificate store.  We knew there had to come a time when we programmatically import and remove certificates.

And right after that, we know we will unwittingly shoot ourselves in the foot full-auto.

Still, it has to be done.  Here’s a library of functions that will import and delete certificates, as well as display the Root CA for a given certificate.  Hey, that last one is a read-only operation, so it’s mostly harmless.  (Yes, I remember to $store.Close()…)

Remember: With great power comes great responsibility.

 

<#
.Synopsis
Functions to audit, remove, and install certificates.

.Description
A way to cause wholesale damage to a bunch of servers with minimal effort.  Use with care.  -Confirm and -WhatIf are not implemented.

You have been warned.

.Notes
When        Who     What    Why
2013-05-28 timdunn  1.0     Release to operations

#>

function Get-CertificateTrustChain {
    <#
    .Synopsis
    Returns trust chain for specified X509 certificate.

    .Description
    Uses .NET methods to build a trust chain for the specified X.509 certificate.  The chain is returned as an array of certificates. The first element of the array is the specified X.509 certificate itself.  The last element is the root CA (e.g.: GTE CyberTrust)

    .Parameter Certificate
    Certificate for which to validate the trust chain.  The value can be either a path to a file, or an X509 object.

    .Parameter Password
    Password used to open the certificate if it is in .PFX format.

    .Inputs
    [object] Certificate

    .Outputs
    [X509Certificate2[]] X.509 certificates.

    .Link
    https://msdn.microsoft.com/en-us/library/vstudio/system.security.cryptography.x509certificates.x509chain.build.aspx
    https://msdn.microsoft.com/en-us/library/vstudio/system.security.cryptography.x509certificates.x509chain.chainelements.aspx

    .Notes
    When        Who     What    Why
    2013-05-28  timdunn 1.0     Release to operations

    #>

    param (
        [Parameter(ValueFromPipeline=$true,Position=0)][Object]$Certificate,
        [string]$Password = $null
    );

    begin { $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain; }

    process {
        if ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) {
            #noop
        } elseif (Test-Path $Certificate) {
            $Path = $Certificate;
            try {
                $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $Certificate, $Password;
            } # try
            catch [Exception]{
                Write-Warning "Unable to cast path '$Path' to X509Certificate2 object.";
                $Certificate = $null;
            } # catch
        } else {
            Write-Warning "Unable to find -Certificate $Certificate";
            $Certificate = $null;
        } # if ($Certificate is

        if ($Certificate) {
            Write-Progress -Activity "Building trust chain for" -Status ("$($Certificate.Subject) ($($Certificate.Thumbprint))");
            $chain.Build($Certificate) | Out-Null;
            if ( $chain.ChainElements ) {
                $chain.ChainElements | % { $_.Certificate; }
            } else {
                Write-Warning "Unable to verify certificate chain for certificate with thumbprint $($Certificate.Thumbprint)."
            } # if ($chain.ChainElements...
        } # if ($Certificate...

    } # process {
} # function

function Get-RemoteCertificate {
    <#
    .Synopsis
    Get list of certificates on specified computer(s).

    .Description
    Get list of certificates on specified computer(s).  Data returned is a PSObject consisting of the following properties:

    - [string]ComputerName: the computer from which the certificate was obtained.

    - [string]Subject: the "Issued to" string for the certificate.

    - [string]NotAfter: the expiration date for the certificate.

    - [string]SerialNumber: the unique serial number for the certificate.

    - [string]RootCA: the root Certificate Authority which issued the intermediate Certificate Authority certificates resulting in the certificate.

    .Parameter ComputerName
    Computers from which to get list of certificates.  Defaults to $env:ComputerName.

    .Parameter StoreName
    Folder under Certificate store from which to get list of certificates.  Defaults to 'My', which corresponds to 'LocalComputer\Personal' folder.

    .Parameter X509
    In addition to the above properties, the PSObject returned has the X509 property which is an [X509Certificate2] object.

    .Inputs
    [String[]]

    .Outputs
    [PSObject[]]

    .Notes
    When        Who     What    Why
    2013-05-28  timdunn 1.0     Release to operations

    #>

    param (
        [Parameter(ValueFromPipeline=$true,Position=0)][String[]]$computerName = @($Env:COMPUTERNAME),
        [string]$StoreName = 'My',
        [switch]$X509
    );

    process {
        foreach ($computer in $computerName) {
            $storeLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]'LocalMachine';
            $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("\\$computer\$StoreName",$storeLocation);
            if (!$store) {
                Write-Warning "Unable to open -computerName '\\$computer\LocalComputer\$StoreName\' certificate store.";
                continue;
            } # if (!store...

            $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly");

            $store.Certificates | Select-Object -Property @{
                n = 'ComputerName'; e = { $computer; }
            }, @{
                n = 'Subject'; e = {
                    if ($subject = $_.Subject -replace ',.*' -replace '.*=') {
                        $subject;
                    } else {
                        # default to DnsNameList.Unicode if no Subject found
                        $_.DnsNameList.Unicode;
                    } # if ($subject
                } # n = 'Subject'; e = {
            }, NotAfter, SerialNumber, @{
                n = 'RootCA'; e = {
                    (Get-CertificateTrustChain $_ | Select-Object -Last 1).Subject -replace ',.*' -replace '.*=';
                } # n = 'RootCA'; e = {
            }, @{
                n = 'X509'; e = { $_; }
            } | % {
                if ($X509) {
                    $_;
                } else {
                    $_ | Select-Object -Property ComputerName, Subject, NotAfter, SerialNumber, RootCA;
                } # if ($X509)
            } # $store.Certificates | Select-Object... | foreach {

            $store.Close();

        } # foreach ($computer in
    } # process {
} # function

function Test-RemoteCertificate {
    <#
    .Synopsis
    Test for a certificate (specified by serial number) on specified computer(s).

    .Description
    Test for a certificate (specified by serial number) on specified computer(s).  Data returned is a PSObject consisting of the following properties:

    - [string]ComputerName: the computer for which the certificate was tested.

    - [string]SerialNumber: the serial number for the certificate for which was tested.

    - [boolean]Found: the presence or absence of the certificate on the specified computer.

    .Parameter ComputerName
    Computers from which to get list of certificates.  Defaults to $env:ComputerName.

    .Parameter SerialNumber
    Serial number of certificate whose existence for which to test.  Default is $null, which will cause the function to emit a warning and return.

    .Parameter StoreName
    Folder under Certificate store from which to get list of certificates.  Defaults to 'My', which corresponds to 'LocalComputer\Personal' folder.

    .Parameter AsBoolean
    For each computer(s) specified, only return $True (certificate with specified serial number was found) or $False (certificate with specified serial number was not found).

    .Inputs
    [String[]]

    .Outputs
    [PSObject[]]

    or

    [Bool[]]

    .Notes
    When        Who     What    Why
    2013-05-28  timdunn 1.0     Release to operations

    #>

    param (
        [Parameter(ValueFromPipeline=$true,Position=1)][String[]]$computerName = @($Env:COMPUTERNAME),
        [Parameter(Position=0)][string]$SerialNumber = $null,
        [string]$StoreName = 'My',
        [switch]$AsBool
    );

    process {
        if (!$SerialNumber) {
            Write-Warning "-SerialNumber not specified";
            return;
        } # if (!$SerialNumber

        foreach ($computer in $computerName) {
            $storeLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]'LocalMachine';
            $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("\\$computer\$StoreName",$storeLocation);
            if (!$store) {
                Write-Warning "Unable to open -computerName '\\$computer\LocalComputer\$StoreName\' certificate store.";
                continue;
            } # if (!store...

            $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly");

            New-Object -TypeName PSObject -Property @{
                ComputerName = $computer;
                SerialNumber = $SerialNumber;
                Found = [bool]($store.Certificates | Where-Object { $_.SerialNumber -eq $serialNumber })
            } | % {
                if ($AsBool) {
                    $_.Found;
                } else {
                    $_ | Select-Object -Property ComputerName, SerialNumber, Found;
                } # if ($AsBool
            } # New-Object ... | foreach {

            $store.Close();

        } # foreach ($computer
    } # process {
} # function

function Import-RemoteCertificate {
    <#
    .Synopsis
    Installs a certificate (specified by path\to\file) on specified computer(s).

    .Description
    Installs a certificate (specified by path\to\file) on specified computer(s).  Data returned is a PSObject consisting of the following properties:

    - [string]ComputerName: the computer for which the certificate was tested.

    - [string]SerialNumber: the serial number for the certificate for which was tested.

    - [string]Status: the result of attempting to install the certificate on the specified computer.  Valid values are

        * FOUND: the certificate was already present on the computer.

        * INSTALLED: the certificate was not already present, but was successfully installed on the computer.

        * FAILED_TO_INSTALL: the certificate was not already present, and was not successfully installed on the computer.

    .Parameter ComputerName
    Computers on which to install the certificate.  Defaults to $env:ComputerName.

    .Parameter Path
    Path\to\file containing the certificate.  Default is $null, which will cause the function to emit a warning and return.

    .Parameter Password
    Password used to open the certificate file if it is in .PFX format.  Default is $null, which is supported by .CER, .CRT, and .P7B files.

    .Parameter StoreName
    Folder under Certificate store in which to install the certificate.  Defaults to 'My', which corresponds to 'LocalComputer\Personal' folder.

    .Inputs
    [String[]]

    .Outputs
    [PSObject[]]

    .Notes
    When        Who     What    Why
    2013-05-28  timdunn 1.0     Release to operations

    #>

    param (
        [Parameter(ValueFromPipeline=$true,Position=1)][String[]]$ComputerName = @($Env:COMPUTERNAME),
        [Parameter(Position=0)][string]$Path = $null,
        [string]$Password = $null,
        [string]$StoreName = 'My'
    );

    begin {
        # toggle to only display header once
        $headerDisplayed = $false;

        $scriptBlock = {
            param (
                [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
                [string]$StoreName = 'My'
            );

            $storeLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]'LocalMachine';
            $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($StoreName,$storeLocation);
            if (!$store) {
                Write-Warning "Unable to open -computerName '\\$computer\LocalComputer\$StoreName\' certificate store.";
                continue;
            } # if (!store...

            $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]"ReadWrite");
            $store.Add($Certificate);
            $store.Close();

        } # $scriptBlock =
    } # begin

    process {

        if (!$Path -or !(Test-Path -Path $Path)) {
            Write-Warning "-Path $Path not found";
            return;
        } # if (!$Path...

        $Path = (Resolve-Path -Path $Path).ProviderPath -replace ':','$';
        if ($Path -notmatch '^\\\\') { $Path = $Path -replace '^',"\\$($env:ComputerName.ToLower())\"; }
        Remove-Item Variable:Certificate -ErrorAction SilentlyContinue;

        $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $Path, $Password;
        if (!$Certificate) {
            Write-Warning "-Path $path cannot be opened with specified -Password value";
            return;
        } # if (!$Certificate

        $SerialNumber = $Certificate.SerialNumber;

        if (!$headerDisplayed) {
            # Display header of certificate only once
            if (!($Subject = $Certificate.Subject -replace ',.*' -replace '.*=')) {
                $Subject = $Certificate.DnsNameList.Unicode;
            } # if (!($Subject = ...
            $NotAfter = $Certificate.NotAfter;

            Write-Host -ForegroundColor Green "Subject:`t'$subject'`nExpires:`t$notAfter`nSerialNumber:`t$SerialNumber";
            $headerDisplayed = $true;
        } # if ($headerDisplayed

        foreach ($computer in $computerName) {
            if ((Test-RemoteCertificate -ComputerName $computer -StoreName $StoreName -SerialNumber $SerialNumber).Found) {
                $status = 'FOUND';
            } else {

                if ($computer -eq $env:ComputerName) {
                    # running Invoke-Command on LocalHost seems to have issues
                    Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $Certificate, $StoreName;
                } else {
                    Invoke-Command -ComputerName $computer -ScriptBlock $scriptBlock -ArgumentList $Certificate, $StoreName;
                } # if ($computer

                if ((Test-RemoteCertificate -ComputerName $computer -StoreName $StoreName -SerialNumber $SerialNumber).Found) {
                    # after installing certificate with Invoke-Command above, test for presence in store.
                    $status = 'INSTALLED';
                } else {
                    $status = 'FAILED_TO_INSTALL';
                } # if (Test-RemoteCertificate...

            } # if((Test-RemoteCertificate...

            New-Object -TypeName PSObject -Property @{
                ComputerName = $computer;
                SerialNumber = $SerialNumber;
                Status = $status
            } | Select-Object -Property ComputerName, SerialNumber, Status;

        } # foreach ($computer
    } # process
} # function

function Remove-RemoteCertificate {
    <#
    .Synopsis
    Remove a certificate (specified by serial number) on specified computer(s).

    .Description
    Remove a certificate (specified by serial number) on specified computer(s).  Data returned is a PSObject consisting of the following properties:

    - [string]ComputerName: the computer for which the certificate was tested.

    - [string]Subject: the "Issued to" string for the certificate.

    - [string]NotAfter: the expiration date for the certificate.

    - [string]Status: the result of attempting to remove the certificate on the specified computer.  Valid values are

        * NOT_FOUND: the certificate was already not present on the computer.

        * REMOVED: the certificate was present, but was successfully removed from the computer.

        * FAILED_TO_REMOVE: the certificate was present, and was not successfully removed from the computer.

    .Parameter ComputerName
    Computers from which to remove the specified certificate.  Defaults to $env:ComputerName.

    .Parameter SerialNumber
    Serial number of certificate to remove.  Default is $null, which will cause the function to emit a warning and return.

    .Parameter StoreName
    Folder under Certificate store from which to get list of certificates.  Defaults to 'My', which corresponds to 'LocalComputer\Personal' folder.

    .Inputs
    [String[]]

    .Outputs
    [PSObject[]]

    .Notes
    When        Who     What    Why
    2013-05-28  timdunn 1.0     Release to operations

    #>

    param (
        [Parameter(ValueFromPipeline=$true,Position=1)][String[]]$ComputerName = @($Env:COMPUTERNAME),
        [Parameter(Position=0)][string]$SerialNumber = $null,
        [string]$StoreName = 'My'
    );

    process {
        if (!$SerialNumber) {
            Write-Warning "-SerialNumber not specified";
            return;
        } # if (!$serialnumber

        foreach ($computer in $computerName) {
            $storeLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]'LocalMachine';
            $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("\\$computer\$StoreName",$storeLocation);
            if (!$store) {
                Write-Warning "Unable to open -computerName '\\$computer\LocalComputer\$StoreName\' certificate store.";
                continue;
            } # if (!store...

            $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]"ReadWrite");

            if ($Certificate = $store.Certificates | Where-Object { $_.SerialNumber -eq $serialNumber }) {

                $SerialNumber = $Certificate.SerialNumber;
                if (!($Subject = $Certificate.Subject -replace ',.*' -replace '.*=')) {
                    $Subject = $Certificate.DnsNameList.Unicode;
                } # if (!($subject
                $NotAfter = $Certificate.NotAfter;

                $store.Remove($Certificate);
                if ($store.Certificates | Where-Object { $_.SerialNumber -eq $serialNumber }) {
                    $status = 'FAILED_TO_REMOVE';
                } else {
                    $status = 'REMOVED';
                } # if ($store.Certificates

            } else {
                $status = 'NOT_FOUND';
            } # if ($Certificate = ...

            $store.Close();

            New-Object -TypeName PSObject -Property @{
                ComputerName = $computer;
                Subject = $subject;
                NotAfter = $NotAfter;
                Status = $status;
            } | Select-Object -Property ComputerName, Subject, NotAfter, Status;

        } # foreach ($computer
    } # process {
} # function