使用 openssl C++ API 生成的签名与 python 中的相同代码不匹配

问题描述 投票:0回答:1

我有一个Python代码,它根据哈希字符串作为数据生成签名。它使用密码学库根据哈希值计算签名。它使用私钥文件 .pem,其中也包含私钥和公钥。它使用serialization.load_pem_private_key() API通过读取.pem文件来获取私钥。然后使用这个私钥,使用以下 python 加密方法生成签名。

privateKey.sign(hashData,
     padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=32),
     utils.Prehashed(hashes.SHA256()))

如果这里输入的 hashData 相同,它总是生成一个新的签名。 然后,它通过从同一 .pem 文件获取公钥并调用验证 API 来验证生成的签名,如下所示

publicKey.verify(
                generated_signature,
                hashData,
                padding.PSS(
                    mgf=padding.MGF1(hashes.SHA256()),
                    salt_length=32
                ),
                utils.Prehashed(hashes.SHA256())
            )

在python中验证成功。我尝试使用版本 3.4.0 的 OpenSSL API 在 C++ 中模仿相同的逻辑。签名生成成功,但是当我尝试使用这个基于 python 的工具验证它时,验证因错误而失败

failed to verify with private key from key file 'key.pem'.

以下是我为模仿相同逻辑而编写的示例 C++ 代码

#define _CRT_SECURE_NO_WARNINGS 
#include "openssl/pem.h"
#include "openssl/err.h"
#include "openssl/evp.h"
#include "openssl/rsa.h"
#include "openssl/sha.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <vector>
#include "ms/applink.c"


void handleOpenSSLErrors() 
{
    ERR_print_errors_fp(stderr);
    abort();
}

// Load RSA private key from a PEM file
EVP_PKEY* loadPrivateKey(const std::string& filePath) 
{
    FILE* keyFile = nullptr;
    errno_t r_code = fopen_s(&keyFile, filePath.c_str(), "rb");
    if (!keyFile) {
        std::cerr << "Unable to open private key file: " << filePath << std::endl;
        return nullptr;
    }
    EVP_PKEY* pkey = PEM_read_PrivateKey(keyFile, nullptr, nullptr, nullptr);
    fclose(keyFile);

    if (!pkey) 
    {
        std::cerr << "Unable to read private key from file: " << filePath << std::endl;
    }

    int keyType = EVP_PKEY_base_id(pkey);

    switch (keyType) {
    case EVP_PKEY_RSA:
        std::cout << "Key Type: RSA" << std::endl;
        break;
    case EVP_PKEY_EC:
        std::cout << "Key Type: EC (Elliptic Curve)" << std::endl;
        break;
    case EVP_PKEY_DSA:
        std::cout << "Key Type: DSA" << std::endl;
        break;
    default:
        std::cout << "Key Type: Unknown" << std::endl;
        break;
    }

    return pkey;
}

std::vector<unsigned char> signData(EVP_PKEY* privateKey, const std::vector<unsigned char>& hashData) {
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    if (!ctx) handleOpenSSLErrors();

    if (EVP_DigestSignInit(ctx, nullptr, EVP_sha256(), nullptr, privateKey) <= 0) {
        handleOpenSSLErrors();
    }

    EVP_PKEY_CTX_set_rsa_padding(EVP_MD_CTX_get_pkey_ctx(ctx), RSA_PKCS1_PSS_PADDING);
    EVP_PKEY_CTX_set_rsa_mgf1_md(EVP_MD_CTX_get_pkey_ctx(ctx), EVP_sha256());
    EVP_PKEY_CTX_set_rsa_pss_saltlen(EVP_MD_CTX_get_pkey_ctx(ctx), -2);
    
    size_t sigLen = 0;
    if (EVP_DigestSign(ctx, nullptr, &sigLen, hashData.data(), hashData.size()) <= 0) {
        handleOpenSSLErrors();
    }

    std::vector<unsigned char> signature(sigLen);
    if (EVP_DigestSign(ctx, signature.data(), &sigLen, hashData.data(), hashData.size()) <= 0) {
        handleOpenSSLErrors();
    }

    EVP_MD_CTX_free(ctx);
    return signature;
}

std::string vectorToHexString(const std::vector<unsigned char>& bytes)
{
    std::ostringstream hexStream;

    // Convert each byte to a two-character hexadecimal representation
    for (unsigned char byte : bytes) {
        hexStream << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
    }

    return hexStream.str();
}

