0

I am very new to C++ and OpenSSL. I have to verify a given JWT token (algorithm RS256) using a public key through OpenSSL in C++. I am using following algorithm to verify the JWT token.

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

I am on a Mac system and using g++ to compile my code. openssl version on terminal shows 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;
}

I am getting Segmentation fault: 11 at EVP_DigestVerifyFinal call. I have no idea where am I wrong. Please help.

alk
  • 69,737
  • 10
  • 105
  • 255
Aosis
  • 311
  • 6
  • 17

2 Answers2

0

If you had done some basic error checking you will see that your createPublicRSA function returns a nullptr. This is because PEM_read_bio_RSA_PUBKEY is expecting to see newlines and your publicKey string doesn't have any.

If you change it to have newlines it should be able to create the RSA key fine.

e.g.

    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";

Also your code will not work as you don't need to "encode" the header and body text, but you do need to "base64url decode" the signature as it needs to be a binary value for the verification to work.

The following code works for me:

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());
}

UPDATE:

The above code assumes that you are consuming the RSA pointer (i.e. passing ownership of the RSA and it will be freed by the exit of the function).

In Dmity's answer he is incorrectly assuming that he can pass the RSA pointer to EVP_PKEY_assign_RSA multiple times. Doing this is what is causing his reasoning that your can't free the EVP_PKEY pointer, as the first free will work and will also destroy the RSA pointer. The second free will cause the sib fault he's talking about as the RSA pointer is already freed. To change my example above to not consume the RSA pointer we need to increase the RSA internal reference so that the free of the EVP_PKEY pointer will not free the RSA pointer but just decrease it using the RSA_up_ref function.

e.g.

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());
}

And the test calling code can now do this and not cause a sig fault:

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();
Shane Powell
  • 13,698
  • 2
  • 49
  • 61
  • You should not free EVP_PKEY in pub_key_handle because of OpenSSL documentation: "The EVP_PKEY_CTX value returned must not be freed directly by the application (it will be freed automatically when the EVP_MD_CTX is freed)." – Dmitry May 27 '19 at 11:17
  • @Dmitry, thanks I missed that one. Have updated the code to remove the bad free. – Shane Powell May 27 '19 at 17:05
  • @Dmitry, I've reread the doc's and I think you are incorrect. It's talking about EVP_PKEY_CTX pointer returned, since I don't care about that pointer I pass through a nullptr. The passed in EVP_PKEY still needs to be freed. I did see where RSA pointer needs to be freed if EVP_PKEY_new fails. – Shane Powell May 27 '19 at 17:27
  • @Dmitry even in that case, you still need to free the EVP_PKEY parameter. It’s only the returned EVP_PkEY_CTX out parameter that you don’t free. You will find you are leaking memory if you don’t free the passed in EVP_PKEY parameter. – Shane Powell Jun 04 '19 at 11:24
  • Try to free EVP_PKEY pointer and you will get SIG FAULT. In my code I'm releasing following: EVP_MD_CTX_cleanup( rsa_ctx ); EVP_MD_CTX_destroy( rsa_ctx ); and in case of releasing PKEY SIG FAULT happens because context already released. In You code its not happens because you are not cleaning context. – Dmitry Jun 04 '19 at 11:33
  • EVP_MD_CTX_destroy is #defined to EVP_MD_CTX_free, just like EVP_MD_CTX_create is #defined to EVP_MD_CTX_new, so they are the same. EVP_MD_CTX_cleanup is meant to be used for stack allocated CTX allocations. So for you there is no point in calling is as it's doing the same job as EVP_MD_CTX_free. I can't get my EVP_PKEY free to SIG fault. It works fine for me. Looking into EVP_DigestVerifyInit it's not holding the PKEY in any way, it's just calling EVP_PKEY_CTX_new with the PKEY. So I still think your code is leaking EVP_PKEY pointers. – Shane Powell Jun 04 '19 at 17:52
0

Following peace of code with CTX cleanup

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;
Dmitry
  • 906
  • 1
  • 13
  • 32
  • Where is rsa coming from? If you end up calling "EVP_PKEY_assign_RSA" twice with the same rsa pointer, then the EVP_PKEY* is bad. This is most likely your sig fault problem on the second EVP_PKEY* free. So you are leaking EVP_PKEY* pointers with this code. For this code to work you need to load the RSA key multiple times or duplicate the RSA key. – Shane Powell Jun 04 '19 at 18:00
  • Using EVP_PKEY_assign_RSA means you are giving up control of the RSA key memory management. Up to that point you control the memory usage (i.e. you need to free what you allocate). – Shane Powell Jun 04 '19 at 18:03
  • 1
    You can use RSA_up_ref to add to the RSA reference count before giving it to EVP_PKEY_assign_RSA if you wish to use the RSA pointer multiple times. – Shane Powell Jun 05 '19 at 22:27
  • @ShanePowell Thank you for useful comments I will try to use RSA_up_ref and inform you about results. Many thanks – Dmitry Jun 08 '19 at 09:19