Hello folks! If you are longing for CryptoAPI stuff here, then you are on the right page. Here you will see another bunch of CryptoAPI, PowerShell and p/invoke hardcore.

Today’s subject is to convert PFX file to PEM format. A time ago I wrote a function that does opposite — converts PEM to PFX: How to convert PEM file to a CryptoAPI compatible format. Read this post to get information about CryptoAPI structures and ASN modules for PKCS#1 and PKCS#8 structures.

The script below performs the following tasks:

  1. Reads certificate or certificate file. If the file is not valid PFX or certificate hasn’t associated private key, an exception will be thrown.
  2. Acquires private key (via unmanaged function calls) and attempts to export raw private key from CSP. If the private key is not marked as exportable or it is stored on smart card, an error will be thrown.
  3. Inspects CryptoAPI private key blob as described here: RSA/Schannel Key BLOBs, removes header, reads raw private key and splits it to components (modulus, primes, exponents, coefficient). Each component is stored in separate variable.
  4. Generates required ASN structures according to output type by using basic ASN encoder.
  5. composes certificate and private key and saves them to file.

and here is the code:

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(Mandatory = $true, Position = 2)]
        [IO.FileInfo]$OutputFile,
        [Parameter(Position = 3)]
        [ValidateSet("Pkcs1","Pkcs8")]
        [string]$OutputType = "Pkcs1"
    )
$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
#region helper functions
    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
    }
#endregion

#region parameterset processing
    switch ($PsCmdlet.ParameterSetName) {
        "__pfxfile" {
            $bytes = [IO.File]::ReadAllBytes($InputFile)
            $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)) {
                $Certificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2
                try {$Certificate.Import($bytes,$Password,"Exportable")}
                catch {throw $_; return}
                finally {
                    [Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
                    Remove-Variable bytes, ptr, pfx -Force
                    [GC]::Collect()
                }
            } 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 do not contains associated private key." -ErrorAction Stop
            }
        }
    }
#endregion

#region private key export routine
    $phCryptProv = [IntPtr]::Zero
    $pdwKeySpec = 0
    $pfCallerFreeProv = $false
    $CRYPT_ACQUIRE_SILENT_FLAG = 0x00000040
    $CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG = 0x00010000
    $dwFlags = $CRYPT_ACQUIRE_SILENT_FLAG -bor $CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG
    # attempt to acquire private key container
    if ([PKI.PfxTools]::CryptAcquireCertificatePrivateKey($Certificate.Handle,$dwFlags,0,[ref]$phCryptProv,[ref]$pdwKeySpec,[ref]$pfCallerFreeProv)) {
        $phUserKey = [IntPtr]::Zero
        # attempt to acquire private key handle
        if ([PKI.PfxTools]::CryptGetUserKey($phCryptProv,$pdwKeySpec,[ref]$phUserKey)) {
            $pdwDataLen = 0
            # attempt to export private key. This method fails if certificate has non-exportable private key.
            if ([PKI.PfxTools]::CryptExportKey($phUserKey,0,0x7,0x40,$null,[ref]$pdwDataLen)) {
                $pbytes = New-Object byte[] -ArgumentList $pdwDataLen
                [void][PKI.PfxTools]::CryptExportKey($phUserKey,0,0x7,0x40,$pbytes,[ref]$pdwDataLen)
                # release private key handle
                [void][PKI.PfxTools]::CryptDestroyKey($phUserKey)
            } else {throw New-Object ComponentModel.Win32Exception ([Runtime.InteropServices.Marshal]::GetLastWin32Error())}
        } else {throw New-Object ComponentModel.Win32Exception ([Runtime.InteropServices.Marshal]::GetLastWin32Error())}
    } else {throw New-Object ComponentModel.Win32Exception ([Runtime.InteropServices.Marshal]::GetLastWin32Error())}
#endregion

#region private key blob splitter
    # 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
    # $out variable just holds output file. The file will contain private key and public certificate
    # each will be enclosed with header and footer.
    $out = New-Object String[] -ArgumentList 6
    if ($OutputType -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
        $out[0] = "-----BEGIN PRIVATE KEY-----"
        $out[2] = "-----END PRIVATE KEY-----"
    } else {
        # not sure about this. As far as I remember, PKCS#1 require RSA identifier in the header.
        # PKCS#1 is an inner structure of PKCS#8 message, therefore no additional encodings are required.
        $out[0] = "-----BEGIN RSA PRIVATE KEY-----"
        $out[2] = "-----END RSA PRIVATE KEY-----"
    }
    $out[1] = [Convert]::ToBase64String($asnblob,"InsertLineBreaks")
    $out[3] = "-----BEGIN CERTIFICATE-----"
    $out[4] = [Convert]::ToBase64String($Certificate.RawData,"InsertLineBreaks")
    $out[5] = "-----END CERTIFICATE-----"
    [IO.File]::WriteAllLines($OutputFile,$out)
#endregion
}

I added comments in the code to describe the most important process steps. The function accepts either existing X509Certificate2 object (for example, certificate in the local store) or PFX file. If you are using PFX file, a password is required. Generally you must specify input certificate and output path. Also you are allowed to specify output format for private key: PKCS#1 or PKCS#8.

Tip: as you can see in the code, PKCS#1 is a part of PKCS#8.

Here are some useful examples:

$cert = dir Cert:\CurrentUser\My\45D9D6C1D63CD765B2959FF49BCFC16AC9493854
Convert-PfxToPem -Certificate $cert -OutputFile C:\Temp\cert.pem -OutputType Pkcs8

In this example we read certificate from Personal store and convert it to PEM. Private key is encoded in PKCS#8.

$pass = Read-Host "Password: " -AsSecureString
Convert-PfxToPem -InputFile C:\Temp\cert.pfx -Password $pass -OutputFile C:\Temp\cert.pem

In this example we point the function to PFX file, provide password to decrypt PFX and convert it to PEM. Private key is encoded in PKCS#1.

certain applications require separate files for certificate and private key. In this case, you can open resulting PEM file and copy appropriate part (including header and footer) to separate file.


Share this article:

Comments:


Post your comment:

Please, solve this little equation and enter result below. Captcha