Learn about Windows PowerShell
Summary: Learn how to use Windows PowerShell and Microsoft .NET classes to find expired certificates on local and remote computers.
Hey, Scripting Guy! How can I use Windows PowerShell and the .NET Framework classes to work with certificates?
-- PB
Hello PB,
Microsoft Scripting Guy Ed Wilson here. We continue Guest Blogger Week today with Boe Prox, currently a senior systems administrator with BAE Systems. He has been in the IT industry since 2003 and has spent the past three years working with VBScript and Windows PowerShell and now looks to script whatever he can, whenever he can. He is also a moderator on the Hey, Scripting Guy! Forum. You can check out his blog at http://boeprox.wordpress.com and also see his current project, the WSUS Administrator module, just recently published on CodePlex.
PKI certificates are one of those things that most system administrators know about but probably never spend a lot of time with until something goes wrong. Usually a certificate expires and causes some sort of interruption to a service in which users or the boss are asking what is happening.
What I am first going to do is to show you how to connect to a local or remote computer and view their certificates using the .NET class, System.Security.Cryptography.X509Certificates.X509Store.
There are two locations that you can connect to:
Of these two certificate store locations, only LocalMachine can be accessed remotely via the .NET class. Attempting to access CurrentUser will result in an “Access Denied” message because of security reasons.
Connecting to the local machine LocalMachine certificate store requires a few lines of code in order to gain access to the certificates:
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") $store.Open("ReadOnly") $store.Certificates
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
$store.Open("ReadOnly")
$store.Certificates
The first thing we see here is that we are creating the object using the X509 class and calling two values that represent the store name and store location that we will be connecting to. There are several store names that are available to view, as shown in the following image.
For this article, we are going to focus on the LocalMachine store name as that is where we will be looking for web SSL certificates, domain controller certificates, and machine certificates.
The next line of code has us setting the flag that we will be using when we open up the store. Because we are only viewing the certificates on the store and nothing else, we opt to go with the ReadOnly flag. There are other flags available when opening the store, as shown in the following image
Finally, we are able to view the machine certificates within store in the third line, as shown in the following image.
If anyone has used the Certificate provider in Windows PowerShell before, this will look very familiar. In fact, look at the output of the each command I run for both the .NET class and using the Certificate provider:
PS C:\Users\boe> $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") PS C:\Users\boe> $store.Open("ReadOnly") PS C:\Users\boe> ($store.certificates | Select -First 1).gettype() | Format-Table Name, BaseType -auto Name BaseType ---- -------- X509Certificate2 System.Security.Cryptography.X509Certificates.X509Certificate PS C:\Users\boe> (GCI cert:/localmachine/my | Select -First 1).gettype() | Format-Table Name,BaseType -auto Name BaseType ---- -------- X509Certificate2 System.Security.Cryptography.X509Certificates.X509Certificate
PS C:\Users\boe> $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
PS C:\Users\boe> $store.Open("ReadOnly")
PS C:\Users\boe> ($store.certificates | Select -First 1).gettype() | Format-Table Name, BaseType -auto
Name BaseType
---- --------
X509Certificate2 System.Security.Cryptography.X509Certificates.X509Certificate
PS C:\Users\boe> (GCI cert:/localmachine/my | Select -First 1).gettype() | Format-Table Name,BaseType -auto
The BaseType of each is System.Security.Cryptography.X509Certificates.X509Certificate, which might make you wonder: “Boe, why in the world are we using the .NET classes when we can so easily perform this with the provider?”
Remember when I mentioned that you could use the .NET class to connect to a certificate store on a remote machine? Well, here lies the limitation of the provider, at least if you do not have Windows PowerShell remoting enabled in your environment. With the provider, though you can more easily view all of the certificates in both the LocalMachine and CurrentUser store locations with nothing more than a line of code, you are unable to use it to make a query to a remote machine without first initiating a remote Windows PowerShell session to that server.
Using the same code snippet earlier, we make one small change to the store name by adding the UNC path to the system:
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("\\dc1\My","LocalMachine") $store.Open("ReadOnly") $store.Certificates
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("\\dc1\My","LocalMachine")
The results are shown in the following image.
Pretty cool, huh?
You can do this using the certificate provider, assuming you not only have Windows PowerShell 2.0 installed on both your client and the remote system, but also have remoting enabled on the remote system. However, if your place of business is still running Windows PowerShell 1.0 or has not made the leap to allow Windows PowerShell remoting for one reason or another, using the .NET classes to make the remote connection is your best bet.
Locating Expiring Certificates
Now that we have made a connection to the remote LocalMachine store on a server, let us go ahead and find out if any certificates are going to expire within the next 14 days.
Using my existing connection, we will look at the NotAfter property—the property that tells us the expiration date of the certificate—to find out if it has expired or if it will expire within 14 days. The NotAfter property of the certificate represents the local time that the given certificate will expire. With 10 lines of code, I can run a query against a remote server to find any certificates that are due to expire in the next 14 days.
#Number of days to look for expiring certificates $threshold = 14 #Set deadline date $deadline = (Get-Date).AddDays($threshold) $store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\dc1\my","LocalMachine") $store.open("ReadOnly") $store.certificates | % { If ($_.NotAfter -lt $deadline) { $_ | Select Issuer, Subject, NotAfter, @{Label="ExpiresIn"; Expression={($_.NotAfter - (Get-Date)).Days}} } } Issuer Subject NotAfter ExpiresIn ------ ------- -------- --------- CN=Root CA, DC=r... 1/25/2011 10:32:... 5
#Number of days to look for expiring certificates
$threshold = 14
#Set deadline date
$deadline = (Get-Date).AddDays($threshold)
$store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\dc1\my","LocalMachine")
$store.open("ReadOnly")
$store.certificates | % {
If ($_.NotAfter -lt $deadline) {
$_ | Select Issuer, Subject, NotAfter, @{Label="ExpiresIn"; Expression={($_.NotAfter - (Get-Date)).Days}}
}
Issuer Subject NotAfter ExpiresIn
------ ------- -------- ---------
CN=Root CA, DC=r... 1/25/2011 10:32:... 5
From the looks of it, there is one certificate that is going to expire in five days. As you can tell, I added the custom property ExpiresIn, which lists how many days until the certificate expires to the output, by using the @{Label=””;Expression={}} hash table. Imagine running this across your domain and finding out if you had certificates expiring before it became an issue! It is as easy as adding a few more lines of code to use a ForEach loop to iterate through all of the computers. Better yet, configure the code to send an email with this information, save as a script, and then set the script up as a scheduled job. You will then have this process completely automated.
Locate Expired Certificates
Building on what I covered with locating expiring certificates, I will slightly modify the existing code to find certificates that have expired.
$store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\dc1\my","LocalMachine") $store.open("ReadOnly") $store.certificates | % { If ($_.NotAfter -lt (Get-Date)) { $_ | Select Issuer, Subject, @{Label="ExpiredOn";Expression={$_.NotAfter}} } } Issuer Subject ExpiredOn ------ ------- -------- CN=Root CA, DC=r... 1/24/2011 04:13:...
If ($_.NotAfter -lt (Get-Date)) {
$_ | Select Issuer, Subject, @{Label="ExpiredOn";Expression={$_.NotAfter}}
Issuer Subject ExpiredOn
------ ------- --------
CN=Root CA, DC=r... 1/24/2011 04:13:...
Just like that, we found a certificate that has expired which leaves us to decide whether it needs to be renewed or removed from the store. Of course, the best part is this can easily be automated to send out an email to you if any expired certificates are found.
Get-PKICertificates Function
To assist with performing queries against local and remote systems and find expiring or expired certificates, I wrote an advanced function that you could use to accomplish this task. This advanced function allows you to supply a local or remote system, determine whether to use the LocalMachine or CurrentUser store, and search store names other than the “My” name. In addition, you can set the OpenFlag that you want to use when you query the store. The script is available for download in the Script Repository.
Here are a few examples of using the advanced function to locate certificates.
Listing Certificates That Expire in 14 Days PS C:\Users\boe> Get-PKICertificates -comp dc1 -StoreLocation LocalMachine -StoreName My -ExpiresIn 14 | Format-Table Su bject, FriendlyName,Issuer, @{Label="ExpiresIn";Expression={($_.NotAfter - (get-Date)).Days}} -auto Subject FriendlyName Issuer ExpiresIn ------- ------------ ------ --------- DC1 CN=Root CA, DC=rivendell, DC=com 4 List All Certificates on a Remote Computer PS C:\Users\boe> Get-PKICertificates -comp dc1 -StoreLocation LocalMachine -StoreName My Thumbprint Subject ---------- ------- A1CECD6C1B0A61F3D784EC29C60BD0D8F2FA46BD 7A42B8E32FABF3B85B1BAEF5D6FF6F29EB03C58E CN=Root CA, DC=rivendell, DC=com 3B0D348B90D663293067F8C481075927016F56CE CN=dc1.rivendell.com 10E740A73045250E9AF0778C7DAABB2E1F22C6D9 CN=dc1, OU=Scripting, O=Prox, L=Bellevue, S=NE, C=US List Certificates on Multiple Computers PS C:\Users\boe> Get-PKICertificates -comp dc1,boe-laptop -StoreLocation LocalMachine -StoreName My Thumbprint Subject ---------- ------- A1CECD6C1B0A61F3D784EC29C60BD0D8F2FA46BD 7A42B8E32FABF3B85B1BAEF5D6FF6F29EB03C58E CN=Root CA, DC=rivendell, DC=com 3B0D348B90D663293067F8C481075927016F56CE CN=dc1.rivendell.com 10E740A73045250E9AF0778C7DAABB2E1F22C6D9 CN=dc1, OU=Scripting, O=Prox, L=Bellevue, S=NE, C=US 211E73160B9E67F8C0EEBAB7007CF68589F392D1 CN=boe-laptop
Listing Certificates That Expire in 14 Days
PS C:\Users\boe> Get-PKICertificates -comp dc1 -StoreLocation LocalMachine -StoreName My -ExpiresIn 14 | Format-Table Su
bject, FriendlyName,Issuer, @{Label="ExpiresIn";Expression={($_.NotAfter - (get-Date)).Days}} -auto
Subject FriendlyName Issuer ExpiresIn
------- ------------ ------ ---------
DC1 CN=Root CA, DC=rivendell, DC=com 4
List All Certificates on a Remote Computer
PS C:\Users\boe> Get-PKICertificates -comp dc1 -StoreLocation LocalMachine -StoreName My
Thumbprint Subject
---------- -------
A1CECD6C1B0A61F3D784EC29C60BD0D8F2FA46BD
7A42B8E32FABF3B85B1BAEF5D6FF6F29EB03C58E CN=Root CA, DC=rivendell, DC=com
3B0D348B90D663293067F8C481075927016F56CE CN=dc1.rivendell.com
10E740A73045250E9AF0778C7DAABB2E1F22C6D9 CN=dc1, OU=Scripting, O=Prox, L=Bellevue, S=NE, C=US
List Certificates on Multiple Computers
PS C:\Users\boe> Get-PKICertificates -comp dc1,boe-laptop -StoreLocation LocalMachine -StoreName My
211E73160B9E67F8C0EEBAB7007CF68589F392D1 CN=boe-laptop
That is it for today. I hope you enjoyed my guest blog on querying for certificates using the .NET classes, and I would also like to thank Ed for allowing me this great opportunity to be a guest blogger for today.
Boe, I want to thank you for sharing with us and for being our guest blogger. Guest Blogger Week will continue tomorrow when Doug Finke will join us.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy
If you want to do more certificate work in powershell check out my post about the powershell certificate helper library @ ig2600.blogspot.com/.../better-certificate-management-in.html. It lets you do more certificate manipulation within powershell; without having to resort to the .net types directly.
This is pretty cool. So How could one build off this to say Delete all Certificate in the "Address Book or Other People" store issued by a specific CA
Really good resource Ed. I'm a complete ps beginner and need to collect SSL info from multiple servers that are not in a domain and all have different credentials. If anyone has any pointers on how I could do that I'd be very grateful.
Thanks!
@John VI
Thanks for the comment! Something like this should work to remove all certificates issued by a specific CA. Just change CA_Name to the CA you are looking for.
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("AddressBook","LocalMachine")
$store.Open("ReadWrite")
$store.Certificates | Where {
$_.Issuer -like "*<CA Name>*"
} | ForEach {
Write-Verbose ("Removing {0}" -f $_.Subject) -Verbose
$Store.Remove($_)
@John IV
Almost forgot, this would also work as well...
$certs = $store.Certificates | Where {
$_.Issuer -like "*<CA_Name>*"
$Store.RemoveRange($certs)
thanks this is cool! could you please alter the script so that i can run this in one server and it will fetch expiry date of other few servers and trigger an email to my inbox
@Ajoy Something like this would work if you are using the script I wrote on gallery.technet.microsoft.com/.../a2a500e5-1dd2-4898-9721-ed677399679c:
$BadCerts = Get-PKICertificates -Computer (Get-Content servers.txt) -StoreLocation LocalMachine -StoreName My -ExpiresIn 14
If ($BadCerts) {
$BadCerts | Export-Csv -NoTypeInformation (Join-Path $Pwd ExpiringCerts.csv)
Send-MailMessage -To <users> -From <account@domain.com> -Subject 'Expiring Certs' -SmtpServer <servername> -Attachments (Join-Path $Pwd ExpiringCerts.csv)
Just update the parameters in the Send-MailMessage cmdlet to reflect your environment.
PB,
You know I still get Access is denied when I call the Open Method on the store with LocalMachine for a remotecomputer.
I have admin privileges on both remote and local system
Exception calling "Open" with "1" argument(s): "Access is denied.
"
At line:1 char:12
+ $store.Open <<<< ("ReadOnly")
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
I am trying to use this to scan all the servers in our servers OU to look for expiring certs. Here is the code I am using.
$threshold = 30
ForEach ($Member in (get-adcomputer -Filter * -Searchbase "OU=servers,DC=xxxxx,dc=yy,dc=zzzz,dc=com"))
{
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("\\$Member.Name\My","LocalMachine")
I am getting the following error in response.
Exception calling "Open" with "1" argument(s): "The network path was not found.
At H:\scripts\LookForExpiredCerts.ps1:12 char:12
+ $store.open <<<< ("ReadOnly")
I know my problem is in the ("\\$Member.Name\My","LocalMachine") part of the code. I have tried numerous different ways to code this, putting quotes in different places and such, but no luck.
Can anyone tell me what I am doing wrong here? I am pretty new to scripting. I know I am close, but just can't quite get it. Any help would be appreciated.
Thank you in advance.
Great article! I really enjoyed the clear, well thought out explanation of the .Net class