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