Я очень новичок в C++ и OpenSSL. Мне нужно проверить данный токен JWT (алгоритм RS256), используя открытый ключ через OpenSSL на C++. Я использую следующий алгоритм для проверки токена 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: 11
по EVP_DigestVerifyFinal
звонку. Я понятия не имею, где я ошибаюсь. Пожалуйста помоги.
Если вы сделали базовую проверку ошибок, вы увидите, что ваша функция 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, и он будет освобожден при выходе из функции).
В ответе Дмити он ошибочно предполагает, что может несколько раз передать указатель RSA на EVP_PKEY_assign_RSA. Это то, что вызывает его рассуждения о том, что вы не можете освободить указатель EVP_PKEY, поскольку первое освобождение сработает и также уничтожит указатель RSA. Второй свободный вызовет ошибку 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();
@Dmitry, спасибо, я пропустил это. Обновили код, чтобы удалить плохое бесплатно.
@Dmitry Дмитрий, я перечитал документ и думаю, что ты не прав. Речь идет о возвращенном указателе EVP_PKEY_CTX, поскольку меня не волнует этот указатель, который я передаю через nullptr. Переданный в EVP_PKEY еще нужно освободить. Я видел, где указатель RSA должен быть освобожден, если EVP_PKEY_new терпит неудачу.
@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 происходит, потому что контекст уже освобожден. В вашем коде этого не происходит, потому что вы не очищаете контекст.
EVP_MD_CTX_destroy определяется как EVP_MD_CTX_free, точно так же, как EVP_MD_CTX_create определяется как EVP_MD_CTX_new, поэтому они одинаковы. EVP_MD_CTX_cleanup предназначен для использования для выделения CTX в стеке. Так что для вас нет смысла вызывать is, поскольку он выполняет ту же работу, что и EVP_MD_CTX_free. Я не могу освободить свой EVP_PKEY для ошибки SIG. Он отлично работает для меня. Глядя на EVP_DigestVerifyInit, он никоим образом не удерживает PKEY, он просто вызывает EVP_PKEY_CTX_new с PKEY. Поэтому я все еще думаю, что ваш код пропускает указатели EVP_PKEY.
Соблюдение кода с очисткой 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? Если вы дважды вызываете «EVP_PKEY_assign_RSA» с одним и тем же указателем rsa, то EVP_PKEY* неисправен. Скорее всего, это ваша проблема с ошибкой подписи на втором свободном EVP_PKEY*. Таким образом, вы пропускаете указатели EVP_PKEY * с помощью этого кода. Чтобы этот код работал, вам нужно загрузить ключ RSA несколько раз или продублировать ключ RSA.
Использование EVP_PKEY_assign_RSA означает, что вы отказываетесь от контроля над управлением памятью ключей RSA. До этого момента вы контролируете использование памяти (т.е. вам нужно освободить то, что вы выделяете).
Вы можете использовать RSA_up_ref для добавления к счетчику ссылок RSA, прежде чем передать его EVP_PKEY_assign_RSA, если вы хотите использовать указатель RSA несколько раз.
@ShanePowell Спасибо за полезные комментарии. Я постараюсь использовать RSA_up_ref и сообщу вам о результатах. Большое спасибо
Вы не должны освобождать EVP_PKEY в pub_key_handle из-за документации OpenSSL: «Возвращаемое значение EVP_PKEY_CTX не должно освобождаться непосредственно приложением (оно будет освобождено автоматически при освобождении EVP_MD_CTX)».