Files
Invoke-OpenSSHConfiguration/Toolkit/Functions/ConvertFrom-CertificateToPEM.ps1
T
GraceSolutions c10b19fa6e Remove unnecessary individual PEM exports for CA bundle
- Removed -Export switch when creating CA bundle (only bundle is needed)
- Individual certificate directories and PEM files are no longer created
- Only the combined CA bundle file is written to disk
- Updated Example 4 documentation to reflect correct usage
2026-05-04 10:22:27 -04:00

940 lines
66 KiB
PowerShell

## Microsoft Function Naming Convention: http://msdn.microsoft.com/en-us/library/ms714428(v=vs.85).aspx
#region Function ConvertFrom-CertificateToPEM
Function ConvertFrom-CertificateToPEM
{
<#
.SYNOPSIS
Convert X.509 certificates to PEM format with support for certificate, public key, and private key extraction.
.DESCRIPTION
Extracts and converts X.509 certificates from the Windows certificate store to Privacy-Enhanced Mail (PEM) format.
This function supports both RSA and ECC (ECDSA) key algorithms and can extract:
- Certificate data (-----BEGIN CERTIFICATE-----/-----END CERTIFICATE-----)
- Public keys (-----BEGIN PUBLIC KEY-----/-----END PUBLIC KEY-----)
- Private keys (-----BEGIN PRIVATE KEY-----/-----END PRIVATE KEY-----)
The function is particularly useful for:
- Setting up certificate-based authentication for OpenSSH servers and clients
- Exporting certificates for use with Linux/Unix systems
- Creating CA trust chains for SSH host or user certificate authentication
- Converting Windows certificates for cross-platform applications
- Preparing certificates for web servers, APIs, or other TLS-enabled services
Certificates can be specified by serial number, thumbprint, or passed directly as X509Certificate2 objects.
When exporting, individual PEM files are created for each component, and optionally a combined CA bundle file
can be generated containing all certificates for trust chain scenarios.
.PARAMETER SerialNumberList
One or more certificate serial numbers to locate and convert. The function searches all certificate stores
(LocalMachine and CurrentUser) to find matching certificates. Serial numbers are case-insensitive.
This parameter belongs to the 'BySerialNumber' parameter set.
.PARAMETER ThumbprintList
One or more certificate thumbprints (SHA-1 hash) to locate and convert. The function searches all certificate
stores to find matching certificates. Thumbprints are 40 hexadecimal characters and are case-insensitive.
This parameter belongs to the 'ByThumbprint' parameter set.
.PARAMETER CertificateList
One or more X509Certificate2 objects to convert. Use this when you already have certificate objects in memory,
such as from Get-ChildItem Cert:\ or New-SelfSignedCertificate.
This parameter belongs to the 'ByCertificate' parameter set.
.PARAMETER Export
When specified, exports the PEM data to files on disk. Without this switch, only the in-memory PEM blobs
are returned in the output object. Each certificate's components are exported to a subdirectory named
by serial number under the ExportRootDirectory.
.PARAMETER ExportRootDirectory
The root directory where exported PEM files will be saved. Each certificate creates a subdirectory
named by its serial number containing Certificate.pem, PublicKey.pem, and PrivateKey.pem files.
Defaults to '$Env:Public\Documents\ConvertFrom-CertificateToPEM' if not specified.
.PARAMETER CreateCABundle
When specified along with -Export, creates a combined CA bundle file (ca-bundle.pem) containing all
processed certificates. This is useful for creating trust chains for OpenSSH TrustedUserCAKeys or
TrustedHostCAKeys configuration, or for other applications that need a single file with multiple
CA certificates.
.PARAMETER CABundleFileName
The filename for the combined CA bundle when -CreateCABundle is specified.
Defaults to 'ca-bundle.pem' if not specified.
.PARAMETER IncludePrivateKey
When specified, attempts to extract and export the private key if the certificate has an exportable
private key. By default, private keys are NOT exported for security reasons.
For OpenSSH certificate authentication, private keys are typically NOT needed on the server side -
only the CA's public certificate is required for TrustedUserCAKeys or TrustedHostCAKeys.
Only use this switch when you specifically need the private key, such as:
- Backing up a CA certificate with its private key
- Migrating certificates to another system
- Setting up client-side identity keys
.PARAMETER ContinueOnError
When specified, continues processing remaining certificates if an error occurs with one certificate.
Without this switch, the first error terminates the function.
.EXAMPLE
Export a certificate by serial number for use with OpenSSH user authentication.
$ConvertFromCertificateToPEMParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConvertFromCertificateToPEMParameters.SerialNumberList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$ConvertFromCertificateToPEMParameters.SerialNumberList.Add('3d05e1f73beb5d3cd1be2b9f8d96f6c323260f78')
$ConvertFromCertificateToPEMParameters.Export = $True
$ConvertFromCertificateToPEMParameters.ExportRootDirectory = "$($Env:Userprofile)\Downloads\Certificates"
$ConvertFromCertificateToPEMParameters.ContinueOnError = $False
$ConvertFromCertificateToPEMParameters.Verbose = $True
$ConvertFromCertificateToPEMResult = ConvertFrom-CertificateToPEM @ConvertFromCertificateToPEMParameters
Write-Output -InputObject ($ConvertFromCertificateToPEMResult)
.EXAMPLE
Export a certificate by thumbprint.
$ConvertFromCertificateToPEMParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConvertFromCertificateToPEMParameters.ThumbprintList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$ConvertFromCertificateToPEMParameters.ThumbprintList.Add('A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2')
$ConvertFromCertificateToPEMParameters.Export = $True
$ConvertFromCertificateToPEMParameters.ExportRootDirectory = "$($Env:ProgramData)\OpenSSH\Certificates"
$ConvertFromCertificateToPEMParameters.ContinueOnError = $False
$ConvertFromCertificateToPEMParameters.Verbose = $True
$ConvertFromCertificateToPEMResult = ConvertFrom-CertificateToPEM @ConvertFromCertificateToPEMParameters
Write-Output -InputObject ($ConvertFromCertificateToPEMResult)
.EXAMPLE
Create a self-signed CA certificate and export it for OpenSSH TrustedUserCAKeys configuration.
[String]$RootCALocation = 'Cert:\LocalMachine\My'
[String]$RootCAFriendlyName = 'SSH User CA Certificate'
$DNSHostEntry = [System.Net.Dns]::GetHostEntry('LocalHost')
$NewSelfSignedCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$NewSelfSignedCertificateParameters.FriendlyName = $RootCAFriendlyName
$NewSelfSignedCertificateParameters.Subject = "CN=SSH User CA"
$NewSelfSignedCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$NewSelfSignedCertificateParameters.DnsName.Add($DNSHostEntry.HostName)
$NewSelfSignedCertificateParameters.NotBefore = (Get-Date).Date
$NewSelfSignedCertificateParameters.NotAfter = $NewSelfSignedCertificateParameters.NotBefore.Date.AddYears(10)
$NewSelfSignedCertificateParameters.CertStoreLocation = $RootCALocation
$NewSelfSignedCertificateParameters.KeyExportPolicy = New-Object -TypeName 'System.Collections.Generic.List[Microsoft.Certificateservices.Commands.Keyexportpolicy]'
$NewSelfSignedCertificateParameters.KeyExportPolicy.Add('Exportable')
$NewSelfSignedCertificateParameters.Provider = 'Microsoft Enhanced RSA and AES Cryptographic Provider'
$NewSelfSignedCertificateParameters.TextExtension = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$NewSelfSignedCertificateParameters.TextExtension.Add('2.5.29.19={text}CA=1&pathlength=3')
$NewSelfSignedCertificateParameters.KeyUsageProperty = New-Object -TypeName 'System.Collections.Generic.List[Microsoft.Certificateservices.Commands.Keyusageproperty]'
$NewSelfSignedCertificateParameters.KeyUsageProperty.Add('All')
$NewSelfSignedCertificateParameters.KeyUsage = New-Object -TypeName 'System.Collections.Generic.List[Microsoft.Certificateservices.Commands.Keyusage]'
$NewSelfSignedCertificateParameters.KeyUsage.Add('CertSign')
$NewSelfSignedCertificateParameters.KeyUsage.Add('CRLSign')
$NewSelfSignedCertificateParameters.KeyUsage.Add('DigitalSignature')
$NewSelfSignedCertificateParameters.Confirm = $False
$SelfSignedCertificate = New-SelfSignedCertificate @NewSelfSignedCertificateParameters
$ConvertFromCertificateToPEMParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConvertFromCertificateToPEMParameters.CertificateList = New-Object -TypeName 'System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]'
$ConvertFromCertificateToPEMParameters.CertificateList.Add($SelfSignedCertificate)
$ConvertFromCertificateToPEMParameters.Export = $True
$ConvertFromCertificateToPEMParameters.ExportRootDirectory = "$($Env:ProgramData)\ssh"
$ConvertFromCertificateToPEMParameters.ContinueOnError = $False
$ConvertFromCertificateToPEMParameters.Verbose = $True
$ConvertFromCertificateToPEMResult = ConvertFrom-CertificateToPEM @ConvertFromCertificateToPEMParameters
# The Certificate.pem file can be used in sshd_config as:
# TrustedUserCAKeys C:\ProgramData\ssh\<SerialNumber>\Certificate.pem
Write-Output -InputObject ($ConvertFromCertificateToPEMResult)
.EXAMPLE
Export all unexpired Root CAs and Intermediate CAs (Sub-CAs) to the machine SSH trust location without private keys.
This is the recommended approach for configuring OpenSSH TrustedUserCAKeys or TrustedHostCAKeys.
# Collect all unexpired CA certificates from both Root and Intermediate CA stores
$CACertificateList = New-Object -TypeName 'System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]'
# Get unexpired Root CAs (Cert:\LocalMachine\Root)
$RootCAs = Get-ChildItem -Path 'Cert:\LocalMachine\Root' | Where-Object { ($_.NotAfter -gt (Get-Date)) -and ($_.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Basic Constraints' -and $_.CertificateAuthority }) }
$CACertificateList.AddRange([System.Security.Cryptography.X509Certificates.X509Certificate2[]]$RootCAs)
# Get unexpired Intermediate/Sub CAs (Cert:\LocalMachine\CA)
$IntermediateCAs = Get-ChildItem -Path 'Cert:\LocalMachine\CA' | Where-Object { ($_.NotAfter -gt (Get-Date)) -and ($_.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Basic Constraints' -and $_.CertificateAuthority }) }
$CACertificateList.AddRange([System.Security.Cryptography.X509Certificates.X509Certificate2[]]$IntermediateCAs)
# Create CA bundle in the standard OpenSSH machine configuration directory
# Note: -Export is NOT specified, so individual certificate files are NOT created (only the bundle)
# Note: -IncludePrivateKey is NOT specified, so only public certificates are exported (secure default)
$ConvertFromCertificateToPEMParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConvertFromCertificateToPEMParameters.CertificateList = $CACertificateList
$ConvertFromCertificateToPEMParameters.ExportRootDirectory = "$($Env:ProgramData)\ssh"
$ConvertFromCertificateToPEMParameters.CreateCABundle = $True
$ConvertFromCertificateToPEMParameters.CABundleFileName = 'OpenSSH-TrustedCertificateAuthorities-LocalMachine.pem'
$ConvertFromCertificateToPEMParameters.ContinueOnError = $False
$ConvertFromCertificateToPEMParameters.Verbose = $True
$ConvertFromCertificateToPEMResult = ConvertFrom-CertificateToPEM @ConvertFromCertificateToPEMParameters
# The combined CA bundle can be referenced in sshd_config as:
# TrustedUserCAKeys C:\ProgramData\ssh\OpenSSH-TrustedCertificateAuthorities-LocalMachine.pem
#
# For host certificate verification:
# TrustedHostCAKeys C:\ProgramData\ssh\OpenSSH-TrustedCertificateAuthorities-LocalMachine.pem
Write-Output -InputObject ($ConvertFromCertificateToPEMResult)
.NOTES
OpenSSH Certificate Authentication:
- For TrustedUserCAKeys: Export the CA certificate and reference the Certificate.pem file
- For TrustedHostCAKeys: Same approach for host certificate verification
- The CA's private key is NOT needed on the SSH server - only the public certificate
- The CA bundle option (-CreateCABundle) is useful when multiple CAs can sign user/host certificates
- Machine trust location on Windows: $Env:ProgramData\ssh (typically C:\ProgramData\ssh)
Certificate Expiration and Validity:
- OpenSSH does NOT accept expired certificates for authentication
- Certificates must have valid NotBefore and NotAfter dates at the time of authentication
- If a CA certificate expires, all user/host certificates signed by that CA become invalid
- Plan certificate renewal before expiration to avoid authentication failures
- Use the output properties NotBefore and NotAfter to monitor certificate validity
Private Key Security:
- By default, private keys are NOT exported for security reasons
- Use -IncludePrivateKey only when you specifically need to export private keys
- Private keys must be marked as exportable in the certificate store to be extracted
- Access to private keys may require administrative privileges
- Keep private keys secure and never expose them unnecessarily
Key Algorithm Support:
- RSA: Fully supported for certificate, public key, and private key extraction
- ECC/ECDSA: Fully supported for certificate, public key, and private key extraction
- Other algorithms may have limited support
PEM File Encoding:
- All PEM files are written with ASCII encoding for maximum compatibility
- Line endings use the platform default (CRLF on Windows)
.LINK
https://man.openbsd.org/sshd_config#TrustedUserCAKeys
.LINK
https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement
.LINK
https://www.rfc-editor.org/rfc/rfc7468 (Textual Encodings of PKIX, PKCS, and CMS Structures)
#>
[CmdletBinding(ConfirmImpact = 'Low', DefaultParameterSetName = 'BySerialNumber')]
Param
(
[Parameter(Mandatory=$True, ParameterSetName = 'BySerialNumber')]
[ValidateNotNullOrEmpty()]
[String[]]$SerialNumberList,
[Parameter(Mandatory=$True, ParameterSetName = 'ByThumbprint')]
[ValidateNotNullOrEmpty()]
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
[String[]]$ThumbprintList,
[Parameter(Mandatory=$True, ParameterSetName = 'ByCertificate')]
[ValidateNotNullOrEmpty()]
[System.Security.Cryptography.X509Certificates.X509Certificate2[]]$CertificateList,
[Parameter(Mandatory=$False)]
[Switch]$Export,
[Parameter(Mandatory=$False)]
[AllowEmptyString()]
[AllowNull()]
[System.IO.DirectoryInfo]$ExportRootDirectory,
[Parameter(Mandatory=$False)]
[Switch]$CreateCABundle,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[String]$CABundleFileName,
[Parameter(Mandatory=$False)]
[Switch]$IncludePrivateKey,
[Parameter(Mandatory=$False)]
[Switch]$ContinueOnError
)
Begin
{
Try
{
$DateTimeLogFormat = 'dddd, MMMM dd, yyyy @ hh:mm:ss.FFF tt' ###Monday, January 01, 2019 @ 10:15:34.000 AM###
[ScriptBlock]$GetCurrentDateTimeLogFormat = {(Get-Date).ToString($DateTimeLogFormat)}
$DateTimeMessageFormat = 'MM/dd/yyyy HH:mm:ss.FFF' ###03/23/2022 11:12:48.347###
[ScriptBlock]$GetCurrentDateTimeMessageFormat = {(Get-Date).ToString($DateTimeMessageFormat)}
$DateFileFormat = 'yyyyMMdd' ###20190403###
[ScriptBlock]$GetCurrentDateFileFormat = {(Get-Date).ToString($DateFileFormat)}
$DateTimeFileFormat = 'yyyyMMdd_HHmmss' ###20190403_115354###
[ScriptBlock]$GetCurrentDateTimeFileFormat = {(Get-Date).ToString($DateTimeFileFormat)}
$TextInfo = (Get-Culture).TextInfo
$LoggingDetails = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$LoggingDetails.Add('LogMessage', $Null)
$LoggingDetails.Add('WarningMessage', $Null)
$LoggingDetails.Add('ErrorMessage', $Null)
$CommonParameterList = New-Object -TypeName 'System.Collections.Generic.List[String]'
$CommonParameterList.AddRange([System.Management.Automation.PSCmdlet]::CommonParameters)
$CommonParameterList.AddRange([System.Management.Automation.PSCmdlet]::OptionalCommonParameters)
[ScriptBlock]$ErrorHandlingDefinition = {
Param
(
[Int16]$Severity,
[Boolean]$ContinueOnError
)
$ExceptionPropertyDictionary = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ExceptionPropertyDictionary.Message = $_.Exception.Message
$ExceptionPropertyDictionary.Category = $_.Exception.ErrorRecord.FullyQualifiedErrorID
$ExceptionPropertyDictionary.Script = [System.IO.Path]::GetFileName($_.InvocationInfo.ScriptName)
$ExceptionPropertyDictionary.LineNumber = $_.InvocationInfo.ScriptLineNumber
$ExceptionPropertyDictionary.LinePosition = $_.InvocationInfo.OffsetInLine
$ExceptionPropertyDictionary.Code = $_.InvocationInfo.Line.Trim()
$ExceptionMessageList = New-Object -TypeName 'System.Collections.Generic.List[String]'
ForEach ($ExceptionProperty In $ExceptionPropertyDictionary.GetEnumerator())
{
$ExceptionMessageList.Add("[$($ExceptionProperty.Key): $($ExceptionProperty.Value)]")
}
$LogMessageParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$LogMessageParameters.Message = $ExceptionMessageList -Join ' '
$LogMessageParameters.Verbose = $True
Switch ($Severity)
{
{($_ -in @(1))} {Write-Verbose @LogMessageParameters}
{($_ -in @(2))} {Write-Warning @LogMessageParameters}
{($_ -in @(3))} {Write-Error @LogMessageParameters}
}
Switch ($ContinueOnError)
{
{($_ -eq $False)}
{
Throw
}
}
}
#Determine the date and time we executed the function
$FunctionStartTime = (Get-Date)
[String]$FunctionName = $MyInvocation.MyCommand
[System.IO.FileInfo]$InvokingScriptPath = $MyInvocation.PSCommandPath
[System.IO.DirectoryInfo]$InvokingScriptDirectory = $InvokingScriptPath.Directory.FullName
[System.IO.FileInfo]$FunctionPath = "$($InvokingScriptDirectory.FullName)\Functions\$($FunctionName).ps1"
[System.IO.DirectoryInfo]$FunctionDirectory = "$($FunctionPath.Directory.FullName)"
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Function `'$($FunctionName)`' is beginning. Please Wait..."
Write-Verbose -Message ($LoggingDetails.LogMessage) -Verbose
#Define Default Action Preferences
$ErrorActionPreference = 'Stop'
[String[]]$AvailableScriptParameters = (Get-Command -Name ($FunctionName)).Parameters.GetEnumerator() | Where-Object {($_.Value.Name -inotin $CommonParameterList)} | ForEach-Object {"-$($_.Value.Name):$($_.Value.ParameterType.Name)"}
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Available Function Parameter(s) = $($AvailableScriptParameters -Join ', ')"
Write-Verbose -Message ($LoggingDetails.LogMessage) -Verbose
[String[]]$SuppliedScriptParameters = $PSBoundParameters.GetEnumerator() | ForEach-Object {Try {"-$($_.Key):$($_.Value.GetType().Name)"} Catch {"-$($_.Key):Unknown"}}
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Supplied Function Parameter(s) = $($SuppliedScriptParameters -Join ', ')"
Write-Verbose -Message ($LoggingDetails.LogMessage) -Verbose
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Execution of $($FunctionName) began on $($FunctionStartTime.ToString($DateTimeLogFormat))"
Write-Verbose -Message ($LoggingDetails.LogMessage) -Verbose
#Create an object that will contain the functions output.
$OutputObjectList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
#Set default parameter value(s)
Switch ($True)
{
{([String]::IsNullOrEmpty($ExportRootDirectory) -eq $True) -or ([String]::IsNullOrWhiteSpace($ExportRootDirectory) -eq $True)}
{
[System.IO.DirectoryInfo]$ExportRootDirectory = "$($Env:Public)\Documents\$($FunctionName)"
}
{([String]::IsNullOrEmpty($CABundleFileName) -eq $True) -or ([String]::IsNullOrWhiteSpace($CABundleFileName) -eq $True)}
{
[String]$CABundleFileName = 'ca-bundle.pem'
}
}
$X509Certificate2List = New-Object -TypeName 'System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]'
#List to collect all certificate PEM blobs for CA bundle creation
$CABundleCertificateBlobs = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
Switch ($PSCmdlet.ParameterSetName)
{
{($_ -iin @('BySerialNumber'))}
{
$ExistingCertificateList = Get-ChildItem -Path @('Microsoft.PowerShell.Security\Certificate::\') -Recurse -Force -ErrorAction SilentlyContinue | Where-Object {($Null -ine $_.SerialNumber)}
ForEach ($SerialNumber In $SerialNumberList)
{
Switch ($ExistingCertificateList.SerialNumber -icontains $SerialNumber)
{
{($_ -eq $True)}
{
$ExistingCertificates = $ExistingCertificateList | Where-Object {($_.SerialNumber -ieq $SerialNumber)} | Sort-Object -Property @('NotAfter') -Descending -Unique
$ExistingCertificateCount = ($ExistingCertificates | Measure-Object).Count
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Located $($ExistingCertificateCount) certificate(s) for serial number `"$($SerialNumber)`"."
Write-Verbose -Message ($LoggingDetails.LogMessage)
Switch ($ExistingCertificateCount)
{
{($_ -eq 1)}
{
$X509Certificate2List.Add($ExistingCertificates)
}
{($_ -gt 1)}
{
ForEach ($ExistingCertificate In $ExistingCertificates)
{
$X509Certificate2List.Add($ExistingCertificate)
}
}
}
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - No certificate found with serial number `"$($SerialNumber)`"."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
}
}
{($_ -iin @('ByThumbprint'))}
{
$ExistingCertificateList = Get-ChildItem -Path @('Microsoft.PowerShell.Security\Certificate::\') -Recurse -Force -ErrorAction SilentlyContinue | Where-Object {($Null -ine $_.Thumbprint)}
ForEach ($Thumbprint In $ThumbprintList)
{
Switch ($ExistingCertificateList.Thumbprint -icontains $Thumbprint)
{
{($_ -eq $True)}
{
$ExistingCertificates = $ExistingCertificateList | Where-Object {($_.Thumbprint -ieq $Thumbprint)} | Sort-Object -Property @('NotAfter') -Descending -Unique
$ExistingCertificateCount = ($ExistingCertificates | Measure-Object).Count
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Located $($ExistingCertificateCount) certificate(s) for thumbprint `"$($Thumbprint)`"."
Write-Verbose -Message ($LoggingDetails.LogMessage)
Switch ($ExistingCertificateCount)
{
{($_ -eq 1)}
{
$X509Certificate2List.Add($ExistingCertificates)
}
{($_ -gt 1)}
{
ForEach ($ExistingCertificate In $ExistingCertificates)
{
$X509Certificate2List.Add($ExistingCertificate)
}
}
}
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - No certificate found with thumbprint `"$($Thumbprint)`"."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
}
}
{($_ -iin @('ByCertificate'))}
{
$CertificateList | Sort-Object -Property @('NotAfter') -Descending -Unique | ForEach-Object {($X509Certificate2List.Add($_))}
}
}
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Total certificates to process: $($X509Certificate2List.Count)"
Write-Verbose -Message ($LoggingDetails.LogMessage)
}
Catch
{
$ErrorHandlingDefinition.Invoke(2, $ContinueOnError.IsPresent)
}
Finally
{
}
}
Process
{
Try
{
$X509Certificate2ListCounter = 1
$X509Certificate2ListCount = ($X509Certificate2List | Measure-Object).Count
For ($X509Certificate2ListIndex = 0; $X509Certificate2ListIndex -lt $X509Certificate2ListCount; $X509Certificate2ListIndex++)
{
Try
{
$X509Certificate2ListItem = $X509Certificate2List[$X509Certificate2ListIndex]
$OutputObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$OutputObjectProperties.SerialNumber = $X509Certificate2ListItem.SerialNumber
$OutputObjectProperties.Thumbprint = $X509Certificate2ListItem.Thumbprint
$OutputObjectProperties.Subject = $X509Certificate2ListItem.Subject
$OutputObjectProperties.Issuer = $X509Certificate2ListItem.Issuer
$OutputObjectProperties.NotBefore = $X509Certificate2ListItem.NotBefore
$OutputObjectProperties.NotAfter = $X509Certificate2ListItem.NotAfter
$OutputObjectProperties.KeyAlgorithm = $Null
$OutputObjectProperties.HasPrivateKey = $X509Certificate2ListItem.HasPrivateKey
$OutputObjectProperties.Blobs = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$OutputObjectProperties.Blobs.Certificate = $Null
$OutputObjectProperties.Blobs.PublicKey = $Null
$OutputObjectProperties.Blobs.PrivateKey = $Null
$OutputObjectProperties.Exports = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$OutputObjectProperties.Exports.Certificate = $Null
$OutputObjectProperties.Exports.PublicKey = $Null
$OutputObjectProperties.Exports.PrivateKey = $Null
$OutputObjectProperties.Certificate = $X509Certificate2ListItem
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Attempting to process certificate `"$($X509Certificate2ListItem.SerialNumber)`". Please Wait..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Issuer: $($X509Certificate2ListItem.Issuer)"
Write-Verbose -Message ($LoggingDetails.LogMessage)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Thumbprint: $($X509Certificate2ListItem.Thumbprint)"
Write-Verbose -Message ($LoggingDetails.LogMessage)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Serial Number: $($X509Certificate2ListItem.SerialNumber)"
Write-Verbose -Message ($LoggingDetails.LogMessage)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - DNS Name List: $($X509Certificate2ListItem.DnsNameList -Join ', ')"
Write-Verbose -Message ($LoggingDetails.LogMessage)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Not Before: $($X509Certificate2ListItem.NotBefore.ToString($DateTimeLogFormat))"
Write-Verbose -Message ($LoggingDetails.LogMessage)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Not After: $($X509Certificate2ListItem.NotAfter.ToString($DateTimeLogFormat))"
Write-Verbose -Message ($LoggingDetails.LogMessage)
$StringBuilders = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$StringBuilders.Certificate = New-Object -TypeName 'System.Text.StringBuilder'
$StringBuilders.PrivateKey = New-Object -TypeName 'System.Text.StringBuilder'
$StringBuilders.PublicKey = New-Object -TypeName 'System.Text.StringBuilder'
$X509KeyStorageFlags = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$X509KeyStorageFlags.Valid = [System.Enum]::GetNames('System.Security.Cryptography.X509Certificates.X509KeyStorageFlags') | Sort-Object
$X509KeyStorageFlags.Specified = New-Object -TypeName 'System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'
$X509KeyStorageFlags.Specified.Add('Exportable')
$X509Certificate2 = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$X509Certificate2.Object = $X509Certificate2ListItem
$X509Certificate2.ObjectEncoded = $Null
$X509Certificate2.ContentType = $Null
$X509Certificate2.OID = $Null
$X509Certificate2.DSA = $Null
$X509Certificate2.CNG = $Null
$X509Certificate2.PublicKey = $Null
$X509Certificate2.PublicKeyEncoded = $Null
$X509Certificate2.PrivateKey = $Null
$X509Certificate2.PrivateKeyEncoded = $Null
$X509Certificate2.ContentType = [System.Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($X509Certificate2.Object.RawData)
$X509Certificate2.ObjectEncoded = [System.Convert]::ToBase64String($X509Certificate2.Object.RawData, [System.Base64FormattingOptions]::InsertLineBreaks)
$Null = $StringBuilders.Certificate.Append('-----BEGIN CERTIFICATE-----').AppendLine()
$Null = $StringBuilders.Certificate.Append($X509Certificate2.ObjectEncoded).AppendLine()
$Null = $StringBuilders.Certificate.Append('-----END CERTIFICATE-----')
$OutputObjectProperties.Blobs.Certificate = $StringBuilders.Certificate.ToString()
#Add to CA bundle collection for combined export
$CABundleCertificateBlobs.Add($OutputObjectProperties.Blobs.Certificate)
$X509Certificate2.OID = [System.Security.Cryptography.OID]::New($X509Certificate2.Object.GetKeyAlgorithm())
$OutputObjectProperties.KeyAlgorithm = $X509Certificate2.OID.FriendlyName
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - A certificate key algorithm of `"$($X509Certificate2.OID.FriendlyName)`" [OID: $($X509Certificate2.OID.Value)] was detected."
Write-Verbose -Message ($LoggingDetails.LogMessage)
Switch ($Null -ine $X509Certificate2.Object.PublicKey)
{
{($_ -eq $True)}
{
Try
{
Switch ($X509Certificate2.OID.FriendlyName)
{
{($_ -iin @('RSA'))}
{
$X509Certificate2.DSA = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($X509Certificate2.Object)
$X509Certificate2.CNG = [System.Security.Cryptography.RSACNG]($X509Certificate2.DSA)
}
{($_ -iin @('ECC'))}
{
$X509Certificate2.DSA = [System.Security.Cryptography.X509Certificates.ECDSACertificateExtensions]::GetECDSAPublicKey($X509Certificate2.Object)
$X509Certificate2.CNG = [System.Security.Cryptography.ECDSACNG]($X509Certificate2.DSA)
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - The certificate key algorithm of `"$($X509Certificate2.OID.FriendlyName)`" [OID: $($X509Certificate2.OID.Value)] is unsupported for public key blob extraction."
Write-Verbose -Message ($LoggingDetails.LogMessage)
}
}
Switch ($Null -ine $X509Certificate2.CNG)
{
{($_ -eq $True)}
{
$X509Certificate2.PublicKey = $X509Certificate2.CNG.Key
$X509Certificate2.PublicKeyEncoded = [System.Convert]::ToBase64String(($X509Certificate2.PublicKey.Export([System.Security.Cryptography.CngKeyBlobFormat]::GenericPublicBlob)), [System.Base64FormattingOptions]::InsertLineBreaks)
$Null = $StringBuilders.PublicKey.Append('-----BEGIN PUBLIC KEY-----').AppendLine()
$Null = $StringBuilders.PublicKey.Append($X509Certificate2.PublicKeyEncoded).AppendLine()
$Null = $StringBuilders.PublicKey.Append('-----END PUBLIC KEY-----')
$OutputObjectProperties.Blobs.PublicKey = $StringBuilders.PublicKey.ToString()
}
}
}
Catch
{
#Note: Some certificates use RSABCrypt or other non-CNG implementations that cannot be cast to RSACng/ECDsaCng.
#The certificate PEM blob is still valid - only the separate public key blob extraction fails.
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Public key blob extraction skipped (key implementation: $($X509Certificate2.DSA.GetType().Name)). Certificate PEM is still valid."
Write-Verbose -Message ($LoggingDetails.LogMessage)
}
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - The certificate `"$($X509Certificate2ListItem.SerialNumber)`" [Thumbprint: $($X509Certificate2ListItem.Thumbprint)] does not contain a public key."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
#region Private Key Extraction (only if -IncludePrivateKey is specified)
Switch ($IncludePrivateKey.IsPresent)
{
{($_ -eq $True)}
{
Switch ($X509Certificate2.Object.HasPrivateKey)
{
{($_ -eq $True)}
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Attempting to extract private key (as requested by -IncludePrivateKey)..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
Try
{
Switch ($X509Certificate2.OID.FriendlyName)
{
{($_ -iin @('RSA'))}
{
$X509Certificate2.DSA = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($X509Certificate2.Object)
$X509Certificate2.CNG = [System.Security.Cryptography.RSACNG]($X509Certificate2.DSA)
}
{($_ -iin @('ECC'))}
{
$X509Certificate2.DSA = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDSAPrivateKey($X509Certificate2.Object)
$X509Certificate2.CNG = [System.Security.Cryptography.ECDSACNG]($X509Certificate2.DSA)
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - The certificate key algorithm of `"$($X509Certificate2.OID.FriendlyName)`" [OID: $($X509Certificate2.OID.Value)] is unsupported for private key extraction."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
Switch ($Null -ine $X509Certificate2.CNG)
{
{($_ -eq $True)}
{
$X509Certificate2.PrivateKey = $X509Certificate2.CNG.Key
$X509Certificate2.PrivateKeyEncoded = [System.Convert]::ToBase64String(($X509Certificate2.PrivateKey.Export([System.Security.Cryptography.CngKeyBlobFormat]::PKCS8PrivateBlob)), [System.Base64FormattingOptions]::InsertLineBreaks)
$Null = $StringBuilders.PrivateKey.Append('-----BEGIN PRIVATE KEY-----').AppendLine()
$Null = $StringBuilders.PrivateKey.Append($X509Certificate2.PrivateKeyEncoded).AppendLine()
$Null = $StringBuilders.PrivateKey.Append('-----END PRIVATE KEY-----')
$OutputObjectProperties.Blobs.PrivateKey = $StringBuilders.PrivateKey.ToString()
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Private key successfully extracted."
Write-Verbose -Message ($LoggingDetails.LogMessage)
}
}
}
Catch
{
#Note: Some certificates use RSABCrypt or other non-CNG implementations that cannot be cast to RSACng/ECDsaCng.
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Private key extraction failed (key implementation: $($X509Certificate2.DSA.GetType().Name)). This key type does not support CNG export."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - The certificate `"$($X509Certificate2ListItem.SerialNumber)`" [Thumbprint: $($X509Certificate2ListItem.Thumbprint)] does not contain a private key."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Skipping private key extraction (use -IncludePrivateKey to export private keys)."
Write-Verbose -Message ($LoggingDetails.LogMessage)
}
}
#endregion
$WriteAllTextParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$WriteAllTextParameters.Path = $Null
$WriteAllTextParameters.Contents = $Null
$WriteAllTextParameters.Encoding = [System.Text.Encoding]::ASCII
Switch ($Export.IsPresent)
{
{($_ -eq $True)}
{
$X509Certificate2.ExportDirectory = "$($ExportRootDirectory.FullName)\$($X509Certificate2ListItem.SerialNumber)" -As [System.IO.DirectoryInfo]
Switch (([String]::IsNullOrEmpty($OutputObjectProperties.Blobs.Certificate) -eq $False) -or ([String]::IsNullOrWhiteSpace($OutputObjectProperties.Blobs.Certificate) -eq $False))
{
{($_ -eq $True)}
{
Switch ($True)
{
{([System.IO.Directory]::Exists($X509Certificate2.ExportDirectory.FullName) -eq $False)}
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Attempting to create directory `"$($X509Certificate2.ExportDirectory.FullName)`". Please Wait..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
$Null = [System.IO.Directory]::CreateDirectory($X509Certificate2.ExportDirectory.FullName)
}
}
$WriteAllTextParameters.Path = "$($X509Certificate2.ExportDirectory.FullName)\Certificate.pem"
$WriteAllTextParameters.Contents = $OutputObjectProperties.Blobs.Certificate
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Attempting to export the certificate to `"$($WriteAllTextParameters.Path)`". Please Wait..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
$Null = [System.IO.File]::WriteAllText($WriteAllTextParameters.Path, $WriteAllTextParameters.Contents, $WriteAllTextParameters.Encoding)
Switch ($?)
{
{($_ -eq $True)}
{
$OutputObjectProperties.Exports.Certificate = $WriteAllTextParameters.Path
}
}
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Unable to export the certificate from `"$($X509Certificate2ListItem.SerialNumber)`" [Thumbprint: $($X509Certificate2ListItem.Thumbprint)] to `"$($WriteAllTextParameters.Path)`"."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
Switch (([String]::IsNullOrEmpty($OutputObjectProperties.Blobs.PublicKey) -eq $False) -or ([String]::IsNullOrWhiteSpace($OutputObjectProperties.Blobs.PublicKey) -eq $False))
{
{($_ -eq $True)}
{
$WriteAllTextParameters.Path = "$($X509Certificate2.ExportDirectory.FullName)\PublicKey.pem"
$WriteAllTextParameters.Contents = $OutputObjectProperties.Blobs.PublicKey
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Attempting to export the public key to `"$($WriteAllTextParameters.Path)`". Please Wait..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
$Null = [System.IO.File]::WriteAllText($WriteAllTextParameters.Path, $WriteAllTextParameters.Contents, $WriteAllTextParameters.Encoding)
Switch ($?)
{
{($_ -eq $True)}
{
$OutputObjectProperties.Exports.PublicKey = $WriteAllTextParameters.Path
}
}
}
Default
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Unable to export the public key from certificate `"$($X509Certificate2ListItem.SerialNumber)`" [Thumbprint: $($X509Certificate2ListItem.Thumbprint)] to `"$($WriteAllTextParameters.Path)`"."
Write-Warning -Message ($LoggingDetails.LogMessage)
}
}
#Only export private key if -IncludePrivateKey was specified and blob was successfully extracted
Switch (($IncludePrivateKey.IsPresent) -and (([String]::IsNullOrEmpty($OutputObjectProperties.Blobs.PrivateKey) -eq $False) -or ([String]::IsNullOrWhiteSpace($OutputObjectProperties.Blobs.PrivateKey) -eq $False)))
{
{($_ -eq $True)}
{
$WriteAllTextParameters.Path = "$($X509Certificate2.ExportDirectory.FullName)\PrivateKey.pem"
$WriteAllTextParameters.Contents = $OutputObjectProperties.Blobs.PrivateKey
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Attempting to export the private key to `"$($WriteAllTextParameters.Path)`". Please Wait..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
$Null = [System.IO.File]::WriteAllText($WriteAllTextParameters.Path, $WriteAllTextParameters.Contents, $WriteAllTextParameters.Encoding)
Switch ($?)
{
{($_ -eq $True)}
{
$OutputObjectProperties.Exports.PrivateKey = $WriteAllTextParameters.Path
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Private key exported successfully."
Write-Verbose -Message ($LoggingDetails.LogMessage)
}
}
}
}
}
}
}
Catch
{
$ErrorHandlingDefinition.Invoke(2, $True)
}
Finally
{
$OutputObject = New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($OutputObjectProperties)
$OutputObjectList.Add($OutputObject)
$X509Certificate2ListCounter++
}
}
}
Catch
{
$ErrorHandlingDefinition.Invoke(2, $ContinueOnError.IsPresent)
}
Finally
{
}
}
End
{
Try
{
#region Create CA Bundle if requested
Switch (($Export.IsPresent) -and ($CreateCABundle.IsPresent) -and ($CABundleCertificateBlobs.Count -gt 0))
{
{($_ -eq $True)}
{
$CABundlePath = "$($ExportRootDirectory.FullName)\$($CABundleFileName)" -As [System.IO.FileInfo]
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Creating CA bundle with $($CABundleCertificateBlobs.Count) certificate(s)..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
#Ensure export directory exists
Switch ([System.IO.Directory]::Exists($ExportRootDirectory.FullName))
{
{($_ -eq $False)}
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Creating directory `"$($ExportRootDirectory.FullName)`"..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
$Null = [System.IO.Directory]::CreateDirectory($ExportRootDirectory.FullName)
}
}
#Combine all certificate blobs with line breaks between them
$CABundleContent = $CABundleCertificateBlobs -Join "`r`n`r`n"
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Attempting to export CA bundle to `"$($CABundlePath.FullName)`"..."
Write-Verbose -Message ($LoggingDetails.LogMessage)
$Null = [System.IO.File]::WriteAllText($CABundlePath.FullName, $CABundleContent, [System.Text.Encoding]::ASCII)
Switch ($?)
{
{($_ -eq $True)}
{
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - CA bundle successfully exported to `"$($CABundlePath.FullName)`"."
Write-Verbose -Message ($LoggingDetails.LogMessage)
#Add CA bundle info to output
$CABundleOutputProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$CABundleOutputProperties.Type = 'CABundle'
$CABundleOutputProperties.Path = $CABundlePath.FullName
$CABundleOutputProperties.CertificateCount = $CABundleCertificateBlobs.Count
$CABundleOutputProperties.Content = $CABundleContent
$CABundleOutput = New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($CABundleOutputProperties)
$OutputObjectList.Add($CABundleOutput)
}
}
}
}
#endregion
#Determine the date and time the function completed execution
$FunctionEndTime = (Get-Date)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Execution of $($FunctionName) ended on $($FunctionEndTime.ToString($DateTimeLogFormat))"
Write-Verbose -Message ($LoggingDetails.LogMessage) -Verbose
#Log the total script execution time
$FunctionExecutionTimespan = New-TimeSpan -Start ($FunctionStartTime) -End ($FunctionEndTime)
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Function execution took $($FunctionExecutionTimespan.Hours.ToString()) hour(s), $($FunctionExecutionTimespan.Minutes.ToString()) minute(s), $($FunctionExecutionTimespan.Seconds.ToString()) second(s), and $($FunctionExecutionTimespan.Milliseconds.ToString()) millisecond(s)"
Write-Verbose -Message ($LoggingDetails.LogMessage) -Verbose
$LoggingDetails.LogMessage = "$($GetCurrentDateTimeMessageFormat.Invoke()) - Function `'$($FunctionName)`' is completed."
Write-Verbose -Message ($LoggingDetails.LogMessage) -Verbose
}
Catch
{
$ErrorHandlingDefinition.Invoke(2, $ContinueOnError.IsPresent)
}
Finally
{
#Write the object to the powershell pipeline
$OutputObjectList = $OutputObjectList.ToArray()
Write-Output -InputObject ($OutputObjectList)
}
}
}
#endregion