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
Comments
- Anonymous
January 03, 2014
Very nice. I just did basically the same thing back in March 2013 with a ton of verbose logging. Kudos.