尝试以与 Openssl 相同的方式在 PowerShell 中创建加密的私钥

RIT*_*ITT 5 encryption powershell openssl

我一直在尝试使用 PowerShell 加密 RSA 私钥,其方式与低于 1.1.0 的 Openssl 版本相同。

使用rashadrivera.com中的优秀代码,我可以使用 PowerShell 从 pfx 文件中提取私钥。然后我想做的是使用与 openssl 相同的密码密钥派生方法来加密私钥。感谢mti2935对 openssl 遗留密钥派生 EVP_BytesToKey 的精彩解释。

我知道这里的方法被认为是过时的,因此这是一个纯粹的学术练习,旨在尝试在 PowerShell 中实现相同的目标。

以下PowerShell函数可以创建pem格式的加密私钥,但openssl无法解密它。它要求输入密码,然后失败。

openssl rsa -check -in .\encryptedprivkey.pem

这是powershell函数

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 
}
Run Code Online (Sandbox Code Playgroud)

要使用 powershell 功能,我首先执行以下操作,将我的证书和私钥提取到名为 mypfx.pfx 的 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
Run Code Online (Sandbox Code Playgroud)

我注意到 PowerShell 函数创建的 pem 文件的大小比使用 openssl 命令加密的相同私钥大

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

PowerShell 添加了哪些 Openssl 没有添加的功能?非常感谢您的帮助。

Top*_*aco 1

脚本中有两个问题:

由于加密密钥将以 PKCS#1 格式创建,因此要加密的密钥也必须具有 PKCS#1 格式。然而,事实并非如此,因为

$keyToEncryptThumbPrint.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
Run Code Online (Sandbox Code Playgroud)

以 PKCS#8 格式导出密钥(这也是加密密钥比预期长的原因,因为 PKCS#8 格式的密钥是算法标识符和 PKCS#1 格式的密钥的组合,s.here)。

以 PKCS#1 格式导出密钥的一种方法是使用RSA.ExportRSAPrivateKey()方法。为此,必须按如下方式修改脚本:

# Export key in PKCS#1 format
$dataToEncrypt = $rsaCng.ExportRSAPrivateKey()
Run Code Online (Sandbox Code Playgroud)

请注意,使用此方法需要 Powershell 7.x。

第二个问题是没有正确考虑盐与IV之间的关系。IV 不得使用盐来导出,EVP_BytesToKey()就像当前代码中发生的那样。相反,必须生成一个随机 IV,其中 IV 的前 8 个字节是盐 s。有关更多详细信息,请参阅PEM 加密格式部分:

# 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]
Run Code Online (Sandbox Code Playgroud)

Create an 8 byte salt...[byte[]]$IV = $md5.ComputeHash($secondIteration + $passwordBytes + $saltBytes)并且必须相应地删除该行。

通过这些更改,我可以在我的计算机上创建 PKCS#1 格式的加密密钥,该密钥通过一致性检查并通过 OpenSSL 成功解密:

openssl rsa -check -in <path to encrypted PKCS#1 key>
Run Code Online (Sandbox Code Playgroud)

为了完整起见:对于私钥,从 .NET Core 3.0 开始,除了ExportRSAPrivateKey()上面用于导出 DER 编码的 PKCS#1 密钥之外,还用于ExportPkcs8PrivateKey()导出 DER 编码的 PKCS#8 密钥和ExportEncryptedPkcs8PrivateKey()导出 DER 编码的加密 PKCS #8 键。不支持加密的 PKCS#1 密钥。从 .NET 7 Preview 3 开始,还支持 PEM 导出:ExportRSAPrivateKeyPem()ExportPkcs8PrivateKeyPem()ExportEncryptedPkcs8PrivateKeyPem().


完整代码:

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 
}
Run Code Online (Sandbox Code Playgroud)