std::vector<unsigned char> hexStringToBinary(const std::string& hex) {
    if (hex.length() % 2 != 0) {
        throw std::invalid_argument("Hex string length must be even");
    }

    std::vector<unsigned char> binary;
    binary.reserve(hex.length() / 2);

    for (size_t i = 0; i < hex.length(); i += 2) {
        std::string byteString = hex.substr(i, 2); // Get two characters
        unsigned char byte = static_cast<unsigned char>(std::stoi(byteString, nullptr, 16));
        binary.push_back(byte);
    }

    return binary;
}

int main()
{
    const std::string privateKeyPath = "private_key.pem";

    // Example hash data
    std::string data = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
    
    std::vector<unsigned char> hashBinary = hexStringToBinary(data);
    for (unsigned char byte : hashBinary)
    {
        std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
    }
    // Verify size
    if (hashBinary.size() != 32) {
        std::cerr << "Invalid hash size! Expected 32 bytes (SHA-256)." << std::endl;
        return 1;
    }
    // Load keys
    EVP_PKEY* privateKey = loadPrivateKey(privateKeyPath);
    if (!privateKey) return 1;

    if (EVP_PKEY_base_id(privateKey) == EVP_PKEY_RSA)
    {
        std::cout << "RSA key loaded successfully." << std::endl;
    }
    else 
    {
        std::cerr << "Invalid key type! RSA key required." << std::endl;
        EVP_PKEY_free(privateKey);
        return 1;
    }
    // Generate signature
    std::vector<unsigned char> signature = signData(privateKey, hashBinary);
    std::cout << "Signature generated successfully." << std::endl;
    for (unsigned char byte : signature)
    {
        std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
    }
    
    // Clean up
    EVP_PKEY_free(privateKey);

    return 0;
}

在 C++ 代码中,如果我加载公钥并尝试使用 OpenSSL API 进行验证,则验证会成功。但是当尝试使用基于Python的工具验证时,生成的签名验证失败!

我应该如何调试它以及如何解决这个问题? 我在 C++ 代码中遗漏了什么吗?

c++ openssl
1个回答
0
投票

您隐式使用了哈希的

EVP_DigestSignXXX()
函数,因此 C 代码总体哈希两次。您可能认为情况并非如此,因为还有
EVP_SignXXX()
函数。但这些也隐式散列。顺便说一句,前者更年轻,应该在较新的应用程序中使用(请参阅此处)。
你需要的是函数
EVP_PKEY_signXXX()
,它隐式散列,例如这里(为了简单起见,没有异常处理):

...
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(privateKey, NULL);
EVP_PKEY_sign_init(ctx);
EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PSS_PADDING);
EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256());
//EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()); // for this use case optional
//EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, RSA_PSS_SALTLEN_DIGEST); // for this use case optional
size_t sigLen = 0;
EVP_PKEY_sign(ctx, NULL, &sigLen, hashData.data(), hashData.size());
std::vector<unsigned char> signature(sigLen);
EVP_PKEY_sign(ctx, signature.data(), &sigLen, hashData.data(), hashData.size());
EVP_PKEY_CTX_free(ctx);
...

为了与Python代码兼容,请注意以下事项:

  • PSS 摘要必须使用
    EVP_PKEY_CTX_set_signature_md()
    设置为 SHA256,默认为 SHA1。
  • MGF1 摘要隐式设置为 PSS 摘要,即在这种情况下两个摘要相同,因此不需要显式设置。当然,显式设置为 SHA256 也没有什么坏处。这是用
    EVP_PKEY_CTX_set_rsa_mgf1_md()
    完成的。
  • 盐长度必须设置为
    RSA_PSS_SALTLEN_DIGEST
    (这对应于
    -1
    )而不是
    -2
    -2
    对应于
    RSA_PSS_SALTLEN_AUTO
    。签名时,后者与使用最大盐长度的
    RSA_PSS_SALTLEN_MAX
    (值为
    -3
    )没有区别。由于
    RSA_PSS_SALTLEN_DIGEST
    是签名的默认值,因此在这种情况下不需要显式设置盐长度,请参阅此处
© www.soinside.com 2019 - 2024. All rights reserved.