Ошибка EVP_DigestVerifyFinal — ECDSA P-256/SHA-256 с OpenSSL (Libcrypto)

Изучая Libcrypto, я дошёл до того, что мне понадобился внешний вклад.

У меня есть следующий рабочий пример Python:

#!/usr/bin/env python3

import hashlib
from ecdsa import curves, VerifyingKey

public_key_bytes = bytearray.fromhex("0303b2ce64bc207bdd8bc4df859187fcb686320d63ffa091410fc158fbb77980ea")
public_key = VerifyingKey.from_string(public_key_bytes, curve=curves.NIST256p, hashfunc=hashlib.sha256)

ds = bytearray.fromhex("F8CD88299FA4605800207BFEBEAC55024053F30F7C69B35C15E60800AC3B6FE3ED0639952F7B028D86867445961FFE94FB226BFF7006E0C451EE3F8728C177FB")
m = bytearray.fromhex("8210492204E060610BDF26D77B5BF8C9CBFCF70422081475FD445DF0FF")

print(public_key.verify(ds, m))

При выполнении этого кода я получаю True. Когда я меняю любой байт m, ds или public_key, я получаю ожидаемые ошибки проверки.

m — сообщение, ds — цифровая подпись.


Теперь самое сложное. Я создал следующие две функции:

Первая функция для загрузки открытого ключа.

EVP_PKEY* load_public_key(const unsigned char* public_key_bytes, size_t key_len) {
    EVP_PKEY* pkey = nullptr;
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr);
    if (!ctx) {
        std::cerr << "Failed to create EVP_PKEY_CTX" << std::endl;
        print_openssl_errors();
        return nullptr;
    }

    if (EVP_PKEY_fromdata_init(ctx) <= 0) {
        std::cerr << "EVP_PKEY_fromdata_init failed" << std::endl;
        print_openssl_errors();
        EVP_PKEY_CTX_free(ctx);
        return nullptr;
    }

    OSSL_PARAM params[3];
    params[0] = OSSL_PARAM_construct_utf8_string("group", (char*)"P-256", 0);
    params[1] = OSSL_PARAM_construct_octet_string("pub", (void*)public_key_bytes, key_len);
    params[2] = OSSL_PARAM_construct_end();

    if (EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) {
        std::cerr << "EVP_PKEY_fromdata failed" << std::endl;
        print_openssl_errors();
        EVP_PKEY_CTX_free(ctx);
        return nullptr;
    }

    EVP_PKEY_CTX_free(ctx);
    return pkey;
}

Вторая функция должна проверять подпись с помощью открытого ключа и сообщения.

bool verify_signature(EVP_PKEY* pkey, const unsigned char* msg, size_t msg_len, const unsigned char* sig, size_t sig_len) {
    EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
    if (!mdctx) {
        std::cerr << "Failed to create EVP_MD_CTX" << std::endl;
        print_openssl_errors();
        return false;
    }

    bool result = false;
    if (EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pkey) <= 0) {
        std::cerr << "EVP_DigestVerifyInit failed" << std::endl;
        print_openssl_errors();
        EVP_MD_CTX_free(mdctx);
    }

    if (EVP_DigestVerifyUpdate(mdctx, msg, msg_len) <= 0) {
        std::cerr << "EVP_DigestVerifyUpdate failed" << std::endl;
        print_openssl_errors();
        EVP_MD_CTX_free(mdctx);
    }

    int verify_result = EVP_DigestVerifyFinal(mdctx, sig, sig_len);
    if (verify_result == 1) {
        result = true; // Verification succeeded
    } else if (verify_result == 0) {
        std::cerr << "Signature is invalid" << std::endl;
    } else {
        // THIS CASE IS HAPPENING ALL THE TIME
        std::cerr << "EVP_DigestVerifyFinal failed" << std::endl;
        print_openssl_errors();
    }

    EVP_MD_CTX_free(mdctx);

    return result;
}

При вызове этих функций с данными, использованными в примере Python, мне сообщается, что EVP_DigestVerifyFinal failed, а EVP_DigestVerifyFinal() возвращает -1. К сожалению, EVP_get_error() не показывает ошибок, и в документации указано, что -1 означает, что с подписью, вероятно, что-то не так.

EVP_DigestVerifyFinal() и EVP_DigestVerify() возвращают 1 в случае успеха; любое другое значение указывает на сбой. Возвращаемое значение, равное нулю, указывает на то, что подпись не прошла успешную проверку (т. е. tbs не соответствует исходным данным или подпись имела недопустимую форму), тогда как другие значения указывают на более серьезную ошибку (а иногда также указывают на недопустимую форму подписи). ).

Я думаю, что я загрузил открытый ключ правильно, потому что, когда я немного меняю открытый ключ, я получаю сообщение об ошибке.

Мой контекст вызова выглядит следующим образом:

std::array<std::uint8_t, 33> pk{0x03, 0x03, 0xB2, 0xCE, 0x64, 0xBC, 0x20, 0x7B, 0xDD, 0x8B, 0xC4, 0xDF, 0x85, 0x91, 0x87, 0xFC, 0xB6, 0x86, 0x32, 0x0D, 0x63, 0xFF, 0xA0, 0x91, 0x41, 0x0F, 0xC1, 0x58, 0xFB, 0xB7, 0x79, 0x80, 0xEA};
std::array<std::uint8_t, 64> ds{0xF8, 0xCD, 0x88, 0x29, 0x9F, 0xA4, 0x60, 0x58, 0x00, 0x20, 0x7B, 0xFE, 0xBE, 0xAC, 0x55, 0x02, 0x40, 0x53, 0xF3, 0x0F, 0x7C, 0x69, 0xB3, 0x5C, 0x15, 0xE6, 0x08, 0x00, 0xAC, 0x3B, 0x6F, 0xE3, 0xED, 0x06, 0x39, 0x95, 0x2F, 0x7B, 0x02, 0x8D, 0x86, 0x86, 0x74, 0x45, 0x96, 0x1F, 0xFE, 0x94, 0xFB, 0x22, 0x6B, 0xFF, 0x70, 0x06, 0xE0, 0xC4, 0x51, 0xEE, 0x3F, 0x87, 0x28, 0xC1, 0x77, 0xFB};
std::array<std::uint8_t, 29> m{0x82, 0x10, 0x49, 0x22, 0x04, 0xE0, 0x60, 0x61, 0x0B, 0xDF, 0x26, 0xD7, 0x7B, 0x5B, 0xF8, 0xC9, 0xCB, 0xFC, 0xF7, 0x04, 0x22, 0x08, 0x14, 0x75, 0xFD, 0x44, 0x5D, 0xF0, 0xFF};


// Load public key
EVP_PKEY* pkey = load_public_key(pk.data(), pk.size());
if (!pkey) {
    std::cerr << "Failed to load public key" << std::endl;
    return;
}

// Verify signature
bool is_valid = verify_signature(pkey, m.data(), m.size(), ds.data(), ds.size());
if (is_valid) {
    std::cout << "Signature is valid" << std::endl;
} else {
    // THIS ERROR IS SEEN
    std::cout << "Signature is invalid" << std::endl;
}

// Tidy up public key
EVP_PKEY_free(pkey);

Если кто-то увидит возможность сократить код, чтобы получить более минимальный пример, дайте мне знать. Спасибо!

Делает ли освобождение EVP_PKEY_CTX недействительным EVP_PKEY, полученный из этого контекста? Пример кода по адресу openssl.org/docs/man3.1/man3/EVP_PKEY_fromdata.html не вызывает EVP_PKEY_CTX_free() до тех пор, пока EVP_PKEY_free(). Если освобождение EVP_PKEY_CTX делает EVP_PKEY недействительным, это должно быть до боли очевидно, если вы запускаете свой код под чем-то вроде Valgrind.

Andrew Henle 03.07.2024 15:40

Хорошая идея, быстро проверил, прокомментировав звонки EVP_PKEY_CTX_free(ctx). Никаких изменений в поведении программ я не заметил. Но запустить его с помощью valgrind — хорошая идея. А пока попробую.

Andre Buschermöhle 03.07.2024 15:50

Я проверил свою программу с помощью valgrind. Он жаловался на то, что не освобождает mdctx должным образом. Я исправил это и обновлю вопрос соответствующим кодом. Спасибо!

Andre Buschermöhle 03.07.2024 16:02

Ваша подпись ECDSA имеет формат P1363, а ваш код C требует подписи в формате ASN.1/DER, s. здесь о связи между обоими форматами

Topaco 03.07.2024 16:18

@Topaco Замечательно, этот вклад был необходим. Я попросил у ChatGPT соответствующую функцию преобразования для преобразования моей подписи в формат DER, и теперь она работает. Я очень ценю ваш комментарий. Ты спас мне всю неделю. Я боролся с этим два дня. Большое спасибо.

Andre Buschermöhle 03.07.2024 16:41

Если у вас есть время, напишите в ответе, что вы нашли, чтобы другие могли это найти.

