使用 base64 public 密钥 NID_secp384r1 和 openSSL 验证签名

Verify Signature with a base64 public key NID_secp384r1 with openSSL

我正在尝试使用 openssl 1.1.1k 验证签名,但我无法导入我使用 SubtleCrypto Web Crypto API 生成的 DER 编码 SPKI 格式 public 密钥。

使用 https://holtstrom.com/ 解码 public 密钥:

SEQUENCE {
   SEQUENCE {
      OBJECTIDENTIFIER 1.2.840.10045.2.1 (ecPublicKey)
      OBJECTIDENTIFIER 1.3.132.0.34 (P-384)
   }
   BITSTRING 0x04b8546c630d4c48195e071d109d36ecbddf328274c6882f6f9de9c112d8691e5428f08baeee7d2f2bf4ea888f759d5313cd4f1ed14862f1d5a24f69520242b116702cc5e573bc7deb392042b8a3a8f00e13f90e69f9c45a8b0ce60aae2c74dcef : 0 unused bit(s)
}

我 3 次尝试的 C++ 代码。我还在问题发生的行旁边评论了 openssl 库中的错误及其字符串表示形式:

#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

//include OpenSSL headers
#include <cassert>
#include <openssl/sha.h>
#include <openssl/bn.h>
#include <openssl/hmac.h>
#include <openssl/ec.h>      
#include <openssl/ecdsa.h>   
#include <openssl/obj_mac.h> 
#include <openssl/opensslv.h>
#include <openssl/engine.h>

#include <openssl/ssl.h>

#include <sstream>
#include <vector>
#include <openssl/err.h>

static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";


static inline bool is_base64(unsigned char c) {
    return (isalnum(c) || (c == '+') || (c == '/'));
}

namespace {

    std::string officalHash = "MTY0MjY5ODgyMg==";
    std::string officalSignature = "YkTPCGVj1Su+b4cqrOQPSwxHh79BTd9HZk/0OH71HjIbQ8/lSuKOEeNcpY9O7+4vgabIDRlyH5QmZmMV7X9s8eCk4cU7RAfn2YwE2pSvoik+upILLS9qmIDSaDz6LU2x";
    const auto officalPublicKey = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEuFRsYw1MSBleBx0QnTbsvd8ygnTGiC9vnenBEthpHlQo8Iuu7n0vK/TqiI91nVMTzU8e0Uhi8dWiT2lSAkKxFnAsxeVzvH3rOSBCuKOo8A4T+Q5p+cRaiwzmCq4sdNzv";

    struct BIOFreeAll { void operator()(BIO* p) { BIO_free_all(p); } };

}

std::string base64_decode(std::string const& encoded_string);

void tryOne(const std::vector<uint8_t>& binaryPublicKey);
void tryTwo(const std::vector<uint8_t>& binaryPublicKey);
void tryThree(const std::vector<uint8_t>& binaryPublicKey);

int main()
{
    SSL_library_init();
    SSL_load_error_strings();

    const auto hashBytes = base64_decode(officalHash);

    const auto publicKeyTemp = officalPublicKey;

    std::unique_ptr<BIO, BIOFreeAll> b64(BIO_new(BIO_f_base64()));
    BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
    BIO* source = BIO_new_mem_buf(officalPublicKey, -1); // read-only source
    BIO_push(b64.get(), source);
    const int maxlen = strlen(officalPublicKey) / 4 * 3 + 1;
    std::vector<uint8_t> decoded(maxlen);
    const int len = BIO_read(b64.get(), decoded.data(), maxlen);
    decoded.resize(len);


    tryOne(decoded);
    tryTwo(decoded);
    tryThree(decoded);

    return 0;
}

void tryOne(const std::vector<uint8_t>& binaryPublicKey)
{
    EVP_PKEY* key;
    EC_KEY* ec_key = EC_KEY_new_by_curve_name(NID_secp384r1);


    EVP_PKEY* ret = EVP_PKEY_new();

    EVP_PKEY_assign_EC_KEY(ret, ec_key);

    unsigned char const* ptr = binaryPublicKey.data();
    key = d2i_PublicKey(EVP_PKEY_EC, &ret, &ptr, binaryPublicKey.size());
    if (!key) {
        std::string error4 = ERR_error_string(ERR_get_error(), nullptr); //<- Error: error:10067066:elliptic curve routines:ec_GFp_simple_oct2point:invalid encoding
        return;
    }
}

void tryTwo(const std::vector<uint8_t>& binaryPublicKey)
{
    const auto tempstr = base64_decode(officalSignature);
    std::vector<uint8_t> SignatureBytes(tempstr.begin(), tempstr.end());

    std::shared_ptr<EVP_MD_CTX> mdctx = std::shared_ptr<EVP_MD_CTX>(EVP_MD_CTX_create(), EVP_MD_CTX_free);
    const EVP_MD* md = EVP_get_digestbyname("SHA512");
    if (nullptr == md)
    {
        return;
    }
    if (0 == EVP_VerifyInit_ex(mdctx.get(), md, NULL))
    {
        return;
    }

    if (0 == EVP_VerifyUpdate(mdctx.get(), binaryPublicKey.data(), binaryPublicKey.size()))
    {
        return;
    }


    std::shared_ptr<BIO> b644 = std::shared_ptr<BIO>(BIO_new(BIO_f_base64()), BIO_free);
    BIO_set_flags(b644.get(), BIO_FLAGS_BASE64_NO_NL);

    std::shared_ptr<BIO> bPubKey = std::shared_ptr<BIO>(BIO_new(BIO_s_mem()), BIO_free);
    BIO_puts(bPubKey.get(), officalPublicKey);
    BIO_push(b644.get(), bPubKey.get());

    std::shared_ptr<EVP_PKEY> pubkey = std::shared_ptr<EVP_PKEY>(d2i_PUBKEY_bio(b644.get(), NULL), EVP_PKEY_free);
    if (!pubkey) {
        std::string error = ERR_error_string(ERR_get_error(), nullptr);
        return;
    }

    auto b = EVP_VerifyFinal(mdctx.get(), SignatureBytes.data(), SignatureBytes.size(), pubkey.get());
    std::string error = ERR_error_string(ERR_get_error(), nullptr); // <- Error: error:0D0680A8:asn1 encoding routines:asn1_check_tlen:wrong tag
}


void tryThree(const std::vector<uint8_t>& binaryPublicKey)
{
    EC_KEY* eckey = NULL;

    std::vector<uint8_t> pubKeyVC = binaryPublicKey;

    const unsigned char* pubKeyVCp = pubKeyVC.data();

    const unsigned char** pubKeyVCpp = &pubKeyVCp;

    eckey = EC_KEY_new_by_curve_name(NID_secp384r1);

    EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);

    eckey = o2i_ECPublicKey(&eckey, pubKeyVCpp, pubKeyVC.size());

    if (!EC_KEY_check_key(eckey)) {
        std::string error = ERR_error_string(ERR_get_error(), nullptr); //<- Error: "error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error"
    }
}

std::string base64_decode(std::string const& encoded_string) {
    int in_len = encoded_string.size();
    int i = 0;
    int j = 0;
    int in_ = 0;
    unsigned char char_array_4[4], char_array_3[3];
    std::string ret;

    while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
        char_array_4[i++] = encoded_string[in_]; in_++;
        if (i == 4) {
            for (i = 0; i < 4; i++)
                char_array_4[i] = base64_chars.find(char_array_4[i]);

            char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
            char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
            char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

            for (i = 0; (i < 3); i++)
                ret += char_array_3[i];
            i = 0;
        }
    }

    if (i) {
        for (j = i; j < 4; j++)
            char_array_4[j] = 0;

        for (j = 0; j < 4; j++)
            char_array_4[j] = base64_chars.find(char_array_4[j]);

        char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
        char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
        char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

        for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
    }

    return ret;
}

密钥生成: public 密钥是在 javascript:

中使用 SubtleCrypto 生成的
function generateEcdsaKeypair() {
let keypair = window.crypto.subtle.generateKey(
  {
    name: "ECDSA",
    namedCurve: "P-384"
  },
  true,
  ["sign", "verify"]
)

return keypair
}

然后使用 RFC 5280 第 4.1 节中使用 ASN.1 表示法定义的 SubjectPublicKeyInfo 格式导出:

function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint8Array(buf));
}

async function exportCryptoKey(key) {
  const exported = await window.crypto.subtle.exportKey(
  "spki",
  key
 );

 const exportedAsString = ab2str(exported);
 const exportedAsBase64 = window.btoa(exportedAsString);
 console.log("PublicKey: ", exportedAsBase64);
}

