Ошибка сегментации при проверке токена JWT с использованием открытого ключа через openSSL в C++

Я очень новичок в 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 звонку. Я понятия не имею, где я ошибаюсь. Пожалуйста помоги.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
640
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Если вы сделали базовую проверку ошибок, вы увидите, что ваша функция 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();

Вы не должны освобождать EVP_PKEY в pub_key_handle из-за документации OpenSSL: «Возвращаемое значение EVP_PKEY_CTX не должно освобождаться непосредственно приложением (оно будет освобождено автоматически при освобождении EVP_MD_CTX)».

Dmitry 27.05.2019 13:17

@Dmitry, спасибо, я пропустил это. Обновили код, чтобы удалить плохое бесплатно.

Shane Powell 27.05.2019 19:05

@Dmitry Дмитрий, я перечитал документ и думаю, что ты не прав. Речь идет о возвращенном указателе EVP_PKEY_CTX, поскольку меня не волнует этот указатель, который я передаю через nullptr. Переданный в EVP_PKEY еще нужно освободить. Я видел, где указатель RSA должен быть освобожден, если EVP_PKEY_new терпит неудачу.

Shane Powell 27.05.2019 19:27

@Dmitry, даже в этом случае вам все равно нужно освободить параметр EVP_PKEY. Вы не освобождаете только возвращаемый выходной параметр EVP_PkEY_CTX. Вы обнаружите утечку памяти, если не освободите переданный параметр EVP_PKEY.

Shane Powell 04.06.2019 13:24

Попробуйте освободить указатель EVP_PKEY, и вы получите SIG FAULT. В моем коде я выпускаю следующее: EVP_MD_CTX_cleanup(rsa_ctx); EVP_MD_CTX_destroy(rsa_ctx); и в случае освобождения PKEY SIG FAULT происходит, потому что контекст уже освобожден. В вашем коде этого не происходит, потому что вы не очищаете контекст.

Dmitry 04.06.2019 13:33

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.

Shane Powell 04.06.2019 19:52

Соблюдение кода с очисткой 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.

Shane Powell 04.06.2019 20:00

Использование EVP_PKEY_assign_RSA означает, что вы отказываетесь от контроля над управлением памятью ключей RSA. До этого момента вы контролируете использование памяти (т.е. вам нужно освободить то, что вы выделяете).

Shane Powell 04.06.2019 20:03

Вы можете использовать RSA_up_ref для добавления к счетчику ссылок RSA, прежде чем передать его EVP_PKEY_assign_RSA, если вы хотите использовать указатель RSA несколько раз.

Shane Powell 06.06.2019 00:27

@ShanePowell Спасибо за полезные комментарии. Я постараюсь использовать RSA_up_ref и сообщу вам о результатах. Большое спасибо

Dmitry 08.06.2019 11:19

Другие вопросы по теме