Last active
January 10, 2022 17:56
-
-
Save rmbolger/d3f5ec63113e75a4097cca4e579976f0 to your computer and use it in GitHub Desktop.
Convert-PfxToPem.ps1
This file contains 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
# Adapted from Vadims Podāns' amazing work here. | |
# https://www.sysadmins.lv/blog-en/how-to-convert-pkcs12pfx-to-pem-format.aspx | |
# | |
# Also, if you need a more complete PKI PowerShell module, he has authored one here: | |
# https://github.com/Crypt32/PSPKI | |
# | |
# This version of the function includes a few fixes from the module's version of the | |
# function and changes up the output options so you get separate .crt/.key files by | |
# default named the same as the pfx file (or thumbprint if directly referencing a cert). | |
# -IncludeChain adds an additional -chain.pem. Relative paths are now | |
# supported as well. | |
function Convert-PfxToPem { | |
[CmdletBinding(DefaultParameterSetName = '__pfxfile')] | |
param( | |
[Parameter(Mandatory = $true, ParameterSetName = '__pfxfile', Position = 0)] | |
[IO.FileInfo]$InputFile, | |
[Parameter(Mandatory = $true, ParameterSetName = '__cert', Position = 0)] | |
[Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, | |
[Parameter(Mandatory = $true, ParameterSetName = '__pfxfile', Position = 1)] | |
[Security.SecureString]$Password, | |
[Parameter(Position = 2)] | |
[string]$OutFolder = '.\', | |
[Parameter(Position = 3)] | |
[string]$OutName, | |
[Parameter(Position = 4)] | |
[ValidateSet("Pkcs1","Pkcs8")] | |
[string]$OutType = "Pkcs8", | |
[Alias("chain")] | |
[switch]$IncludeChain | |
) | |
$signature = @" | |
[DllImport("crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)] | |
public static extern bool CryptAcquireCertificatePrivateKey( | |
IntPtr pCert, | |
uint dwFlags, | |
IntPtr pvReserved, | |
ref IntPtr phCryptProv, | |
ref uint pdwKeySpec, | |
ref bool pfCallerFreeProv | |
); | |
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] | |
public static extern bool CryptGetUserKey( | |
IntPtr hProv, | |
uint dwKeySpec, | |
ref IntPtr phUserKey | |
); | |
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] | |
public static extern bool CryptExportKey( | |
IntPtr hKey, | |
IntPtr hExpKey, | |
uint dwBlobType, | |
uint dwFlags, | |
byte[] pbData, | |
ref uint pdwDataLen | |
); | |
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] | |
public static extern bool CryptDestroyKey( | |
IntPtr hKey | |
); | |
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)] | |
public static extern bool PFXIsPFXBlob( | |
CRYPTOAPI_BLOB pPFX | |
); | |
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)] | |
public static extern bool PFXVerifyPassword( | |
CRYPTOAPI_BLOB pPFX, | |
[MarshalAs(UnmanagedType.LPWStr)] | |
string szPassword, | |
int dwFlags | |
); | |
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | |
public struct CRYPTOAPI_BLOB { | |
public int cbData; | |
public IntPtr pbData; | |
} | |
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | |
public struct PUBKEYBLOBHEADERS { | |
public byte bType; | |
public byte bVersion; | |
public short reserved; | |
public uint aiKeyAlg; | |
public uint magic; | |
public uint bitlen; | |
public uint pubexp; | |
} | |
"@ | |
Add-Type -MemberDefinition $signature -Namespace PKI -Name PfxTools | |
function Encode-ASN ([Byte[]]$RawData, [byte]$Tag) { | |
if ($RawData.Length -lt 128) { | |
$Tag, $RawData.Length + $RawData | |
} else { | |
$hexlength = "{0:x2}" -f $RawData.Length | |
if ($hexlength.Length % 2) {$hexlength = "0" + $hexlength} | |
$lengtbytes = @($hexlength -split "([a-f0-9]{2})" | Where-Object {$_} | ForEach-Object {[Convert]::ToByte($_,16)}) | |
$padding = $lengtbytes.Length + 128 | |
$Tag, $padding + $lengtbytes + $RawData | |
} | |
} | |
function Encode-Integer ([Byte[]]$RawData) { | |
# since CryptoAPI is little-endian by nature, we have to change byte ordering | |
# to big-endian. | |
[array]::Reverse($RawData) | |
# if high byte contains more than 7 bits, an extra zero byte is added | |
if ($RawData[0] -ge 128) {$RawData = ,0 + $RawData} | |
Encode-ASN $RawData 2 | |
} | |
switch ($PsCmdlet.ParameterSetName) { | |
"__pfxfile" { | |
$pfxFile = Get-ChildItem (Resolve-Path $InputFile.Name) | |
$bytes = [IO.File]::ReadAllBytes($pfxFile) | |
$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length) | |
[Runtime.InteropServices.Marshal]::Copy($bytes,0,$ptr,$bytes.Length) | |
$pfx = New-Object PKI.PfxTools+CRYPTOAPI_BLOB -Property @{ | |
cbData = $bytes.Length; | |
pbData = $ptr | |
} | |
# just check whether input file is valid PKCS#12/PFX file. | |
if ([PKI.PfxTools]::PFXIsPFXBlob($pfx)) { | |
$certs = New-Object Security.Cryptography.X509Certificates.X509Certificate2Collection | |
try { | |
$certs.Import( | |
$bytes, | |
[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)), | |
"Exportable" | |
) | |
$Certificate = ($certs | Where-Object {$_.HasPrivateKey})[0] | |
} catch { | |
throw $_ | |
return | |
} finally { | |
[Runtime.InteropServices.Marshal]::FreeHGlobal($ptr) | |
Remove-Variable bytes, ptr, pfx -Force | |
} | |
} else { | |
[Runtime.InteropServices.Marshal]::FreeHGlobal($ptr) | |
Remove-Variable bytes, ptr, pfx -Force | |
Write-Error -Category InvalidData -Message "Input file is not valid PKCS#12/PFX file." -ErrorAction Stop | |
} | |
} | |
"__cert" { | |
if (!$Certificate.HasPrivateKey) { | |
Write-Error -Category InvalidOperation -Message "Specified certificate object does not contain associated private key." -ErrorAction Stop | |
} | |
} | |
} | |
$CRYPT_ACQUIRE_SILENT_FLAG = 0x40 | |
$PRIVATEKEYBLOB = 0x7 | |
$CRYPT_OAEP = 0x40 | |
$phCryptProv = [IntPtr]::Zero | |
$pdwKeySpec = 0 | |
$pfCallerFreeProv = $false | |
# attempt to acquire private key container | |
if (![PKI.PfxTools]::CryptAcquireCertificatePrivateKey($Certificate.Handle,$CRYPT_ACQUIRE_SILENT_FLAG,0,[ref]$phCryptProv,[ref]$pdwKeySpec,[ref]$pfCallerFreeProv)) { | |
throw New-Object ComponentModel.Win32Exception ([Runtime.InteropServices.Marshal]::GetLastWin32Error()) | |
return | |
} | |
$phUserKey = [IntPtr]::Zero | |
# attempt to acquire private key handle | |
if (![PKI.PfxTools]::CryptGetUserKey($phCryptProv,$pdwKeySpec,[ref]$phUserKey)) { | |
throw New-Object ComponentModel.Win32Exception ([Runtime.InteropServices.Marshal]::GetLastWin32Error()) | |
return | |
} | |
$pdwDataLen = 0 | |
# attempt to export private key. This method fails if certificate has non-exportable private key. | |
if (![PKI.PfxTools]::CryptExportKey($phUserKey,0,$PRIVATEKEYBLOB,$CRYPT_OAEP,$null,[ref]$pdwDataLen)) { | |
throw New-Object ComponentModel.Win32Exception ([Runtime.InteropServices.Marshal]::GetLastWin32Error()) | |
return | |
} | |
$pbytes = New-Object byte[] -ArgumentList $pdwDataLen | |
[void][PKI.PfxTools]::CryptExportKey($phUserKey,0,$PRIVATEKEYBLOB,$CRYPT_OAEP,$pbytes,[ref]$pdwDataLen) | |
# release private key handle | |
[void][PKI.PfxTools]::CryptDestroyKey($phUserKey) | |
# extracting private key blob header. | |
$headerblob = $pbytes[0..19] | |
# extracting actual private key data exluding header. | |
$keyblob = $pbytes[20..($pbytes.Length - 1)] | |
Remove-Variable pbytes -Force | |
# public key structure header has fixed length: 20 bytes: http://msdn.microsoft.com/en-us/library/aa387689(VS.85).aspx | |
# copy header information to unmanaged memory and copy it to structure. | |
$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal(20) | |
[Runtime.InteropServices.Marshal]::Copy($headerblob,0,$ptr,20) | |
$header = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr,[Type][PKI.PfxTools+PUBKEYBLOBHEADERS]) | |
[Runtime.InteropServices.Marshal]::FreeHGlobal($ptr) | |
# extract public exponent from blob header and convert it to a byte array | |
$pubExponentHex = "{0:x2}" -f $header.pubexp | |
if ($pubExponentHex.Length % 2) {$pubExponentHex = "0" + $pubExponentHex} | |
$publicExponent = $pubExponentHex -split "([a-f0-9]{2})" | Where-Object {$_} | ForEach-Object {[Convert]::ToByte($_,16)} | |
# this object is created to reduce code size. This object has properties, where each property represents | |
# a part (component) of the private key and property value contains private key component length. | |
# 8 means that the length of the component is KeyLength / 8. Resulting length is measured in bytes. | |
# for details see private key structure description: http://msdn.microsoft.com/en-us/library/aa387689(VS.85).aspx | |
$obj = New-Object psobject -Property @{ | |
modulus = 8; privateExponent = 8; | |
prime1 = 16; prime2 = 16; exponent1 = 16; exponent2 = 16; coefficient = 16; | |
} | |
$offset = 0 | |
# I pass variable names (each name represents the component of the private key) to foreach loop | |
# in the order as they follow in the private key structure and parse private key for | |
# appropriate offsets and write component information to variable. | |
"modulus","prime1","prime2","exponent1","exponent2","coefficient","privateExponent" | ForEach-Object { | |
Set-Variable -Name $_ -Value ($keyblob[$offset..($offset + $header.bitlen / $obj.$_ - 1)]) | |
$offset = $offset + $header.bitlen / $obj.$_ | |
} | |
# PKCS#1/PKCS#8 uses slightly different component order, therefore I reorder private key | |
# components and pass them to a simplified ASN encoder. | |
$asnblob = Encode-Integer 0 | |
$asnblob += "modulus","publicExponent","privateExponent","prime1","prime2","exponent1","exponent2","coefficient" | ForEach-Object { | |
Encode-Integer (Get-Variable -Name $_).Value | |
} | |
# remove unused variables | |
Remove-Variable modulus,publicExponent,privateExponent,prime1,prime2,exponent1,exponent2,coefficient -Force | |
# encode resulting set of INTEGERs to a SEQUENCE | |
$asnblob = Encode-Asn $asnblob 48 | |
# build the encoded key | |
$key = New-Object Text.StringBuilder | |
if ($OutType -eq "Pkcs8") { | |
$asnblob = Encode-Asn $asnblob 4 | |
$algid = [Security.Cryptography.CryptoConfig]::EncodeOID("1.2.840.113549.1.1.1") + 5,0 | |
$algid = Encode-Asn $algid 48 | |
$asnblob = 2,1,0 + $algid + $asnblob | |
$asnblob = Encode-Asn $asnblob 48 | |
$base64 = [Convert]::ToBase64String($asnblob) | |
$base64 = $base64 -replace '(.{64}(?!$))',"`$1$([Environment]::NewLine)" | |
[void]$key.AppendLine("-----BEGIN PRIVATE KEY-----") | |
[void]$key.AppendLine($base64) | |
[void]$key.AppendLine("-----END PRIVATE KEY-----") | |
} else { | |
# PKCS#1 requires RSA identifier in the header. | |
# PKCS#1 is an inner structure of PKCS#8 message, therefore no additional encodings are required. | |
$base64 = [Convert]::ToBase64String($asnblob) | |
$base64 = $base64 -replace '(.{64}(?!$))',"`$1$([Environment]::NewLine)" | |
[void]$key.AppendLine("-----BEGIN RSA PRIVATE KEY-----") | |
[void]$key.AppendLine($base64) | |
[void]$key.AppendLine("-----END RSA PRIVATE KEY-----") | |
} | |
# build the encoded cert | |
$cert = New-Object Text.StringBuilder | |
$base64 = [Convert]::ToBase64String($Certificate.RawData) | |
$base64 = $base64 -replace '(.{64}(?!$))',"`$1$([Environment]::NewLine)" | |
[void]$cert.AppendLine("-----BEGIN CERTIFICATE-----") | |
[void]$cert.AppendLine($base64) | |
[void]$cert.AppendLine("-----END CERTIFICATE-----") | |
# build the encoded chain | |
if ($IncludeChain) { | |
$chainStr = New-Object Text.StringBuilder | |
$chain = New-Object Security.Cryptography.X509Certificates.X509Chain | |
$chain.ChainPolicy.RevocationMode = "NoCheck" | |
if ($certs) { | |
$chain.ChainPolicy.ExtraStore.AddRange($certs) | |
} | |
[void]$chain.Build($Certificate) | |
for ($n = 1; $n -lt $chain.ChainElements.Count; $n++) { | |
$base64 = [Convert]::ToBase64String($chain.ChainElements[$n].Certificate.RawData) | |
$base64 = $base64 -replace '(.{64}(?!$))',"`$1$([Environment]::NewLine)" | |
[void]$chainStr.AppendLine("-----BEGIN CERTIFICATE-----") | |
[void]$chainStr.AppendLine($base64) | |
[void]$chainStr.AppendLine("-----END CERTIFICATE-----") | |
} | |
} | |
# build the output paths | |
$outPath = Resolve-Path $OutFolder | |
if (!$OutName) { | |
if ($PsCmdlet.ParameterSetName -eq '__pfxfile') { | |
$OutName = $pfxFile.BaseName | |
} else { | |
$OutName = $Certificate.Thumbprint | |
} | |
} | |
$certPath = Join-Path $outPath "$OutName.crt" | |
$keyPath = Join-Path $outPath "$OutName.key" | |
$chainPath = Join-Path $outPath "$OutName-chain.pem" | |
# write files as necessary | |
Write-Host "Writing $certPath" | |
$cert.ToString() | Out-File $certPath -Encoding ascii -NoNewline | |
Write-Host "Writing $keyPath" | |
$key.ToString() | Out-File $keyPath -Encoding ascii -NoNewline | |
if ($IncludeChain) { | |
Write-Host "Writing $chainPath" | |
$key.ToString() | Out-File $chainPath -Encoding ascii -NoNewline | |
$cert.ToString() | Out-File $chainPath -Append -Encoding ascii -NoNewline | |
$chainStr.ToString() | Out-File $chainPath -Append -Encoding ascii -NoNewline | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment