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

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

我一直在尝试使用 PowerShell 来加密 RSA 私钥 1.1.0 以下的 Openssl 版本以同样的方式做到了。

使用 rashadrivera.com 中的优秀代码,我可以使用 PowerShell 从 pfx 文件中提取私钥。那我想做什么 do 是使用与 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 
}

要使用 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

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

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

Openssl 没有添加的 PowerShell 函数是什么?非常感谢您的帮助。

脚本中有两个问题:

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

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

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

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

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

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

第二个问题是没有正确考虑salt和IV的关系。不能像在当前代码中那样使用 salt 和 EVP_BytesToKey() 派生 IV。相反,必须生成一个随机 IV,其中 IV 的前 8 个字节是盐 s。 here 有关详细信息,请参阅 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]

创建一个 8 字节盐... 并且必须相应地删除行 [byte[]]$IV = $md5.ComputeHash($secondIteration + $passwordBytes + $saltBytes)

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

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

为了完整性:对于私钥,从 .NET Core 3.0 开始,除了上面用于导出 DER 编码的 PKCS#1 密钥的 ExportRSAPrivateKey() 之外,还有 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()


完整代码:

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 
}