Created
October 20, 2025 07:33
-
-
Save PanosGreg/63b704adfb65c6c40ae349021215ed45 to your computer and use it in GitHub Desktop.
Functions to Get/Delete/Import a certificate from/on a windows service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This file contains the following functions: | |
| # Import-ServiceCertificate - Import a PFX certificate to a service cert store | |
| # Get-ServiceCertificate - Get the certificates of a service | |
| # Remove-ServiceCertificate - Delete a certificate from a service | |
| #Requires -RunAsAdministrator | |
| function Import-ServiceCertificate { | |
| <# | |
| .SYNOPSIS | |
| Import a PFX certificate to the cert store of a windows service | |
| .EXAMPLE | |
| Import-ServiceCertificate -PfxPath C:\temp\my.pfx -PfxPass 123qwe -Service tapisrv | |
| #> | |
| [CmdletBinding()] | |
| [OutputType([void])] | |
| param ( | |
| [Parameter(Mandatory, HelpMessage = 'The path to the PFX Certificate File.')] | |
| [ValidateScript({[Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($_)})] | |
| [Alias('Path')] | |
| [string]$PfxPath, | |
| [Parameter(Mandatory, HelpMessage = 'The password of the PFX certificate.')] | |
| [Alias('Password')] | |
| [string]$PfxPass, | |
| [Parameter(Mandatory, HelpMessage = 'The service that the certificate will be installed on.')] | |
| [ValidateScript({(Get-Service -Name $_) -as [bool]})] | |
| [Alias('Name')] | |
| [string]$Service | |
| ) | |
| $ErrorActionPreference = 'Stop' | |
| # create a temporary certificate store to put the service certificate (this is done on the Cert PSDrive) | |
| $TempStore = 'Cert:\LocalMachine\Temp' # <-- can only create custom stores in LocalMachine, not CurrentUser | |
| if (-not (Test-Path $TempStore)) {New-Item -Path $TempStore -Verbose:$false | Out-Null} | |
| # import the cert into the temp store (this is done on the Cert PSDrive) | |
| $Pass = $PfxPass | ConvertTo-SecureString -AsPlainText -Force | |
| $Cert = Import-PfxCertificate -File $PfxPath -Pass $Pass -CertStore $TempStore -Export -Verbose:$false | |
| # copy the cert from the temp store to the service store (this is done on the Registry) | |
| $Thumbprint = $Cert.Thumbprint | |
| $Source = "HKLM\Software\Microsoft\SystemCertificates\Temp\Certificates\$Thumbprint" | |
| $Destin = "HKLM\SOFTWARE\Microsoft\Cryptography\Services\$Service\SystemCertificates\My\Certificates\$Thumbprint" | |
| reg copy $Source $Destin /s /f 2>&1 | where {$_ -is [Management.Automation.ErrorRecord]} | Write-Error -EA Stop | |
| # clean up the temp store, delete the certs within, but leave the temp store (this is done on the PSDrive) | |
| $Cert | Remove-Item -Verbose:$false -Force | |
| } | |
| function Get-ServiceCertificate { | |
| <# | |
| .SYNOPSIS | |
| Gets the certificates of a windows service | |
| .EXAMPLE | |
| Get-ServiceCertificate -Name tapisrv | |
| #> | |
| [cmdletbinding()] | |
| [OutputType([psobject])] | |
| param ( | |
| [Parameter(Mandatory, HelpMessage = 'The service which we''ll get the certificates from.')] | |
| [ValidateScript({(Get-Service -Name $_) -as [bool]})] | |
| [Alias('Name')] | |
| [string]$Service | |
| ) | |
| $ErrorActionPreference = 'Stop' | |
| # get all the certificate thumbprints of that service (this is done on the Registry) | |
| $RegPath = 'HKLM:\SOFTWARE\Microsoft\Cryptography\Services\*\SystemCertificates\My\Certificates\' | |
| $RegFilter = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Services\$Service\SystemCertificates\My\Certificates\*" | |
| $SvcThumbs = (Get-ChildItem -Path $RegPath -Recurse | where Name -like $RegFilter).PSChildName | |
| # exit if we did not find any certificates for that service | |
| if (([array]$SvcThumbs).Count -eq 0) { | |
| Write-Verbose "No certificates found for the $Service service" | |
| return | |
| } | |
| # create a temporary certificate store to put the service certificate (this is done on the Cert PSDrive) | |
| $TempStore = 'Cert:\LocalMachine\Temp' # <-- can only create custom stores in LocalMachine, not CurrentUser | |
| if (-not (Test-Path $TempStore)) {New-Item -Path $TempStore -Verbose:$false | Out-Null} | |
| # copy the cert from the service store into the temp store (this is done on the Registry) | |
| # and then get the certificate object (this is done on the PSDrive) | |
| $CertList = foreach ($Thumbprint in $SvcThumbs) { | |
| $Source = "HKLM\SOFTWARE\Microsoft\Cryptography\Services\$Service\SystemCertificates\My\Certificates\$Thumbprint" | |
| $Destin = "HKLM\Software\Microsoft\SystemCertificates\Temp\Certificates\$Thumbprint" | |
| reg copy $Source $Destin /s /f 2>&1 | where {$_ -is [Management.Automation.ErrorRecord]} | Write-Error -EA Stop | |
| Get-ChildItem $TempStore | where Thumbprint -eq $Thumbprint | |
| } | |
| # assemble the output object | |
| $out = $CertList | foreach { | |
| if ($_.Subject -match 'CN=') {$Name = (@($_.Subject.Split(',')) -match 'CN=')[0] -replace 'CN=',$null} | |
| else {$Name = $_.Subject} | |
| [pscustomobject]@{ | |
| PSTypeName = 'Service.Certificate' | |
| ComputerName = $env:COMPUTERNAME | |
| ServiceName = $Service # <-- [string] the service name, not the sevice display name | |
| ServiceName2 = (Get-Service -Name $Service).DisplayName | |
| Thumbprint = $_.Thumbprint | |
| Name = $Name # <-- [string] the CN part from the certificate's subject name | |
| ExpiresAt = $_.NotAfter # <-- [datetime] | |
| Certificate = $_ # <-- [Security.Cryptography.X509Certificates.X509Certificate2] | |
| } | |
| } | |
| # clean up the temp store (delete the certs within, but leave the temp store) | |
| $CertList | Remove-Item -Verbose:$false -Force | |
| # finally return the output | |
| Write-Output $out | |
| } | |
| function Remove-ServiceCertificate { | |
| <# | |
| .SYNOPSIS | |
| Delete a certificate from the cert store of a windows service | |
| .EXAMPLE | |
| Remove-ServiceCertificate -Name tapisrv -Thumbprint 621A5BBE9CC5F9FE4337D37D8B859DB0A0E1E293 | |
| .EXAMPLE | |
| Remove-ServiceCertificate -Name tapisrv | |
| It deletes all the certificates from the tapisrv service | |
| #> | |
| [cmdletbinding()] | |
| [OutputType([void])] | |
| param ( | |
| [Parameter(Mandatory, HelpMessage = 'The service which we''ll delete the certificates from.')] | |
| [ValidateScript({(Get-Service -Name $_) -as [bool]})] | |
| [Alias('Name')] | |
| [string]$Service, | |
| [ValidatePattern('^[A-F0-9]{40}$')] | |
| [string[]]$Thumbprint | |
| ) | |
| $ErrorActionPreference = 'Stop' | |
| # get all the certificate thumbprints of that service (this is done on the Registry) | |
| if (-not ($PSBoundParameters.ContainsKey('Thumbprint'))) { | |
| $RegPath = 'HKLM:\SOFTWARE\Microsoft\Cryptography\Services\*\SystemCertificates\My\Certificates\' | |
| $RegFilter = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Services\$Service\SystemCertificates\My\Certificates\*" | |
| $RegKeys = Get-ChildItem -Path $RegPath -Recurse | where Name -like $RegFilter | |
| if ($RegKeys) {$Thumbprint = $RegKeys.PSChildName} | |
| else {return} | |
| } | |
| # delete the certificates from the service cert store (this is done on the Registry) | |
| foreach ($thumb in $Thumbprint) { | |
| $Path = "HKLM:\SOFTWARE\Microsoft\Cryptography\Services\$Service\SystemCertificates\My\Certificates\$thumb" | |
| Remove-Item -Path $Path -Recurse -Force -Verbose:$false | |
| } | |
| } | |
| ## Notes about -match | |
| # The -match operator is both a comparison operator and an array-filter operator, depending on its input object. | |
| # If you pass a single string, it returns a boolean. But if it's an array of strings, | |
| # it returns all the elements of the array that match the pattern. | |
| # If you don't know whether you'll have an array of strings or a single string. | |
| # Enclose the input item with the array syntax @() to ensure that you still pass an array to the -match operator | |
| # Otherwise, if it's single string you'll get back $True or $False instead of the item if it matches. | |
| # Further on, using the -match operator with an array input, it will always return an array | |
| # even if it has a single item in it. | |
| # So if you then need to select the 1st item only, you can use an index. | |
| # By comparison if you use the Where-Object to filter an array of strings and get a single string | |
| # it will be unboxed and thus the index will return a character from the string | |
| ## About the Temporary Certificate Store | |
| # You can only create a custom store under the LocalMachine, it cannot be done in CurrentUser | |
| # That is why you need to have admin rights to run the Get-ServiceCertificate function. | |
| ## About the reg.exe CMD command | |
| # The reason why we're using the reg.exe command instead of the PowerShell native Copy-Item | |
| # is because the reg.exe will also create any necessary registry folders if not there. | |
| # While the Copy-Item will just error out. As-in the following would error out: | |
| # Copy-Item -Path $Source -Destination $Destin -Recurse -Verbose:$false | |
| # Which then means we would first need to check if the registry folder is there, | |
| # create one if not there, and then copy the registry key with the Copy-Item. | |
| # So by using the reg.exe we're saving a few lines of code, and simplify our script. | |
| ## About NTFS permissions | |
| # These commands do not take into account any potential changes that might be needed on the | |
| # actual certificate files. As-in if the windows service is running under a specific account | |
| # and that account is not a local admin, then you would need to grant access of the cert files | |
| # to that account on the file system level. These commands do not do that, to keep things simple. | |
| ## About argument completion for the service parameter | |
| # The service parameter can take only a specific value, which is any windows service name of the | |
| # local computer. I could've added an argument completer to create a list of those service names | |
| # or a custom validation attribute for the same. But I've chosen to ommit that to keep the code | |
| # simple. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment