Home > database >  Trying to create an encypted private key in PowerShell the same way Openssl does it
Trying to create an encypted private key in PowerShell the same way Openssl does it

Time:05-07

I have been trying to use PowerShell to encrypt an RSA private key in the same way versions of Openssl below 1.1.0 did it.

Using the excelent code from rashadrivera.com I can extract the private key from a pfx file using PowerShell. What I would then like to do is encrypt the private key using the same password key derivation method as openssl. thanks to mti2935 for such a great explanation of openssl legacy key derivation EVP_BytesToKey.

I understand the method here is considered obsolete, so this is a purely academic exercise to try and achieve the same thing in PowerShell.

The following PowerShell function can create an encrypted private key in pem format but openssl cannot decrypt it. It asks for the password, then fails.

openssl rsa -check -in .\encryptedprivkey.pem

Here is the powershell function

function Export-PrivateKeyPemEncrypted([System.Security.Cryptography.X509Certificates.X509Certificate2]$pfx, [System.String]$outputPath) {

    # Process RSA key
    $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($pfx)
    $rsaCng = ([System.Security.Cryptography.RSACng]$rsa)
    $keyToEncryptThumbPrint = $rsaCng.Key
    $dataToEncrypt = $keyToEncryptThumbPrint.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
    
    # Convert the password into a byte array
    $passphrase = "passphrase123456"
    [byte[]] $passwordBytes = [Text.Encoding]::UTF8.GetBytes($passphrase) # 16 bytes
    
    # Create an 8 byte salt
    # to be added onto the end
    # of the password to increase its randomness
    $array = @()
    for($i=0;$i -lt 8;$i  )
    {
        $array  = [math]::Round($(Get-Random -Minimum 50.1 -Maximum 190.1))
    }
    [byte[]] $saltBytes = $array 

    # Create a new instance of the MD5 hashing algorythum
    $md5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
    
    # Pre openssl V1.1.0 way to create an encryption key
    # from a password. This is a bit like the 
    # obelete standard 
    # RFC2898 PBKDF1, but not exactly. 
    # Google EVP_BytesToKey
    [byte[]]$firstIteration = $md5.ComputeHash($passwordBytes   $saltBytes) # 16 bytes
    [byte[]]$secondIteration = $md5.ComputeHash($firstIteration   $passwordBytes   $saltBytes) # 16 bytes
    
    # Derive the encryption key and Initialization vector
    [byte[]]$key = $firstIteration   $secondIteration
    [byte[]]$IV  = $md5.ComputeHash($secondIteration   $passwordBytes   $saltBytes) # 16 bytes

    # Geneate an AES symetrical encryption standard object
    # This is the encryption algorythum that will encrypt
    # our private key using the key derived from the password above
    $aesManaged = New-Object System.Security.Cryptography.AesManaged
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7 
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256

    # Instruct AES object what the key and IV are
    $aesManaged.Key = $key
    $aesManaged.IV = $IV
    $ivAsString = [System.BitConverter]::ToString($IV) -replace "-"
    Write-Host " iv  = $ivAsString"
    Write-Host " key = $([System.BitConverter]::ToString($aesManaged.Key) -replace "-")"

    # Now do the actual encypting of the RSA private key
    # Data to encrypt must be binary formatted into an array
    # of bytes
    $encryptor = $aesManaged.CreateEncryptor()
    [byte[]] $encryptedData = $encryptor.TransformFinalBlock($dataToEncrypt, 0, $dataToEncrypt.Length)
    $aesManaged.Dispose()

    # Format the base64 string into lines of 64
    # which is what openssl does
    $base64CertText = [System.Convert]::ToBase64String($encryptedData) -replace ".{64}", "`$&`r`n"
    
    # Creat a variable to store the encypted key
    $out = New-Object String[] -ArgumentList 5

    # PEM file. See RFC1421 page 24
    # for heading explanations
    $out[0] = "-----BEGIN RSA PRIVATE KEY-----"
    $out[1] = "Proc-Type: 4,ENCRYPTED"
    $out[2] = "DEK-Info: AES-256-CBC,$ivAsString`r`n"
    $out[3] = $base64CertText
    $out[4] = "-----END RSA PRIVATE KEY-----"
    
    $out | Out-File $outputPath
    # this removes CR/LF combination that openssl hates
    (Get-Content $outputPath) | Set-Content $outputPath 
}

To use the powershell function I first do the following, having already extracted my certificate and private key to a pfx file called mypfx.pfx

$privKeyPasWd = ConvertTo-SecureString -String "passphrase123456" -Force -AsPlainText
$pfxExportOptions = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable 
$pfxAsCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("C:\Certs\mypfx.pfx", $privKeyPasWd, $pfxExportOptions)
$privateKeyName = "C:\Certs\encryptedprivkey.pem"
Export-PrivateKeyPemEncrypted $pfxAsCertificate $privateKeyName

I notice the pem file created by the PowerShell function is larger in size than the same private key encypted using openssl command

openssl rsa -aes256 -in .\privkey.pem -out .\encryptedprivkey.pem

What is the PowerShell function adding that Openssl does not? Many thanks for any assistance.

CodePudding user response:

There are two problems in the script:

Since an encrypted key is to be created in PKCS#1 format, the key to be encrypted must also have PKCS#1 format. However, this is not the case, because

$keyToEncryptThumbPrint.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)

exports the key in PKCS#8 format (which is also the reason why the encrypted key is longer than expected, since the key in PKCS#8 format is the combination of an algorithm identifier and the key in PKCS#1 format, s. here).

One way to export the key in PKCS#1 format is to use the RSA.ExportRSAPrivateKey() method. To do this, the script must be modified as follows:

# Export key in PKCS#1 format
$dataToEncrypt = $rsaCng.ExportRSAPrivateKey()

Note that using this method requires Powershell 7.x.

The second problem is that the relationship between salt and IV is not properly considered. The IV must not be derived using the salt and EVP_BytesToKey() as it happens in the current code. Instead, a random IV must be generated, where the first 8 bytes of the IV are the salt, s. here for more details, section PEM ENCRYPTION FORMAT:

# Create 16 byte IV
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()   
$IV = New-Object System.Byte[](16)
$rng.GetBytes($IV)

# Get 8 byte salt
$saltBytes = $IV[0..7]

The block Create an 8 byte salt... and the line [byte[]]$IV = $md5.ComputeHash($secondIteration $passwordBytes $saltBytes) must be removed accordingly.

With these changes I can create an encrypted key in PKCS#1 format on my machine which passes the consistency check and is successfully decrypted by OpenSSL via:

openssl rsa -check -in <path to encrypted PKCS#1 key>

For completeness: For private keys, as of .NET Core 3.0, in addition to the ExportRSAPrivateKey() used above for exporting a DER encoded PKCS#1 key, there is also ExportPkcs8PrivateKey() for exporting a DER encoded PKCS#8 key and ExportEncryptedPkcs8PrivateKey() for exporting a DER encoded encrypted PKCS#8 key. Encrypted PKCS#1 keys are not supported. As of .NET 7 Preview 3 the PEM export is also supported: ExportRSAPrivateKeyPem(), ExportPkcs8PrivateKeyPem() and ExportEncryptedPkcs8PrivateKeyPem().


Complete code:

function Export-PrivateKeyPemEncrypted([System.Security.Cryptography.X509Certificates.X509Certificate2]$pfx, [System.String]$outputPath) {

    # Process RSA key
    $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($pfx)
    $rsaCng = ([System.Security.Cryptography.RSACng]$rsa)
    # Export key in PKCS#1 format
    $dataToEncrypt = $rsaCng.ExportRSAPrivateKey()
    
    # Convert the password into a byte array
    $passphrase = "passphrase123456"
    [byte[]] $passwordBytes = [Text.Encoding]::UTF8.GetBytes($passphrase) # 16 bytes
    
    # Create 16 byte IV
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()   
    $IV = New-Object System.Byte[](16)
    $rng.GetBytes($IV)

    # Get 8 byte salt
    $saltBytes = $IV[0..7]

    # Create a new instance of the MD5 hashing algorythum
    $md5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
    
    # Pre openssl V1.1.0 way to create an encryption key
    # from a password. This is a bit like the 
    # obelete standard 
    # RFC2898 PBKDF1, but not exactly. 
    # Google EVP_BytesToKey
    [byte[]]$firstIteration = $md5.ComputeHash($passwordBytes   $saltBytes) # 16 bytes
    [byte[]]$secondIteration = $md5.ComputeHash($firstIteration   $passwordBytes   $saltBytes) # 16 bytes
    
    # Derive the encryption key and Initialization vector
    [byte[]]$key = $firstIteration   $secondIteration

    # Geneate an AES symetrical encryption standard object
    # This is the encryption algorythum that will encrypt
    # our private key using the key derived from the password above
    $aesManaged = New-Object System.Security.Cryptography.AesManaged
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7 
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256

    # Instruct AES object what the key and IV are
    $aesManaged.Key = $key
    $aesManaged.IV = $IV
    $ivAsString = [System.BitConverter]::ToString($IV) -replace "-"
    Write-Host " iv  = $ivAsString"
    Write-Host " key = $([System.BitConverter]::ToString($aesManaged.Key) -replace "-")"

    # Now do the actual encypting of the RSA private key
    # Data to encrypt must be binary formatted into an array
    # of bytes
    $encryptor = $aesManaged.CreateEncryptor()
    [byte[]] $encryptedData = $encryptor.TransformFinalBlock($dataToEncrypt, 0, $dataToEncrypt.Length)
    $aesManaged.Dispose()

    # Format the base64 string into lines of 64
    # which is what openssl does
    $base64CertText = [System.Convert]::ToBase64String($encryptedData) -replace ".{64}", "`$&`r`n"
    
    # Creat a variable to store the encypted key
    $out = New-Object String[] -ArgumentList 5

    # PEM file. See RFC1421 page 24
    # for heading explanations
    $out[0] = "-----BEGIN RSA PRIVATE KEY-----"
    $out[1] = "Proc-Type: 4,ENCRYPTED"
    $out[2] = "DEK-Info: AES-256-CBC,$ivAsString`r`n"
    $out[3] = $base64CertText
    $out[4] = "-----END RSA PRIVATE KEY-----"
    
    $out | Out-File $outputPath
    # this removes CR/LF combination that openssl hates
    (Get-Content $outputPath) | Set-Content $outputPath 
}
  • Related