【问题标题】:Segmentation Fault while verifying JWT token using a public key through openSSL in C++在 C++ 中通过 openSSL 使用公钥验证 JWT 令牌时出现分段错误
【发布时间】:2019-06-06 06:36:28
【问题描述】:

我对 C++ 和 OpenSSL 非常陌生。我必须通过 C++ 中的 OpenSSL 使用公钥验证给定的 JWT 令牌(算法 RS256)。我正在使用以下算法来验证 JWT 令牌。

// signature algorithm data = base64urlEncode( header ) + “.” + base64urlEncode( payload ) hashedData = hash( data, secret ) signature = base64urlEncode( hashedData )

我在 Mac 系统上使用 g++ 编译我的代码。终端上的openssl version 显示LibreSSL 2.6.5

// Assume that base64 encode and decode functions are available
bool RSAVerifySignature( RSA* rsa, std::string token, std::string pub_key) {

  std::vector<std::string> tokenParts;
  split(token, tokenParts, '.');

  std::string decoded_header = tokenParts[0];
  std::string header = base64_encode(reinterpret_cast<const unsigned char*>(decoded_header.c_str()),
    decoded_header.length());

  std::string decoded_body = tokenParts[1];
  std::string body = base64_encode(reinterpret_cast<const unsigned char*>(decoded_body.c_str()),
    decoded_body.length());


  std::string sig = tokenParts[2];

  EVP_PKEY* pubKey  = EVP_PKEY_new();
  EVP_PKEY_assign_RSA(pubKey, rsa);
  EVP_MD_CTX* m_RSAVerifyCtx = EVP_MD_CTX_create();

  if (1 != EVP_DigestVerifyInit(m_RSAVerifyCtx, NULL, EVP_sha256(), NULL, pubKey)) {
      printf("verify init failed....\n");
  } else {
      printf("verify init passed....\n");
  }
  if (1 != EVP_DigestVerifyUpdate(m_RSAVerifyCtx, (unsigned char *)header.data(), header.length())) {
      printf("DigestVerifyUpdate for header failed....\n");
  } else {
          printf("DigestVerifyUpdate for header passed....\n");
  }
  if (1 != EVP_DigestVerifyUpdate(m_RSAVerifyCtx, ".", 1)) {
      printf("DigestVerifyUpdate for dot failed\n");
  } else {
      printf("DigestVerifyUpdate for dot passed\n");
  }
  if (1 != EVP_DigestVerifyUpdate(m_RSAVerifyCtx, (unsigned char *)body.data(), body.length())) {
      printf("DigestVerifyUpdate for body failed\n");
  } else {
      printf("DigestVerifyUpdate for body passed\n");
  }

  int result = EVP_DigestVerifyFinal(m_RSAVerifyCtx, (unsigned char *)sig.data(), sig.length());
  return result;
}

RSA* createPublicRSA(std::string key) {
  RSA *rsa = NULL;
  BIO *keybio;
  const char* c_string = key.c_str();
  keybio = BIO_new_mem_buf((void*)c_string, -1);
  if (keybio==NULL) {
      return 0;
  }
  rsa = PEM_read_bio_RSA_PUBKEY(keybio, &rsa,NULL, NULL);
  return rsa;
}

int main()
{
    std::string token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA";

    std::string publicKey = "-----BEGIN PUBLIC KEY-----"\
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv"\
"vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc"\
"aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy"\
"tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0"\
"e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb"\
"V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9"\
"MwIDAQAB"\
"-----END PUBLIC KEY-----";

    RSA* publicRSA = createPublicRSA(publicKey);
    bool result = RSAVerifySignature(publicRSA, token, publicKey);
    return 0;
}

我收到了Segmentation fault: 11EVP_DigestVerifyFinal 电话。我不知道我哪里错了。请帮忙。