Botje 04.07.2024 09:30
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
101
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Как отметил @Topaco, проблема заключалась в формате подписи. Формат подписи в моем контексте вызова был в формате P1363. Реализация ожидает, что подпись будет ASN.1 DER Encoding for NIST P-256. Подробности здесь.

Я реализовал функцию (в сотрудничестве с

std::vector<uint8_t> convert_p1363_to_der(const std::array<uint8_t, 64>& signature) {
    std::unique_ptr<ECDSA_SIG, decltype(&ECDSA_SIG_free)> ecdsa_sig{ECDSA_SIG_new(), ECDSA_SIG_free};

    using BigNumUniquePtr = std::unique_ptr<BIGNUM, decltype(&BN_free)>;
    BigNumUniquePtr r{BN_bin2bn(signature.data(), 32, nullptr), BN_free};
    BigNumUniquePtr s{BN_bin2bn(signature.data() + 32, 32, nullptr), BN_free};

    if (!r || !s || !ecdsa_sig) {
        std::cerr << "Failed to convert ECDSA_SIG to DER" << std::endl;
        return {};
    }

    ECDSA_SIG_set0(ecdsa_sig.get(), r.release(), s.release());

    std::vector<std::uint8_t> der(72);
    auto* derPtr{der.data()};
    int der_size{i2d_ECDSA_SIG(ecdsa_sig.get(), &derPtr)};
    if (der_size < 0) {
        std::cerr << "Failed to convert ECDSA_SIG to DER" << std::endl;
        return {};
    }

    der.resize(der_size);
    return der;
}

Мой контекст вызова теперь выглядит так:

std::array<std::uint8_t, 33> pk{0x03, 0x03, 0xB2, 0xCE, 0x64, 0xBC, 0x20, 0x7B, 0xDD, 0x8B, 0xC4, 0xDF, 0x85, 0x91, 0x87, 0xFC, 0xB6, 0x86, 0x32, 0x0D, 0x63, 0xFF, 0xA0, 0x91, 0x41, 0x0F, 0xC1, 0x58, 0xFB, 0xB7, 0x79, 0x80, 0xEA};
std::array<std::uint8_t, 64> ds{0xF8, 0xCD, 0x88, 0x29, 0x9F, 0xA4, 0x60, 0x58, 0x00, 0x20, 0x7B, 0xFE, 0xBE, 0xAC, 0x55, 0x02, 0x40, 0x53, 0xF3, 0x0F, 0x7C, 0x69, 0xB3, 0x5C, 0x15, 0xE6, 0x08, 0x00, 0xAC, 0x3B, 0x6F, 0xE3, 0xED, 0x06, 0x39, 0x95, 0x2F, 0x7B, 0x02, 0x8D, 0x86, 0x86, 0x74, 0x45, 0x96, 0x1F, 0xFE, 0x94, 0xFB, 0x22, 0x6B, 0xFF, 0x70, 0x06, 0xE0, 0xC4, 0x51, 0xEE, 0x3F, 0x87, 0x28, 0xC1, 0x77, 0xFB};
std::array<std::uint8_t, 29> m{0x82, 0x10, 0x49, 0x22, 0x04, 0xE0, 0x60, 0x61, 0x0B, 0xDF, 0x26, 0xD7, 0x7B, 0x5B, 0xF8, 0xC9, 0xCB, 0xFC, 0xF7, 0x04, 0x22, 0x08, 0x14, 0x75, 0xFD, 0x44, 0x5D, 0xF0, 0xFF};

// THIS CALL IS NEW
auto dsAsDer{convert_p1363_to_der(ds)};

// Load public key
EVP_PKEY* pkey = load_public_key(pk.data(), pk.size());
if (!pkey) {
    std::cerr << "Failed to load public key" << std::endl;
    return;
}

// Verify signature - USING SIGNATURE IN DER FORMAT HERE
bool is_valid = verify_signature(pkey, m.data(), m.size(), dsAsDer.data(), dsAsDer.size()); ds.size());
if (is_valid) {
    std::cout << "Signature is valid" << std::endl;
} else {
    std::cout << "Signature is invalid" << std::endl;
}

// Tidy up public key
EVP_PKEY_free(pkey);

Пожалуйста, не стесняйтесь перехватить этот ответ, изменить его и опубликовать повторно. Я удалю этот пост и приму лучший ответ. Пока что у меня лишь ограниченные знания о libcrypto и я полагаюсь на опытный опыт. Если можно будет предоставить более подробную информацию о различных форматах подписей или о лучшей реализации, я буду признателен за это.

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