An updated version of the script is published in this blog post: How to convert PEM to PFX in PowerShell (revisited)

Hello again. Continuing the previous post: How to join certificate and private key to a PKCS#12(PFX) file I'll talk a bit more about certutil.exe and openssl.exe private key formats and it differences. Let's start:

  • OpenSSL is big-endian by a nature, Microsoft CryptoAPI — little-endian;
  • OpenSSL uses ASN.1 structures, but Microsoft CryptoAPI — unmanaged C++-like structures.

Here is a structure type definition for PKCS#1 private key structure:

RSAPrivateKey ::= SEQUENCE {
	version Version,
	modulus INTEGER, -- n
	publicExponent INTEGER, -- e
	privateExponent INTEGER, -- d
	prime1 INTEGER, -- p
	prime2 INTEGER, -- q
	exponent1 INTEGER, -- d mod (p-1)
	exponent2 INTEGER, -- d mod (q-1)
	coefficient INTEGER, -- (inverse of q) mod p
	otherPrimeInfos OtherPrimeInfos OPTIONAL
}

Also it may use PKCS#8 structure (as per RFC 5208):

PrivateKeyInfo ::= SEQUENCE {
   version Version,
   privateKeyAlgorithm AlgorithmIdentifier {{PrivateKeyAlgorithms}},
   privateKey PrivateKey,
   attributes [0] Attributes OPTIONAL }

Version ::= INTEGER {v1(0)} (v1,...)

PrivateKey ::= OCTET STRING

Attributes ::= SET OF Attribute

Microsoft CryptoAPI uses different structures (as per Private Key BLOBs):

BLOBHEADER blobheader;
RSAPUBKEY rsapubkey;
BYTE modulus[rsapubkey.bitlen/8];
BYTE prime1[rsapubkey.bitlen/16];
BYTE prime2[rsapubkey.bitlen/16];
BYTE exponent1[rsapubkey.bitlen/16];
BYTE exponent2[rsapubkey.bitlen/16];
BYTE coefficient[rsapubkey.bitlen/16];
BYTE privateExponent[rsapubkey.bitlen/8];

typedef struct _PUBLICKEYSTRUC {
  BYTE   bType;
  BYTE   bVersion;
  WORD   reserved;
  ALG_ID aiKeyAlg;
} BLOBHEADER, PUBLICKEYSTRUC;

typedef struct _RSAPUBKEY {
  DWORD magic;
  DWORD bitlen;
  DWORD pubexp;
} RSAPUBKEY;

As you can see these structures are really different and you need to use multiple tools for the same task. To address this issue I wrote a simple converter that will convert PKCS#1 or PKCS#8 private key structures to a CryptoAPI structures. Here is a code:

function Convert-OpenSSLPrivateKey {
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$InputPath,
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$OutputPath
    )
    $File = Get-Item $InputPath -Force -ErrorAction Stop
    if ($PSBoundParameters.Debug) {$DebugPreference = "continue"}
    function Get-ASNLength ($RawData, $offset) {
        $return = "" | Select FullLength, Padding, LengthBytes, PayLoadLength
        if ($RawData[$offset + 1] -lt 128) {
            $return.lengthbytes = 1
            $return.Padding = 0
            $return.PayLoadLength = $RawData[$offset + 1]
            $return.FullLength = $return.Padding + $return.lengthbytes + $return.PayLoadLength + 1
        } else {
            $return.lengthbytes = $RawData[$offset + 1] - 128
            $return.Padding = 1
            $lengthstring = -join ($RawData[($offset + 2)..($offset + 1 + $return.lengthbytes)] | %{"{0:x2}" -f $_})
            $return.PayLoadLength = Invoke-Expression 0x$($lengthstring)
            $return.FullLength = $return.Padding + $return.lengthbytes + $return.PayLoadLength + 1
        }
        $return
    }

    function Get-NormalizedArray ($array) {
        $padding = $array.Length % 8
        if ($padding) {
            $array = $array[$padding..($array.Length - 1)]
        }
        [array]::Reverse($array)
        [Byte[]]$array
    }
    # parse content
    $Text = [IO.File]::ReadAllText($File)
    Write-Debug "Extracting certificate information..."
    if ($Text -match "(?msx).*-{5}BEGIN\sCERTIFICATE-{5}(.+)-{5}END\sCERTIFICATE-{5}") {
        $RawData = [Convert]::FromBase64String($Matches[1])
        try {$Cert = New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(,$RawData)}
        catch {Write-Warning "The data is invalid."; return}
        Write-Debug "X.509 certificate is correct."
    } else {Write-Warning "Missing certificate file."; return}
    if ($Text -match "(?msx).*-{5}BEGIN\sPRIVATE\sKEY-{5}(.+)-{5}END\sPRIVATE\sKEY-{5}") {
        Write-Debug "Processing Private Key module."
        $Bytes = [Convert]::FromBase64String($matches[1])
        if ($Bytes[0] -eq 48) {Write-Debug "Starting asn.1 decoding."}
        else {Write-Warning "The data is invalid."; return}
        $offset = 0
        # main sequence
        Write-Debug "Process outer Sequence tag."
        $return = Get-ASNLength $Bytes $offset
        Write-Debug "outer Sequence length is $($return.PayloadLength) bytes."
        $offset += $return.FullLength - $return.PayloadLength
        Write-Debug "New offset is: $offset"
        # zero integer
        Write-Debug "Process zero byte"
        $return = Get-ASNLength $Bytes $offset
        Write-Debug "outer zero byte length is $($return.PayloadLength) bytes."
        $offset += $return.FullLength
        Write-Debug "New offset is: $offset"
        # algorithm identifier
        Write-Debug "Proess algorithm identifier"
        $return = Get-ASNLength $Bytes $offset
        Write-Debug "Algorithm identifier length is $($return.PayloadLength) bytes."
        $offset += $return.FullLength
        Write-Debug "New offset is: $offset"
        # octet string
        $return = Get-ASNLength $Bytes $offset
        Write-Debug "Private key octet string length is $($return.PayloadLength) bytes."
        $offset += $return.FullLength - $return.PayLoadLength
        Write-Debug "New offset is: $offset"
    } elseif ($Text -match "(?msx).*-{5}BEGIN\sRSA\sPRIVATE\sKEY-{5}(.+)-{5}END\sRSA\sPRIVATE\sKEY-{5}") {
        Write-Debug "Processing RSA KEY module."
        $Bytes = [Convert]::FromBase64String($matches[1])
        if ($Bytes[0] -eq 48) {Write-Debug "Starting asn.1 decoding"}
        else {Write-Warning "The data is invalid"; return}
        $offset = 0
        Write-Debug "New offset is: $offset"
    }  else {Write-Warning "The data is invalid"; return}
    # private key sequence
    Write-Debug "Process private key sequence."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Private key length (including inner ASN.1 tags) is $($return.PayloadLength) bytes."
    $offset += $return.FullLength - $return.PayLoadLength
    Write-Debug "New offset is: $offset"
    # zero integer
    Write-Debug "Process zero byte"
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Zero byte length is $($return.PayloadLength) bytes."
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # modulus
    Write-Debug "Processing private key modulus."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Private key modulus length is $($return.PayloadLength) bytes."
    $modulus = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $modulus = Get-NormalizedArray $modulus
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # public exponent
    Write-Debug "Process private key public exponent."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Private key public exponent length is $($return.PayloadLength) bytes."
    Write-Debug "Private key public exponent padding is $(4 - $return.PayLoadLength) byte(s)."
    $padding = New-Object byte[] -ArgumentList (4 - $return.PayLoadLength)
    [Byte[]]$PublicExponent = $padding + $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # private exponent
    Write-Debug "Process private key private exponent."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Private key private exponent length is $($return.PayloadLength) bytes."
    $PrivateExponent = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $PrivateExponent = Get-NormalizedArray $PrivateExponent
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # prime1
    Write-Debug "Process Prime1."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Prime1 length is $($return.PayloadLength) bytes."
    $Prime1 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $Prime1 = Get-NormalizedArray $Prime1
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # prime2
    Write-Debug "Process Prime2."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Prime2 length is $($return.PayloadLength) bytes."
    $Prime2 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $Prime2 = Get-NormalizedArray $Prime2
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # exponent1
    Write-Debug "Process Exponent1."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Exponent1 length is $($return.PayloadLength) bytes."
    $Exponent1 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $Exponent1 = Get-NormalizedArray $Exponent1
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # exponent2
    Write-Debug "Process Exponent2."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Exponent2 length is $($return.PayloadLength) bytes."
    $Exponent2 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $Exponent2 = Get-NormalizedArray $Exponent2
    $offset += $return.FullLength
    Write-Debug "New offset is: $offset"
    # coefficient
    Write-Debug "Process Coefficient."
    $return = Get-ASNLength $Bytes $offset
    Write-Debug "Coeicient length is $($return.PayloadLength) bytes."
    $Coefficient = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
    $Coefficient = Get-NormalizedArray $Coefficient

    # creating Private Key BLOB structure
    Write-Debug "Calculating key length."
    $bitLen = "{0:X4}" -f $($modulus.Length * 8)
    Write-Debug "Key length is $($modulus.Length * 8) bits."
    [byte[]]$bitLen1 = iex 0x$([int]$bitLen.Substring(0,2))
    [byte[]]$bitLen2 = iex 0x$([int]$bitLen.Substring(2,2))
    [Byte[]]$PrivateKey = 0x07,0x02,0x00,0x00,0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x32,0x00
    [Byte[]]$PrivateKey = $PrivateKey + $bitLen1 + $bitLen2 + $PublicExponent + ,0x00 + `
    $modulus + $Prime1 + $Prime2 + $Exponent1 + $Exponent2 + $Coefficient + $PrivateExponent
    $Base = [Convert]::ToBase64String($PrivateKey)
    $TempFile = [IO.Path]::GetTempFileName()
    $CertFileName = $TempFile + ".cer"
    $KeyFileName = $TempFile + ".key"
    [IO.File]::WriteAllBytes($CertFileName, $Cert.RawData)
    Set-Content -Path $KeyFileName -Value $Base -Encoding Ascii
    certutil -f -MergePFX $CertFileName $OutputPath
    $TempFile, $CertFileName, $KeyFileName | %{del $_ -Force}
}

I'm using a custom ASN.1 parser to get required data. The following syntax should be used:

Convert-OpenSSLPrivateKey -InputPath path\file.pem -OutputPath path\file.pfx

PEM file must be in the following format:

-----BEGIN PRIVATE KEY-----
<some Base64 content>
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
<some Base64 content>
-----END CERTIFICATE-----

or

-----BEGIN RSA PRIVATE KEY-----
<some Base64 content>
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
<some Base64 content>
-----END CERTIFICATE-----

if you have separate files (.pem and .key) you need to ensure if key file is not already compatible for certutil by running the command:

[↓] [vPodans] certutil -dump .\Desktop\1.txt
Private Key:
  PRIVATEKEYBLOB
  Version: 2
  aiKeyAlg: 0x2400
    CALG_RSA_SIGN
    Algorithm Class: 0x2000(1) ALG_CLASS_SIGNATURE
    Algorithm Type: 0x400(2) ALG_TYPE_RSA
    Algorithm Sub-id: 0x0(0) ALG_SID_RSA_ANY
  0000  52 53 41 32                                        RSA2
  0000  ...
  024c
CertUtil: -dump command completed successfully.
[↓] [vPodans]

If your output is not like this you may try to use my converter. In this case you must manually combine certificate and key files into a single file following file structures above.

In the code I extract both private key and certificate contents. Only private key structures are processed. Once CryptoAPI-compatible structure is ready I copy certificate content to a random file in the current user's Temp (%temp%) folder. Private key is encoded in the Base64 and saved in the same Temp folder with .KEY extension (as required for certutil). If all is ok, certutil will ask you for a new password for output PFX file. Upon function completion (regardless if successfully or not) temporary files are deleted from the Temp folder.

In order to make this job better I would like to hear about script usage experience and feedback.


Share this article:

Comments:

Patrick Sczepanski

Hello Vadims Thank you for your great posts. Always enjoyed it and used a lot of your examples. Do you by any chance already have a script which is doing the oposite of this one? Splitting a pfx into PEM Files? Thank you for your help Best regards patrick -at- sczepanski -dot- com

Vadims Podans

Currently I don't have such script. But this is definitely possible with cryptography functions: http://msdn.microsoft.com/en-us/library/aa380252(VS.85).aspx

Patrick Sczepanski

Hello Vadims Thank you for your great posts. Always enjoyed it and used a lot of your examples. Do you by any chance already have a script which is doing the oposite of this one? Splitting a pfx into PEM Files? Thank you for your help Best regards patrick -at- sczepanski -dot- com

Patrick Sczepanski

Thank you for your feedback. Unfortunately I am good in PowerShell but not a devolper. I know I could use OpenSSL but it would have been handy to use a 'simple' script without installing additional software. Best regards Patrick

Vadims Podans

Further, this function is exactly what you need: http://msdn.microsoft.com/en-us/library/aa379931(VS.85).aspx use this function to export private key BLOB, then you may have to call CryptDecodeObject ( http://msdn.microsoft.com/en-us/library/aa379911(VS.85).aspx ) to decode structure to a right structure and copy decoded object to a structure by callling Marshal.PtrToStructure method. I haven't tested this, but it looks correct sequence.

Chris

Vadims, is there a way in PowerShell or C# that can be invoked within PowerShell, to combine the PEM and Private Key to PKCS12 format without needing certutil?

Vadims Podāns

Yes, it is possible. However this example will be too large for comment. I think, it worth a new blog post. If it is not very urgent, I'll write new blog post that will describe the process that uses pure .NET classes without bothering certutil.

Chris

I have most of what I need.  Just don't know how to convert the key to a RSA Crypto Blob so X509Certificate2 can accept it for the PrivateKey property of the cert object.

Vadims Podāns

Be patient. I'll write a blog post about this.

Chenling Zhang

I ran this script and get the variables like these:

[string]$bitLen = "0800"

[byte]$bitLen1 = 8

[byte]$bitLen2 = 0

[byte[]]$PublicExponent = 0,1,0,1

[Byte[]]$PrivateKey = 0x07,0x02,0x00,0x00,0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x32,0x00,0x08,0x00,0x00,0x01,0x00,0x01,0x00,...

Split above 20 bytes private key header into 5 4-byte segments, then the 3rd segment is magic number of RSA public key, the 4th is bit lengh and the 5th is public exponent.

However, it seems that the header was generated with a correct result but in a wrong way by below process:

[Byte[]]$PrivateKey = 0x07,0x02,0x00,0x00,0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x32,0x00+ $bitLen1 + $bitLen2 + $PublicExponent + ,0x00 + ...

Noticed that the lengh of $bitLen1 and $bitLen are both 1 byte, the 4th segment was concatenated with 1 byte from $PublicExponent and the 5th segment was padded with a zero byte in the end somehow.

This is weird. Should it be like this?:

[Byte[]]$PrivateKey = 0x07,0x02,0x00,0x00,0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x32, $bitLenIn4Bytes + $PublicExponentLittleEndian + ...

 

Chenling Zhang

Type incorrectly,

[byte]$bitLen1 = 8

[byte]$bitLen2 = 0

should be

[byte[]]$bitLen1 = 8

[byte[]]$bitLen2 = 0

 


Post your comment:

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