Functions/Certificate/Export-CertificatePrivateKey.ps1
|
<# .SYNOPSIS Exports the private key of a certificate. .DESCRIPTION This function allows you to export the private key of a certificate to a specified file path. The private key can be exported in different formats such as PKCS#1 (RSA private key) or PKCS#8 (private key). The following overview should give a short introduction about the private key file formats: - PKCS#1 Private Key (-----BEGIN RSA PRIVATE KEY-----) Contains the private part of a single RSA key in the Base64 encoded PKCS#1 format. Used primary on Linux systems. - PKCS#8 Private Key (-----BEGIN PRIVATE KEY-----) Contains the private part of a key in the Base64 encoded PKCS#8 format. Compared to the PKCS#1 format, this format can contain keys of different algorithms. Used primary on Linux systems. .INPUTS System.Security.Cryptography.X509Certificates.X509Certificate2 .EXAMPLE PS C:\> Export-CertificatePrivateKey -InPath $certFile This will export the private key of the certificate file to the pipeline returning in the default PKCS#8 format. .EXAMPLE PS C:\> $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new('cert.pfx', '***', 'Exportable') PS C:\> Export-CertificatePrivateKey -Certificate $cert -OutFormat 'PKCS#1' -OutPath '\privatekey.pem' This will export the private key of the certificate to the specified file path in the PKCS#1 format. .LINK https://github.com/claudiospizzi/SecurityFever #> function Export-CertificatePrivateKey { [CmdletBinding()] param ( # The certificate file from which to export the private key. [Parameter(Mandatory = $true, ParameterSetName = 'File')] [Alias('In', 'InFile')] [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })] [System.String] $InPath, # Password to read the input file. [Parameter(Mandatory = $true, ParameterSetName = 'File')] [System.Security.SecureString] $InPassword, # The certificate from which to export the private key. [Parameter(Mandatory = $true, ParameterSetName = 'Certificate')] [Alias('Cert', 'Certificate', 'X509Certificate', 'X509Certificate2')] [System.Security.Cryptography.X509Certificates.X509Certificate2] $InCertificate, # The path to the output file where the private key will be exported. [Parameter(Mandatory = $true)] [Alias('Out', 'OutFile')] [System.String] $OutPath, # Format of the output private key file. # - PKCS#1 or RsaPrivateKey: Base64 PEM encoded PKCS#1 private key # - PKCS#8 or PrivateKey: Base64 PEM encoded PKCS#8 private key [ValidateSet('PKCS#1', 'RsaPrivateKey', 'PKCS#8', 'PrivateKey')] [System.String] $OutFormat = 'PKCS#8' ) # Resolve the path to the actual provider path e.g. for TestDrive:\ in # Pester or other relative paths and PSDrives. The Resolve-Path cmdlet will # throw if the file does not exist. if ($PSCmdlet.ParameterSetName -eq 'File') { $InPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($InPath) } $OutPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutPath) # Import the certificate from the specified file path if it was specified by # using the -InPath and -InPassword parameters. if ($PSCmdlet.ParameterSetName -eq 'File') { $InCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($InPath, $InPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) } # Check if the certificate has a private key and if the private key is not # null. Give a hint for Windows PowerShell 5.1 users using Get-Item. if (-not $InCertificate.HasPrivateKey) { throw "The certificate does not have a private key." } if ($null -eq $InCertificate.PrivateKey) { if ($PSVersionTable.PSVersion.Major -le 5) { throw "The certificate's private key is null. Hint: On Windows PowerShell 5.1, the private key is not accessible when getting the certificate using `Get-Item -Path 'Cert:\CurrentUser\My\<Thumbprint>'`." } else { throw "The certificate's private key is null." } } # Depending on the .NET version, the RSA based private key is represented by # one of the following types: # - System.Security.Cryptography.RSACryptoServiceProvider # This is the legacy type used in .NET Framework and .NET Core 2.x. # - System.Security.Cryptography.RSAOpenSsl # This is the type used in .NET Core 3.x and .NET 5+ on Linux systems. # - System.Security.Cryptography.RSACng # This is the modern .NET alternative to replace the old type. if ($InCertificate.PrivateKey -isnot [System.Security.Cryptography.RSACryptoServiceProvider] -and $InCertificate.PrivateKey -isnot [System.Security.Cryptography.RSAOpenSsl] -and $InCertificate.PrivateKey -isnot [System.Security.Cryptography.RSACng]) { throw "The certificate's private key is not an RSA key: [$($InCertificate.PrivateKey.GetType().FullName)]. Only RSA private keys are supported." } ## Required?!? # $rsaPrivateKey = $InCertificate.PrivateKey -as [System.Security.Cryptography.RSACng] # if ($null -eq $rsaPrivateKey) # { # throw "The certificate's private key is not an RSA key or is not exportable." # } # if ($rsaPrivateKey.Key.ExportPolicy -notcontains [System.Security.Cryptography.CngExportPolicies]::AllowExport -and # $rsaPrivateKey.Key.ExportPolicy -notcontains [System.Security.Cryptography.CngExportPolicies]::AllowPlaintextExport) # { # throw "The certificate's private key is not marked as exportable." # } $rsaPrivateKey = $InCertificate.PrivateKey $rsaParameters = $rsaPrivateKey.ExportParameters($true) # The generation of the PKCS#1 is required for both the PKCS#1 and PKCS#8 # formats, because the PKCS#8 format contains the PKCS#1 format as a nested # structure. $pkcs1Bytes = @() $pkcs1Stream = [System.IO.MemoryStream]::new() try { $pkcs1Writer = [System.IO.BinaryWriter]::new($pkcs1Stream) try { $pkcs1Writer.Write([System.Byte]0x30) # SEQUENCE $pkcs1InnerStream = [System.IO.MemoryStream]::new() try { $pkcs1InnerWriter = [System.IO.BinaryWriter]::new($pkcs1InnerStream) try { $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value @([System.Byte]0x00) # VERSION $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.Modulus $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.Exponent $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.D $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.P $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.Q $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.DP $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.DQ $pkcs1InnerWriter | Write-CertificateDerIntegerBigEndian -Value $rsaParameters.InverseQ $pkcs1InnerLength = [System.Convert]::ToInt32($pkcs1InnerStream.Length) $pkcs1Writer | Write-CertificateDerLength -Length $pkcs1InnerLength $pkcs1Writer.Write($pkcs1InnerStream.GetBuffer(), 0, $pkcs1InnerLength) } finally { $pkcs1InnerWriter.Dispose() } } finally { $pkcs1InnerStream.Dispose() } $pkcs1Bytes = $pkcs1Stream.ToArray() } finally { $pkcs1Writer.Dispose() } } finally { $pkcs1Stream.Dispose() } # For the PKCS#1 format, we can directly write the Base64 encoded private # key to the specified output file path and exit. if ($OutFormat -in 'PKCS#1', 'RsaPrivateKey') { $pkcs1Base64 = ConvertTo-CertificateBase64 -Type 'RSA PRIVATE KEY' -Byte $pkcs1Bytes [System.IO.File]::WriteAllText($OutPath, $pkcs1Base64) } # For the PKCS#8 format, we need to wrap the PKCS#1 private key in a PKCS#8 # structure. The PKCS#8 format is a generic format that can contain private # keys of different algorithms. The PKCS#8 structure contains the PKCS#1 # structure as a nested structure. if ($OutFormat -in 'PKCS#8', 'PrivateKey') { $pkcs8Bytes = @() $pkcs8Stream = [System.IO.MemoryStream]::new() try { $pkcs8Writer = [System.IO.BinaryWriter]::new($pkcs8Stream) try { $pkcs8Writer.Write([System.Byte]0x30) # SEQUENCE $pkcs8InnerStream = [System.IO.MemoryStream]::new() try { $pkcs8InnerWriter = [System.IO.BinaryWriter]::new($pkcs8InnerStream) try { $pkcs8InnerWriter | Write-CertificateDerIntegerBigEndian -Value @([System.Byte]0x00) # VERSION $pkcs8InnerWriter.Write([System.Byte]0x30) # SEQUENCE $pkcs8InnerWriter | Write-CertificateDerLength -Length 13 $pkcs8InnerWriter.Write([System.Byte]0x06) # OBJECT IDENTIFIER $pkcs8InnerWriter | Write-CertificateDerLength -Length 9 $pkcs8InnerWriter.Write(@([System.Byte]0x2A, [System.Byte]0x86, [System.Byte]0x48, [System.Byte]0x86, [System.Byte]0xF7, [System.Byte]0x0D, [System.Byte]0x01, [System.Byte]0x01, [System.Byte]0x01), 0, 9) $pkcs8InnerWriter.Write([System.Byte]0x05) # NULL $pkcs8InnerWriter | Write-CertificateDerLength -Length 0 $pkcs8InnerWriter.Write([System.Byte]0x04) # OCTET STRING $pkcs8InnerWriter | Write-CertificateDerLength -Length $pkcs1Bytes.Length $pkcs8InnerWriter.Write($pkcs1Bytes, 0, $pkcs1Bytes.Length) $pkcs8InnerLength = [System.Convert]::ToInt32($pkcs8InnerStream.Length) $pkcs8Writer | Write-CertificateDerLength -Length $pkcs8InnerLength $pkcs8Writer.Write($pkcs8InnerStream.GetBuffer(), 0, $pkcs8InnerLength) } finally { $pkcs8InnerWriter.Dispose() } } finally { $pkcs8InnerStream.Dispose() } $pkcs8Bytes = $pkcs8Stream.ToArray() } finally { $pkcs8Writer.Dispose() } } finally { $pkcs8Stream.Dispose() } # Write the Base64 encoded PKCS#8 private key to the specified output # file path containing the PKCS#1 private key as a nested structure. $pkcs8Base64 = ConvertTo-CertificateBase64 -Type 'PRIVATE KEY' -Byte $pkcs8Bytes [System.IO.File]::WriteAllText($OutPath, $pkcs8Base64) } } |