为了签署数据,我使用了以下函数:

function signEcdsa(privateKey, data) {
let signature = window.crypto.subtle.sign(
  {
    name: 'ECDSA',
    hash: { name: 'SHA-512' },
  },
  privateKey,
  data
)

return signature
}

但我什至无法将 base64 字符串转换为 public 密钥。验证更不用说了

整代档案:

js.html 文件:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>Programiz</title>
</head>

<body>
  <script src="js.js"></script>
</body>
</html>

js.js 文件: 可以在控制台选项卡下的检查 window 中找到输出。

function generateEcdsaKeypair() {
    let keypair = window.crypto.subtle.generateKey(
      {
        name: "ECDSA",
        namedCurve: "P-384"
      },
      true,
      ["sign", "verify"]
    )
  
    return keypair
  }
  
  // Generates a signature of the data given
function signEcdsa(privateKey, data) {
    let signature = window.crypto.subtle.sign(
      {
        name: 'ECDSA',
        hash: { name: 'SHA-512' },
      },
      privateKey,
      data
    )
  
    return signature
}

function verifyEcdsa(publicKey, data, signature) {
    let result = window.crypto.subtle.verify(
      {
        name: 'ECDSA',
        hash: { name: 'SHA-512' },
      },
      publicKey,
      signature,
      data
    )
  
    return result
  }

function _arrayBufferToBase64( buffer ) {
    var binary = '';
    var bytes = new Uint8Array( buffer );
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode( bytes[ i ] );
    }
    return window.btoa( binary );
}

function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint8Array(buf));
}

function toHexString(byteArray) {
  return Array.from(byteArray, function(byte) {
    return ('0' + (byte & 0xFF).toString(16)).slice(-2);
  }).join('')
}

function b64EncodeUnicode(str) {
  return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
      return String.fromCharCode('0x' + p1);
  }));
}

function b64DecodeUnicode(str) {
  return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
      return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  }).join(''));
}

function addNewLines(str) {
  var finalString = '';
  for(var i=0; i < str.length; i++) {
      finalString += str.substring(0, 64) + '\n';
      str = str.substring(64);
  }
  finalString += str;

  return finalString;
}

function removeLines(pem) {
  var lines = pem.split('\n');
  var encodedString = '';
  for(var i=0; i < lines.length; i++) {
      encodedString += lines[i].trim();
  }
  return encodedString;
}

function stringToArrayBuffer(byteString){
  var byteArray = new Uint8Array(byteString.length);
  for(var i=0; i < byteString.length; i++) {
      byteArray[i] = byteString.codePointAt(i);
  }
  return byteArray;
}

function  arrayBufferToString(exportedPrivateKey){
  var byteArray = new Uint8Array(exportedPrivateKey);
  var byteString = '';
  for(var i=0; i < byteArray.byteLength; i++) {
      byteString += String.fromCodePoint(byteArray[i]);
  }
  return byteString;
}


async function exportCryptoKey(key) {
  const exported = await window.crypto.subtle.exportKey(
    "spki",
    key
  );
  
  const exportedAsString = ab2str(exported);
  const exportedAsBase64 = window.btoa(exportedAsString);
  console.log("PublicKey: ", exportedAsBase64);
  console.log(toHexString(exported));

  var privateKeyDer = arrayBufferToString(exported); //pkcs#8 to DER
  var privateKeyB64 = b64EncodeUnicode(privateKeyDer); //btoa(privateKeyDer);
  var privateKeyPEMwithLines = addNewLines(privateKeyB64);  //split PEM into 64 character strings
  var privateKeyPEMwithoutLines = removeLines(privateKeyPEMwithLines);  //join PEM
  var privateKeyDerDecoded = b64DecodeUnicode(privateKeyPEMwithoutLines);  // atob(privateKeyB64);
  var privateKeyArrayBuffer = stringToArrayBuffer(privateKeyDerDecoded);  //DER to arrayBuffer

  console.log(exported);
  console.log(privateKeyDer);
  console.log(privateKeyB64);
  console.log(privateKeyPEMwithLines);
  console.log(privateKeyPEMwithoutLines);
  console.log(privateKeyDerDecoded);
  console.log(privateKeyArrayBuffer);

  /*const pemExported = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;

  const exportKeyOutput = document.querySelector(".exported-key");
  exportKeyOutput.textContent = pemExported;*/
}

function  arrayBufferToString(exportedPrivateKey){
  var byteArray = new Uint8Array(exportedPrivateKey);
  var byteString = '';
  for(var i=0; i < byteArray.byteLength; i++) {
      byteString += String.fromCodePoint(byteArray[i]);
  }
  return byteString;
}

async function testSignEcdsa() {
  let {
    privateKey: aPrivate,
    publicKey: aPublic
  } = await generateEcdsaKeypair()

    // A sends and signs data for B using their own private key
    let data = window.crypto.getRandomValues(new Uint8Array(64))
    let signature = await signEcdsa(aPrivate, data)

     // B verifies A's signature using A's public key
    let result = await verifyEcdsa(aPublic, data, signature)

    let base64result = _arrayBufferToBase64(signature)
    let signatureBase64 = window.btoa(String.fromCharCode.apply(null, new Uint8Array(signature)))
    let dataBase64 = window.btoa(String.fromCharCode.apply(null, new Uint8Array(data)))
    // Tests
    
    // Signature is verified
    console.log('Data signed/verified successfully: ', result)
    console.log('Signature: ', signatureBase64)
    console.log('Data: ', dataBase64)

    exportCryptoKey(aPublic);


}

testSignEcdsa()

更新:

我把它放在这里是为了让其他人不需要通过 openssl 的文档来了解如何将 r|s 签名转换为 ans1 der 编码。不要忘记检查错误代码:

std::vector<uint8_t> Vault::convertP1363EncodingSignatureToASN1Der(const std::string& signatureInHexFormat) const
{ std::unique_ptr< ECDSA_SIG, std::function<void(ECDSA_SIG*)>> zSignature(ECDSA_SIG_new(), [](ECDSA_SIG* b) { ECDSA_SIG_free(b); });

std::unique_ptr< BIGNUM, std::function<void(BIGNUM*)>> r(nullptr, [](BIGNUM* b) { BN_free(b); });
BIGNUM* r_ptr = r.get();
std::unique_ptr< BIGNUM, std::function<void(BIGNUM*)>> s(nullptr, [](BIGNUM* b) { BN_free(b); });
BIGNUM* s_ptr = s.get();

const std::string sSignatureR = signatureInHexFormat.substr(0, signatureInHexFormat.size() / 2);
const std::string sSignatureS = signatureInHexFormat.substr(signatureInHexFormat.size() / 2);

BN_hex2bn(&r_ptr, sSignatureR.c_str());
BN_hex2bn(&s_ptr, sSignatureS.c_str());

ECDSA_SIG_set0(zSignature.get(), r_ptr, s_ptr);

unsigned char buffer[256];
unsigned char* pbuffer = buffer;

int signatureLength = i2d_ECDSA_SIG(zSignature.get(), nullptr);
signatureLength = i2d_ECDSA_SIG(zSignature.get(), &pbuffer);


return { buffer, buffer + signatureLength };
}

tryTwo() 允许通过以下更改成功验证发布的数据:

  • 除了密钥和签名外,消息本身也需要验证。但是,在当前代码中根本没有使用该消息。必须在 VerifyUpdate() 中指定(而不是 public 键):

    void tryTwo(const std::vector<uint8_t>& binaryPublicKey)
    {
        const auto hashBytes = base64_decode(officalHash);
        ...
        if (0 == EVP_VerifyUpdate(mdctx.get(), hashBytes.c_str(), hashBytes.size()))
        {
    ...
    

    顺便说一句,officalHashhashBytes 是误导性的名称,因为应用了 未散列 消息。

  • WebCrypto 生成 IEEE P1363 (r|s) 格式的签名,而 EVP_VerifyFinal() 需要 ASN.1/DER 格式的签名。发布的 ASN.1/DER 格式签名为(Base64 编码):

    std::string officalSignature = "MGUCMGJEzwhlY9Urvm+HKqzkD0sMR4e/QU3fR2ZP9Dh+9R4yG0PP5UrijhHjXKWPTu/uLwIxAIGmyA0Zch+UJmZjFe1/bPHgpOHFO0QH59mMBNqUr6IpPrqSCy0vapiA0mg8+i1NsQ==";
    

    解释了两种格式之间的关系,例如here.

经过这两个改动,在我的机器上验证成功。