【问题讨论】:

    标签: c++ openssl jwt


    【解决方案1】:

    如果您进行了一些基本的错误检查,您将看到您的 createPublicRSA 函数返回一个 nullptr。这是因为 PEM_read_bio_RSA_PUBKEY 期望看到换行符,而您的 publicKey 字符串没有。

    如果您将其更改为具有换行符,它应该能够很好地创建 RSA 密钥。

    例如

        std::string publicKey = "-----BEGIN PUBLIC KEY-----\n"\
    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv\n"\
    "vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc\n"\
    "aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy\n"\
    "tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0\n"\
    "e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb\n"\
    "V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9\n"\
    "MwIDAQAB\n"\
    "-----END PUBLIC KEY-----\n";
    

    此外,您的代码将不起作用,因为您不需要“编码”标题和正文文本,但您确实需要“base64url 解码”签名,因为它需要是二进制值才能进行验证。

    以下代码适用于我:

    bool RSAVerifySignature(RSA* rsa, std::string const& token)
    {
        auto const pub_key_handle = make_handle(EVP_PKEY_new(), EVP_PKEY_free);
        if (!pub_key_handle)
        {
            RSA_free(rsa);
            return false;
        }
        EVP_PKEY_assign_RSA(pub_key_handle.get(), rsa);
    
        std::vector<std::string> token_parts;
        split(token, token_parts, '.');
        if (token_parts.size() != 3) return false;
    
        auto& decoded_header = token_parts[0];
        auto& decoded_body = token_parts[1];
        auto sig_decoded = base64_url_decode(token_parts[2]);
    
        auto const rsa_verify_ctx = make_handle(EVP_MD_CTX_new(), EVP_MD_CTX_free);
        if (!rsa_verify_ctx) return false;
    
        if (1 != EVP_DigestVerifyInit(rsa_verify_ctx.get(), nullptr, EVP_sha256(), nullptr, pub_key_handle.get())) return false;
        if (1 != EVP_DigestVerifyUpdate(rsa_verify_ctx.get(), reinterpret_cast<unsigned char*>(decoded_header.data()), decoded_header.length())) return false;
        if (1 != EVP_DigestVerifyUpdate(rsa_verify_ctx.get(), ".", 1)) return false;
        if (1 != EVP_DigestVerifyUpdate(rsa_verify_ctx.get(), reinterpret_cast<unsigned char*>(decoded_body.data()), decoded_body.length())) return false;
        return 1 == EVP_DigestVerifyFinal(rsa_verify_ctx.get(), reinterpret_cast<unsigned char*>(sig_decoded.data()), sig_decoded.length());
    }
    

    更新:

    上面的代码假设您正在使用 RSA 指针(即传递 RSA 的所有权,它将在函数退出时释放)。

    在 Dmity 的回答中,他错误地假设他可以多次将 RSA 指针传递给 EVP_PKEY_assign_RSA。这样做是导致他推理您无法释放 EVP_PKEY 指针的原因,因为第一个释放将起作用并且还会破坏 RSA 指针。第二个 free 将导致他所说的 sib 错误,因为 RSA 指针已经被释放。要将上面的示例更改为不使用 RSA 指针,我们需要增加 RSA 内部引用,以便 EVP_PKEY 指针的释放不会释放 RSA 指针,而只是使用 RSA_up_ref 函数减少它。

    例如

    bool RSAVerifySignature(RSA* rsa, std::string const& token)
    {
        auto pub_key_handle = make_handle(EVP_PKEY_new(), EVP_PKEY_free);
        if (!pub_key_handle)
        {
            return false;
        }
    
        RSA_up_ref(rsa);
        EVP_PKEY_assign_RSA(pub_key_handle.get(), rsa);
    
        std::vector<std::string> token_parts;
        split(token, token_parts, '.');
        if (token_parts.size() != 3) return false;
    
        auto& decoded_header = token_parts[0];
        auto& decoded_body = token_parts[1];
        auto sig_decoded = base64_url_decode(token_parts[2]);
    
        ///auto const rsa_verify_ctx = make_handle(EVP_MD_CTX_new(), EVP_MD_CTX_free);
        auto const rsa_verify_ctx = make_handle(EVP_MD_CTX_new(), EVP_MD_CTX_free);
        if (!rsa_verify_ctx) return false;
    
        EVP_PKEY_CTX *pctx;
        if (1 != EVP_DigestVerifyInit(rsa_verify_ctx.get(), &pctx, EVP_sha256(), nullptr, pub_key_handle.get())) return false;
        pub_key_handle.reset();
        if (1 != EVP_DigestVerifyUpdate(rsa_verify_ctx.get(), reinterpret_cast<unsigned char*>(decoded_header.data()), decoded_header.length())) return false;
        if (1 != EVP_DigestVerifyUpdate(rsa_verify_ctx.get(), ".", 1)) return false;
        if (1 != EVP_DigestVerifyUpdate(rsa_verify_ctx.get(), reinterpret_cast<unsigned char*>(decoded_body.data()), decoded_body.length())) return false;
        return 1 == EVP_DigestVerifyFinal(rsa_verify_ctx.get(), reinterpret_cast<unsigned char*>(sig_decoded.data()), sig_decoded.length());
    }
    

    测试调用代码现在可以做到这一点,并且不会导致 sig 错误:

    auto public_rsa = make_handle(createPublicRSA(publicKey), RSA_free);
    if(!public_rsa) return false;
    
    for(auto i = 0; i < 5; ++i)
    {
        if(!RSAVerifySignature(public_rsa.get(), token)) return false;
    }
    public_rsa.reset();
    

    【讨论】:

    • 您不应该释放 pub_key_handle 中的 EVP_PKEY,因为 OpenSSL 文档:“返回的 EVP_PKEY_CTX 值不能由应用程序直接释放(释放 EVP_MD_CTX 时会自动释放)。”
    • @Dmitry,谢谢我错过了那个。已更新代码以删除不良免费。
    • @Dmitry,我已经重读了文档,我认为你是不正确的。它正在谈论返回的 EVP_PKEY_CTX 指针,因为我不关心我通过 nullptr 传递的那个指针。传入的 EVP_PKEY 仍然需要释放。如果 EVP_PKEY_new 失败,我确实看到了需要释放 RSA 指针的位置。
    • @Dmitry 即使在这种情况下,您仍然需要释放 EVP_PKEY 参数。只有返回的 EVP_PkEY_CTX 输出参数您没有释放。如果你不释放传入的 EVP_PKEY 参数,你会发现你正在泄漏内存。
    • 尝试释放 EVP_PKEY 指针,你会得到 SIG FAULT。在我的代码中,我发布了以下内容: EVP_MD_CTX_cleanup( rsa_ctx ); EVP_MD_CTX_destroy(rsa_ctx);并且在释放 PKEY SIG FAULT 的情况下,因为上下文已经释放。在你的代码中它不会发生,因为你没有清理上下文。
    【解决方案2】:

    通过 CTX 清理遵循代码的和平

    bool sha_validate( const EVP_MD* type, const std::string& input, const std::vector<unsigned char>& digest )
    {
        if( !rsa )
            return false;
    
        EVP_PKEY* pub_key  = EVP_PKEY_new(); 
        EVP_PKEY_assign_RSA(pub_key, rsa);
        EVP_MD_CTX* rsa_verify_ctx = EVP_MD_CTX_create();
    
        auto ctx_free = scope_remove( [rsa_verify_ctx]() {
            EVP_MD_CTX_cleanup( rsa_verify_ctx );
            EVP_MD_CTX_destroy( rsa_verify_ctx );
        });
    
        if (EVP_DigestVerifyInit( rsa_verify_ctx,NULL, type, NULL, pub_key  ) <=0 )
            return false;
    
        if (EVP_DigestVerifyUpdate( rsa_verify_ctx, input.c_str(), input.size() ) <= 0)
            return false;
    
        return EVP_DigestVerifyFinal( rsa_verify_ctx, &digest[0], digest.size() ) == 1;
    }
    
    bool sha_validate( int type, const std::string& input, const std::vector<unsigned char>& digest )
    {
         bool result = false;
    
         if( type & RsaOaep::SHA1 )
             result = sha_validate( EVP_sha1(), input, digest );
    
         if( !result && ( type & RsaOaep::SHA256 ) == static_cast<int>(RsaOaep::SHA256) )
             result = sha_validate( EVP_sha256(), input, digest );
    
         return result;
    }
    
    RSA* rsa = nullptr;
    

    【讨论】:

    • rsa 是从哪里来的?如果您最终使用相同的 rsa 指针调用“EVP_PKEY_assign_RSA”两次,则 EVP_PKEY* 是错误的。这很可能是您在第二个 EVP_PKEY* 免费时出现的 sig 错误问题。因此,您正在使用此代码泄漏 EVP_PKEY* 指针。要使此代码正常工作,您需要多次加载 RSA 密钥或复制 RSA 密钥。
    • 使用 EVP_PKEY_assign_RSA 意味着您放弃了对 RSA 密钥内存管理的控制。到那时,您可以控制内存使用(即您需要释放分配的内容)。
    • 如果您希望多次使用 RSA 指针,您可以使用 RSA_up_ref 添加到 RSA 引用计数,然后再将其提供给 EVP_PKEY_assign_RSA。
    • @ShanePowell 感谢您提供有用的 cmets 我将尝试使用 RSA_up_ref 并告知您结果。非常感谢
    猜你喜欢
    • 1970-01-01
    • 2019-08-25
    • 2017-11-03
    • 2019-10-14
    • 2018-01-06
    • 2018-11-11
    • 1970-01-01
    • 2020-09-13
    • 2022-12-15
    相关资源
    最近更新 更多