Created
May 18, 2026 15:42
-
-
Save Bill-Stewart/b33f7c3b25b140d1871c9bbe5ddec145 to your computer and use it in GitHub Desktop.
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
| # Out-CertificatePrivateKey.ps1 | |
| # Written by Bill Stewart (bstewart AT iname.com) | |
| #requires -version 5.1 | |
| # Version history: | |
| # | |
| # 2026-05-18 | |
| # * Initial version. | |
| <# | |
| .SYNOPSIS | |
| Outputs a certificate's private key in RSA PKCS#8 base64-encoded (PEM) format. | |
| .DESCRIPTION | |
| Outputs a certificate's private key in RSA PKCS#8 base64-encoded (PEM) format. You can specify either the path to a PFX file or the SHA1 thumbprint (hash) of a certificate in the certificate store. | |
| .PARAMETER Path | |
| Specifies the path of a PFX (PKCS#12) file that contains a certificate and its private key. | |
| .PARAMETER Password | |
| Specifies the password for the PFX file. If you omit this parameter, you will be prompted to enter the password. | |
| .PARAMETER Thumbprint | |
| Specifies the SHA1 thumbprint (hash) of a certificate in the certificate store. This parameter is a 40-character hexadecimal string. The certificate must have a private key and the private key must be marked as exportable. | |
| .INPUTS | |
| None | |
| .OUTPUTS | |
| String | |
| .NOTES | |
| The certificate's private key will be output in RSA PKCS#8 format and base64-encoded (PEM format), as in the following example: | |
| -----BEGIN PRIVATE KEY----- | |
| MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWsEjZQRdgGJi26Lr6y5KOmoxp | |
| L+WV8KoPK4qh0sBVEvgclhk/chxcVYjdNuUyaFc/4H5jsq22GfR7XxfRcGY6mcjicJl9royHRrWB | |
| 8RXDX7/dDgcDIqrkmxCAmunoYAyR54DZmJf/eENsAZ0xog5f+sxpWSO0m6/N64ML4dhUpbbGrEmn | |
| ox6EcIw9ktZrLOmZsPSv1X9/1Az2W4PKJLnMcKhSzI6awJ9jpftOLP8E87/QytrZm3VL6bw1/tRZ | |
| uSE2Ndu3+jBJu4caZ0oKSYzyd9zGRYapJQ1ujrmsgCewE13V5viu9vQS8aUFFOjfiSZ2xGDCU2m6 | |
| hQNUGVG82AKBAgMBAAECggEBAISeeSHbl7kA+LVkprxuLKZyyfHgaI5rEGI2n3i/W5gyE5p1Djun | |
| HeXb4KPmWUb+NYd8AhdMNQqcle3wHCKAsGXawcE/lVGSFbUq9MzERJFgAIFpPOOu8qotL9CB4aRu | |
| UNWAkJPaM2rYu61ogdY7KW4Y21Tof6n90mf96AHbDWuboMCCwhGeIwvsvkvQ3ZojwpJvwEHOcjDo | |
| uRhazbPvjJ1sKbJh/Ku1gLD/bM0Pe/66pUXULkHaxNeby4dnWoe8PXWc9YVBi631TrtoE1xer2nr | |
| UYcrIZ7PIp9YwrsboRmLqX37sHPWYX/a8B3K2Dj3+hmwvlvF0CgFwwfU2qFvEJECgYEA8QUSb6M+ | |
| Sv1fkEMqJ4agxEUDSq20YQUcbxqAJabcv/p3NttIOpcM123f5qtWPsZ2Fd+2IOhSEsDjxZLwCI2N | |
| 1TZjZLLQoamHFnvpY0Ke5YE0Hyevm7LKJ5d8Ket4luFdF7h+DA0K5NYywPbv0u9y0S1S2892J6id | |
| bbMB54opaC8CgYEA5AhABDr6FOY/sCsuUjJeSI0lIrn5VOp/gILSp1I4AwveB7jaaNIWTGibhlrG | |
| TZ+NZYs9BOAn+tkggJHj6R51RiuDx+vmS2xFJ6Q9tpeYXi/2sueHeFFiGPgCVcF+xlJwI85O6CIY | |
| CYgx2hwwh/bQbTRUjrV5jMBYRmYH5OJp5E8CgYBIsgXwhCVKywdi5M83IsUkzcgDY0cvqDH+VNjo | |
| Al88zFfexN8RRPQLmklZMr6NqqB9Un+Fh20R2PoaYophmgh4kbIC8mg5CoADSRaI6NQyVvkYmIjA | |
| Bnow3OEHZ810fL7uY6gpDFoH0Y93YkPNtIlGDEycngN+TL5HEyH061g34wKBgQDeZUFyHsb6jRj+ | |
| Sw7xMZiMr8PRG4oVjGLvxieG8oQYTIGuDFZPNt5yhO9x2KRg3kR8DZFkaO31E8BERIUAPiloV1Y+ | |
| LE89rgnAQMnDhBQhgCvdjjusV8sRyQgV9RIcgBhIg5Snh2DkIwVj0GxwnoHmj8wMJue2I320oxv9 | |
| 2egmtwKBgA7eDuxYat7QueREeU7k3zvdZlo9WdTloJiAbXumPtxfrJPrkogKENmtXtzZ+WHPhc1H | |
| wo37f20/pLrbnVEfu/rXFG/V5LnhYoYA9zkyl4JL5i/CZAQHfy6ODJYwAWUb9QZn9qABviqCj6Lx | |
| rUjWoHcK31lw0j6ysbeaSTaNmNKF | |
| -----END PRIVATE KEY----- | |
| The output is a single string with embedded line breaks (rather than an array). | |
| .EXAMPLE | |
| PS > Out-CertificatePrivateKey -Thumbprint dd808ad30526413d816a14b41ed27867f48a8373 | |
| Outputs the private key for the specified certificate in the certificate store. The private key must exist and must be marked as exportable. | |
| .EXAMPLE | |
| PS > Out-CertificatePrivateKey myCert.pfx | |
| Outputs the private key for the certificate stored in the myCert.pfx file. You will be prompted to enter the PFX file password. | |
| #> | |
| [CmdletBinding(DefaultParameterSetName = "PFXFile")] | |
| param( | |
| [Parameter(ParameterSetName = "PFXFile",Position = 0,Mandatory)] | |
| [ValidateNotNullOrEmpty()] | |
| [String] | |
| $Path, | |
| [Parameter(ParameterSetName = "PFXFile")] | |
| [Security.SecureString] | |
| $Password, | |
| [Parameter(ParameterSetName = "Thumbprint",Mandatory)] | |
| [ValidateNotNullOrEmpty()] | |
| [String] | |
| $Thumbprint | |
| ) | |
| if ( [Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT ) { | |
| throw "Windows platform required" | |
| } | |
| # Windows error codes | |
| $ERROR_FILE_NOT_FOUND = 0x00000002 # 'The system cannot find the file specified' | |
| $ERROR_INVALID_PASSWORD = 0x00000056 # 'The specified network password is not correct' | |
| $CRYPT_E_BAD_ENCODE = 0x80092002 # 'An error occurred during encode or decode operation' | |
| $CRYPT_E_UNEXPECTED_MSG_TYPE = 0x8009200A # 'The certificate does not have a property that references a private key' | |
| $CRYPT_E_NO_DECRYPT_CERT = 0x8009200C # 'Cannot find the certificate and private key to use for decryption' | |
| # Writes a custom error to the error stream | |
| function Write-CustomError { | |
| param( | |
| [Int] | |
| $errorCode, | |
| [String] | |
| $subject, | |
| [Management.Automation.ErrorCategory] | |
| $errorCategory = [Management.Automation.ErrorCategory]::NotSpecified | |
| ) | |
| # Get localized error message for the error code | |
| $message = ([ComponentModel.Win32Exception] $errorCode).Message | |
| # Append subject to message if specified | |
| if ( $subject ) { $message += " - '$subject'" } | |
| # Append error code to message (hex string if < 0, decimal if >= 0) | |
| $message += ((" (0x{0:X8})" -f $errorCode),(" ({0})" -f $errorCode))[$errorCode -ge 0] | |
| # Build ErrorRecord object | |
| $errorRecord = New-Object Management.Automation.ErrorRecord((New-Object ComponentModel.Win32Exception($errorCode, | |
| $message)),$message,$errorCategory,$null) | |
| # Use parent scope PSCmdlet to write error (so source reflects script name) | |
| (Get-Variable "PSCmdlet" -Scope 1).Value.WriteError($errorRecord) | |
| } | |
| # Outputs the low 16 bits of a 32-bit error code value | |
| function Get-Win32ErrorCode { | |
| param( | |
| [Int] | |
| $value | |
| ) | |
| [BitConverter]::ToUInt32([BitConverter]::GetBytes($value),0) -band 0xFFFF | |
| } | |
| switch ( $PSCmdlet.ParameterSetName ) { | |
| "Thumbprint" { | |
| # The same certificate might be in multiple locations in the store | |
| $X509Certs = Get-ChildItem Cert:\ -Recurse -Include $Thumbprint | |
| if ( $null -eq $X509Certs ) { | |
| # If the thumbprint was not found, write an error containing error | |
| # code CRYPT_E_NO_DECRYPT_CERT | |
| $ErrorCode = $CRYPT_E_NO_DECRYPT_CERT | |
| Write-CustomError $ErrorCode $Thumbprint ([Management.Automation.ErrorCategory]::ObjectNotFound) | |
| exit $ErrorCode | |
| } | |
| # For the cert(s) we found, select the first one that has a private key | |
| $X509Cert2 = $X509Certs | Where-Object { $_.HasPrivateKey } | Select-Object -First 1 | |
| if ( $null -eq $X509Cert2 ) { | |
| # If we found the certificate in the store but not the private key, write | |
| # an error containing error code CRYPT_E_UNEXPECTED_MSG_TYPE | |
| $ErrorCode = $CRYPT_E_UNEXPECTED_MSG_TYPE | |
| Write-CustomError $ErrorCode -errorCategory ([Management.Automation.ErrorCategory]::ObjectNotFound) | |
| exit $ErrorCode | |
| } | |
| } | |
| "PFXFile" { | |
| # Prompt for PFX file password if -Password parameter was not specified | |
| if ( -not $PSBoundParameters.ContainsKey("Password") ) { | |
| $Password = Read-Host "Password" -AsSecureString | |
| } | |
| try { | |
| $X509Cert2FilePath = (Resolve-Path $Path -ErrorAction Stop).ProviderPath | |
| } | |
| catch { | |
| # Write an error containing the error code ERROR_FILE_NOT_FOUND if we got | |
| # "item not found"; otherwise, write an error containing the HResult code | |
| # in the exception | |
| $ErrorCode = ($_.Exception.HResult,$ERROR_FILE_NOT_FOUND)[$_.Exception.GetType() -eq [Management.Automation.ItemNotFoundException]] | |
| Write-CustomError $ErrorCode $Path $_.CategoryInfo.Category | |
| exit $ErrorCode | |
| } | |
| try { | |
| $X509Cert2 = New-Object Security.Cryptography.X509Certificates.X509Certificate2($X509Cert2FilePath, | |
| $Password,[Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) | |
| } | |
| catch { | |
| # If the error object's inner exception HResult code's least significant | |
| # 16 bits is ERROR_INVALID_PASSWORD, write an error containing that error | |
| # code; otherwise, write an error containing CRYPT_E_BAD_ENCODE | |
| $ErrorCode = ($CRYPT_E_BAD_ENCODE,$ERROR_INVALID_PASSWORD)[(Get-Win32ErrorCode $_.Exception.InnerException.HResult) -eq $ERROR_INVALID_PASSWORD] | |
| Write-CustomError $ErrorCode $X509Cert2FilePath ([Management.Automation.ErrorCategory]::InvalidData) | |
| exit $ErrorCode | |
| } | |
| } | |
| } | |
| # $X509Cert2 is type System.Security.Cryptography.X509Certificates.X509Certificate2 | |
| try { | |
| # Get the private key as an array of bytes (BLOB - binary large object) | |
| $RSACng = [Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($X509Cert2) | |
| $KeyBLOB = $RSACng.Key.Export([Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob) | |
| } | |
| catch { | |
| # Write an error containing the inner exception's HResult | |
| $ErrorCode = $_.Exception.InnerException.HResult | |
| Write-CustomError $ErrorCode -errorCategory $_.CategoryInfo.Category | |
| exit $ErrorCode | |
| } | |
| # Output single PKCS#8 string containing the private key (base64-encoded) | |
| @" | |
| -----BEGIN PRIVATE KEY----- | |
| {0} | |
| -----END PRIVATE KEY----- | |
| "@ -f [Convert]::ToBase64String($KeyBLOB,[Base64FormattingOptions]::InsertLineBreaks) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment