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:
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.
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
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
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
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
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.
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?
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.
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.
Be patient. I'll write a blog post about this.
Updated version: How to convert PEM to PFX in PowerShell (revisited)
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 + ...
Type incorrectly,
[byte]$bitLen1 = 8
[byte]$bitLen2 = 0
should be
[byte[]]$bitLen1 = 8
[byte[]]$bitLen2 = 0
Post your comment:
Comments: