Hello everyone!

Some time ago I wrote a script that converts PEM file to CryptoAPI compatible format: How to convert PEM file to a CryptoAPI compatible format. The script involves some non-PowerShell commands (certutil) which associates private key with a certificate instance. I received several feedback comments about avoiding certutil in favor of native PowerShell/.NET managed code. In this post I want to show some code that eliminates certutil from the script.

Just to recall what we generally do when converting PEM to X509Certificate2/PFX:

  • Read the certificate information from PEM file and instantiate a X509Certificate2 object;
  • Read PKCS#1 or PKCS#8 private key;
  • Convert PKCS#1/PKCS#9 private key to CryptoAPI PRIVATEKEYBLOB;
  • Associate PRIVATEKYEBLOB with an X509Certificate2 instance.

In the first version of such converter I successfully done first three steps. I didn’t know how to do the last step natively by using PowerShell/.NET and used certutil –mergePFX command to associate PRIVATEKYEBLOB with public certificate.

Recently I figured that X509Certificate2.PrivateKey property has setter accessor. and the page provides lots of hints. As per documentation, the property accepts either an RSACryptoServiceProvider or a DSACryptoServiceProvider objects.

RSACryptoServiceProvider class contains a ImportCspBlob(Byte[]) method which does the trick. It accepts binary PRIVATEKEYBLOB as a parameter! However, the key must be stored in some crypto provider and must have a container name within provider. So, if we look at constructors, we can find a suitable one: RSACryptoServiceProvider(CspParameters). So, we need to prepare crypto provider information and use this info during key import.

What information we need to provide in the CspParameters object? At a minimum, we must specify:

  • Provider name. We can use any, but I would suggest to use Microsoft Enhanced RSA and AES Cryptographic Provider as it supports a wide range of keys and key sizes.
  • Key container name. This is just a container name within CSP, so CryptoAPI can locate the right key among thousands which can be stored within CSP. Name format doesn’t really matter, so to maintains a name uniqueness, you are allowed to use GUIDs. The key association code looks like this:
$cspParams = New-Object Security.Cryptography.CspParameters -Property @{
    ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
    KeyContainerName = "pspki-" + [Guid]::NewGuid().ToString()
    KeyNumber = $KeySpec # 1 - AT_EXCHANGE, 2 - AT_SIGNATURE
}
# if we want to install the certificate to local machine store specify appropriate flag
# there is no need to specify additional flags when installing to current user store.
if ($Install -and $StoreLocation -eq "LocalMachine") {
    $cspParams.Flags += [Security.Cryptography.CspProviderFlags]::UseMachineKeyStore
}
# construct RSACryptoServiceProvider from CSP information
$rsa = New-Object Security.Cryptography.RSACryptoServiceProvider $cspParams
# load PRIVATEKEYBLOB into CSP
$rsa.ImportCspBlob($PrivateKey)
# attach private key to certificate
$Cert.PrivateKey = $rsa

By default, the key is exportable, so you can do with it whatever you want. Export policy and other key settings are configured in the CspParameters.Flags property.

Eventually, I reworked the script to provide more flexibility. This includes the following features:

  • An ability to export PEM to PFX file
  • An ability to install the certificate to the certificate store without intermediate PFX file
  • Do both, install to the store and export to a file
  • Specify the provider name you wish to use (default is Microsoft Enhanced RSA and AES Cryptographic Provider).

New function requires only one parameter: path to a PEM file. The rest is optional and depends on your needs.

The function returns an instance of X509Certificate2 class for reference only. Private key object is disposed and cannot be used in this state.

And the full function code:

Add-Type @"
namespace System.Security.Cryptography.X509Certificates {
    public enum X509KeySpecFlags {
        None = 0,
        AT_KEYEXCHANGE = 1,
        AT_SIGNATURE = 2
    }
}
"@
function Convert-PemToPfx {
[OutputType('[System.Security.Cryptography.X509Certificates.X509Certificate2]')]
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$InputPath,
        [string]$KeyPath,
        [string]$OutputPath,
        [Security.Cryptography.X509Certificates.X509KeySpecFlags]$KeySpec = "AT_KEYEXCHANGE",
        [Security.SecureString]$Password,
        [string]$ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider",
        [Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation = "CurrentUser",
        [switch]$Install
    )
    if ($PSBoundParameters.Verbose) {$VerbosePreference = "continue"}
    if ($PSBoundParameters.Debug) {
        $Host.PrivateData.DebugForegroundColor = "Cyan"
        $DebugPreference = "continue"
    }

    #region helper functions
    function __normalizeAsnInteger ($array) {
        $padding = $array.Length % 8
        if ($padding) {
            $array = $array[$padding..($array.Length - 1)]
        }
        [array]::Reverse($array)
        [Byte[]]$array
    }
    function __extractCert([string]$Text) {
        if ($Text -match "(?msx).*-{5}BEGIN\sCERTIFICATE-{5}(.+)-{5}END\sCERTIFICATE-{5}") {
        $keyFlags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
        if ($Install) {
            if ($StoreLocation -eq "CurrentUser") {
               $keyFlags = $keyFlags -bor [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet
            } else {
               $keyFlags = $keyFlags -bor [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet
            }
        }
        $RawData = [Convert]::FromBase64String($matches[1])
            try {
                New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $RawData, "", $keyFlags
            } catch {throw "The data is not valid security certificate."}
            Write-Debug "X.509 certificate is correct."
        } else {throw "Missing certificate file."}
    }
    # returns [byte[]]
    function __composePRIVATEKEYBLOB($modulus, $PublicExponent, $PrivateExponent, $Prime1, $Prime2, $Exponent1, $Exponent2, $Coefficient) {
        Write-Debug "Calculating key length."
        $bitLen = "{0:X4}" -f $($modulus.Length * 8)
        Write-Debug "Key length is $($modulus.Length * 8) bits."
        [byte[]]$bitLen1 = Invoke-Expression 0x$([int]$bitLen.Substring(0,2))
        [byte[]]$bitLen2 = Invoke-Expression 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
        $PrivateKey
    }
    # returns RSACryptoServiceProvider for dispose purposes
    function __attachPrivateKey($Cert, [Byte[]]$PrivateKey) {
        $cspParams = New-Object Security.Cryptography.CspParameters -Property @{
            ProviderName = $ProviderName
            KeyContainerName = "pspki-" + [Guid]::NewGuid().ToString()
            KeyNumber = [int]$KeySpec
        }
        if ($Install -and $StoreLocation -eq "LocalMachine") {
            $cspParams.Flags = $cspParams.Flags -bor [Security.Cryptography.CspProviderFlags]::UseMachineKeyStore
        }
        $rsa = New-Object Security.Cryptography.RSACryptoServiceProvider $cspParams
        $rsa.ImportCspBlob($PrivateKey)
        $Cert.PrivateKey = $rsa
        $rsa
    }
    # returns Asn1Reader
    function __decodePkcs1($base64) {
        Write-Debug "Processing PKCS#1 RSA KEY module."
        $asn = New-Object SysadminsLV.Asn1Parser.Asn1Reader @(,[Convert]::FromBase64String($base64))
        if ($asn.Tag -ne 48) {throw "The data is invalid."}
        $asn
    }
    # returns Asn1Reader
    function __decodePkcs8($base64) {
        Write-Debug "Processing PKCS#8 Private Key module."
        $asn = New-Object SysadminsLV.Asn1Parser.Asn1Reader @(,[Convert]::FromBase64String($base64))
        if ($asn.Tag -ne 48) {throw "The data is invalid."}
        # version
        if (!$asn.MoveNext()) {throw "The data is invalid."}
        # algorithm identifier
        if (!$asn.MoveNext()) {throw "The data is invalid."}
        # octet string
        if (!$asn.MoveNextCurrentLevel()) {throw "The data is invalid."}
        if ($asn.Tag -ne 4) {throw "The data is invalid."}
        if (!$asn.MoveNext()) {throw "The data is invalid."}
        $asn
    }
    #endregion
    $ErrorActionPreference = "Stop"
    
    $File = Get-Item $InputPath -Force -ErrorAction Stop
    if ($KeyPath) {$Key = Get-Item $KeyPath -Force -ErrorAction Stop}
    
    # parse content
    $Text = Get-Content -Path $InputPath -Raw -ErrorAction Stop
    Write-Debug "Extracting certificate information..."
    $Cert = __extractCert $Text
    if ($Key) {$Text = Get-Content -Path $KeyPath -Raw -ErrorAction Stop}
    $asn = if ($Text -match "(?msx).*-{5}BEGIN\sPRIVATE\sKEY-{5}(.+)-{5}END\sPRIVATE\sKEY-{5}") {
        __decodePkcs8 $matches[1]
    } elseif ($Text -match "(?msx).*-{5}BEGIN\sRSA\sPRIVATE\sKEY-{5}(.+)-{5}END\sRSA\sPRIVATE\sKEY-{5}") {
        __decodePkcs1 $matches[1]
    }  else {throw "The data is invalid."}
    # private key version
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    # modulus n
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    $modulus = __normalizeAsnInteger $asn.GetPayload()
    Write-Debug "Modulus length: $($modulus.Length)"
    # public exponent e
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    # public exponent must be 4 bytes exactly.
    $PublicExponent = if ($asn.GetPayload().Length -eq 3) {
        ,0 + $asn.GetPayload()
    } else {
        $asn.GetPayload()
    }
    Write-Debug "PublicExponent length: $($PublicExponent.Length)"
    # private exponent d
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    $PrivateExponent = __normalizeAsnInteger $asn.GetPayload()
    Write-Debug "PrivateExponent length: $($PrivateExponent.Length)"
    # prime1 p
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    $Prime1 = __normalizeAsnInteger $asn.GetPayload()
    Write-Debug "Prime1 length: $($Prime1.Length)"
    # prime2 q
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    $Prime2 = __normalizeAsnInteger $asn.GetPayload()
    Write-Debug "Prime2 length: $($Prime2.Length)"
    # exponent1 d mod (p-1)
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    $Exponent1 = __normalizeAsnInteger $asn.GetPayload()
    Write-Debug "Exponent1 length: $($Exponent1.Length)"
    # exponent2 d mod (q-1)
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    $Exponent2 = __normalizeAsnInteger $asn.GetPayload()
    Write-Debug "Exponent2 length: $($Exponent2.Length)"
    # coefficient (inverse of q) mod p
    if (!$asn.MoveNext()) {throw "The data is invalid."}
    $Coefficient = __normalizeAsnInteger $asn.GetPayload()
    Write-Debug "Coefficient length: $($Coefficient.Length)"
    # creating Private Key BLOB structure
    $PrivateKey = __composePRIVATEKEYBLOB $modulus $PublicExponent $PrivateExponent $Prime1 $Prime2 $Exponent1 $Exponent2 $Coefficient
    #region key attach and export routine
    $rsaKey = __attachPrivateKey $Cert $PrivateKey
    if (![string]::IsNullOrEmpty($OutputPath)) {
        if (!$Password) {
            $Password = Read-Host -Prompt "Enter PFX password" -AsSecureString
        }
        $pfxBytes = $Cert.Export("pfx", $Password)
        Set-Content -Path $OutputPath -Value $pfxBytes -Encoding Byte
    }
    #endregion
    if ($Install) {
        $store = New-Object Security.Cryptography.X509Certificates.X509Store "my", $StoreLocation
        $store.Open("ReadWrite")
        $store.Add($Cert)
        $store.Close()
    }
    $rsaKey.Dispose()
    $Cert
}

The code relies on my new ASN.1 binary parser which is available at GitHub: Asn1DerParser.NET. Click on file and press “Raw” button to download the DLL.

HTH


Share this article:

Comments:

Aftab
Aftab 17.05.2016 08:26 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

I've downloaded the additional dll and unblocked the file, its in the same folder as the function in a ps1 file, however I keep get the following error:

Unable to find type [Security.Cryptography.X509Certificates.X509KeySpecFlags]

I've tried running PowerShell as admin but I don't think that is the issue, it can't seem to find the type on my system, I'm running Windows 10 if that makes a difference, as I've tried on a Server 2012 R2 box with the same results, any ideas?

Vadims Podāns
Vadims Podāns 18.05.2016 08:43 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

I updated the post. It was missing one custom enum type. Now it should work.

Carl Reid
Carl Reid 31.08.2016 15:36 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

I had the same problem as "Aftab" where I would receive the error:

Convert-PemToPfx : Unable to find type [Security.Cryptography.X509Certificates.X509KeySpecFlags]

 

I did a little digging into this and this error is because the type is defined in the PKI.Core assembly rather than .NET framework.

 

I fixed the error by first loading the assembly PKI.Core into the AppDomain.

 

After this error I got a simmilar one for the type SysadminsLV.Asn1Parser.Asn1Reader

New-Object : Cannot find type [SysadminsLV.Asn1Parser.Asn1Reader]: verify that the assembly containing this type is loaded.

 

Again this was fixed by first loading the assembly SysadminsLV.Asn1Parser into the AppDomain.

 

Perhaps the article could be updated to include these or is there a better way of fixing this problem.

Alexander
Alexander 20.09.2016 18:32 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

Is it possible / planned to add russian cryptographic algorithms (GOST 2012 / 2001)?

Vadims Podāns
Vadims Podāns 20.09.2016 19:52 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

There are no such plans. Currently, I'm planning to add ellyptic curve cryptography only.

Chris Lynch
Chris Lynch 16.10.2016 06:20 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

Thank you for researching and providing a solution for us CryptoAPI n00bs.

@Carl Reid, the script just needs to have the Add-Type and [System.Reflectin.Assymbly]::LoadFile() call to load the extension class at the top of the script.  Or in my case, I just replaced the Enum type variable with the integer I wanted, as I only needed the ability to create a PFX/Pkcs12 compliant file.

Ernest
Ernest 02.08.2017 15:26 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

Hello Vadmins

Thanks very much for posting, I am trying to use the script but I get an error. Before getting to the error I am trying to do something slightly different but close to what your script does

I have a PEM file for standard certificate e.g. -----BEGIN CERTIFICATE-----blah blah base64 blah blah----END CERTIFICATE----- this PEM has No private key information

I want to import the PEM into the CurrentUser\My store (or LocalMachine does not matter for my perposes) but importantly I want to use the "Microsoft Enhanced RSA and AES Cryptographic Provider"  if I import the certificate using the MMC or basic PowerShell it appears to end up in the CAPI store (as it were) and therefore I cannot use the certificates public key to check a SHA256 signature as this cryptographic primiative is not available to the CAPI store (as I understand it) but only to CAPI2 

I have gotten around this by using openssl on windows like so

openssl pkcs12 -export -in MyCert.crt -out MyCert.pfx -nodes -nokeys -CSP “Microsoft Enhanced RSA and AES Cryptographic Provider” -passout pass:secret

however I then need to import the PFX to my store, then once in the store I have to get back out again into a certifcate object so I can use the following method of the public key

$Certificate.PublicKey.Key.VerifyData([byte[]][char[]]$DataString, 'SHA256', $SignatureBytes)

basically checking the DataString is signed OK using (SHA256), this works OK but is a lot of work

I found if I just get the base64 certificate and convert it to a certificate object I do not have the PublicKey.Key.VerifyData method, so basically need to install into the store first then get back again, then I have the method, but for SHA265 need to get from the CAPI2 store so needs to be added to the CAPI2 store in the first place.

can you script take a PEM without a private key and add it to the CAPI2 store please?

when I run the script I first load your PSPKI module then I add the following like to the script     [System.Reflection.Assembly]::LoadFile("C:\unix\SysadminsLV.Asn1Parser.dll")

then I run the script and using the single parameter -inputPath e.g.

Convert-PemToPFX -InputPath C:\temp\MyCert.cer  

I get the following error

The data is invalid.
At C:\Unix\Convert.ps1:146 char:9
+     else { throw "The data is invalid." }
+            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (The data is invalid.:String) [], RuntimeException
    + FullyQualifiedErrorId : The data is invalid.

Looking at the PowerShell code in your script I believe it is throwing this error was my PEM does not contain a private key

the reason it does not contain a private key is because what I am really trying to do is just import a standard PEM to the CAPI2 store (Microsoft Enhanced RSA and AES Cryptographic Provider)

I would be very grateful for your advice, thanks in advance

Ernest

 

 

 

 

 

 

Vadims Podāns
Vadims Podāns 02.08.2017 18:14 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

Few notes:

> [System.Reflection.Assembly]::LoadFile("C:\unix\SysadminsLV.Asn1Parser.dll")

it is no longer supported in PowerShell. Instead, you have to use Add-Type cmdlet.

> CAPI2 store (Microsoft Enhanced RSA and AES Cryptographic Provider)

mentioned provider is still CAPI1 provider. CAPI2 providers are key storage providers. Your question is not related to this script, and the script won't help you.

What you can try is to try manually load public key to CngKey .NET class to load the key to CNG provider and then use this object to validate SHA2 signatures. Or call directly NCrypt functions via p/invoke.

Ernest
Ernest 02.08.2017 20:09 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

Hello Vadims

Thanks very much for taking the time to reply, I get the basic concept of what you are saying and will have a go at working it out.

In the mean time if you can have a think about how much you would charge to create a PowerShell function to take an input of a certificate [System.Security.Cryptography.X509Certificates.X509Certificate2]   (again I do not have the private key material) and allow the public key to be used to verify a SHA256 signature.

Then if I cannot figure I have a plan B e.g. ask my project manager to get the money to pay you via our supplier (as we did last time for the tuition) for the function

Thanks Vadmins 

Vadims Podāns
Vadims Podāns 02.08.2017 22:10 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

You can do it this way:

$key = [Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($cert)

This line will return public key loaded into CNG provider and you can use this object to verify the signature. More details: https://msdn.microsoft.com/en-us/library/system.security.cryptography.rsa(v=vs.110).aspx

Ernest
Ernest 02.08.2017 23:13 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

Hi Vadims

Thanks very much, I really appreciate you taking the time,

looks at the overloads on the VerifyData method require a parameter for Padding, I presume I need to pass it something like [System.Security.Cryptography.RSASignaturePadding]::Pkcs1

There is only a few options that I can see for padding (if I am thinking about it in the right way with my pea sized brain), I will give the above padding a try first

I feel like I owe you something for all your kind help we can't be all as bright as you Vadims :) , weather in the UK is awful :(  but I am off to join Ana in sunny Portugal in two weeks :) so you are always welcome at our house there (good wine, food, weather).

Cheers

Ernest

 

 

Michael
Michael 23.02.2018 17:16 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

I attempted to set this up. I found a couple of problems. 

First the Add-Type line needs to be the first line in the script and not inside the function.

Second, no matter what I set for the $Keyspec flag, It is always set to AT_SIGNATURE.

I debugged it for a while and can see the value being set to one on the property to 1 for AT_EXCHANGE but for whatever reason, the resulting certificate has it set to 2 AT_SIGNATURE.

 

Vadims Podāns
Vadims Podāns 25.02.2018 18:10 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

> First the Add-Type line needs to be the first line in the script and not inside the function.

yes, you are correct. I updated the code.

Michael
Michael 26.02.2018 23:01 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

After some more digging I found that if I add install and , I always get the following error 
Convert-PemToPfx : Method invocation failed because [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags] does not contain a method named 'op_Addition'.
At C:\tmp\converttopfx.ps1:182 char:1
+ Convert-PemToPfx -InputPath cert.pem -KeyPath cert.key -OutputPath cert.pfx -ins ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (op_Addition:String) [Convert-PemToPfx], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound,Convert-PemToPfx
 

Running Windows 2012 and Powershell 5

Vadims Podāns
Vadims Podāns 27.02.2018 08:59 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

This code perfectly runs on PowerShell v5, though is incompatible with versions prior to PowerShell 4.0. I updated the code with compatible syntax.

Michael
Michael 01.03.2018 16:36 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

I would say that the code runs fine but the implementation is slightly off.

If I run with the -KeySpec AT_EXCHANGE, It always creates the certficate with a KeySpec of -AT_SIGNATURE

I confirm this by running the following command to verify the setup

certutil –v –store "my" "<certificate name in the certificate store>"

I always get:


 ProviderType = 18
 Flags = 20 (32)
   CRYPT_MACHINE_KEYSET -- 20 (32)
   KeySpec = 2 -- AT_SIGNATURE
 

Vadims Podāns
Vadims Podāns 01.03.2018 18:00 (GMT+2) How to convert PEM to PFX in PowerShell (revisited)

I need to do more tests. I can't tell now what is wrong with KeySpec value.


Post your comment:

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