Изучая 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_free(ctx)
. Никаких изменений в поведении программ я не заметил. Но запустить его с помощью valgrind — хорошая идея. А пока попробую.
Я проверил свою программу с помощью valgrind. Он жаловался на то, что не освобождает mdctx
должным образом. Я исправил это и обновлю вопрос соответствующим кодом. Спасибо!
Ваша подпись ECDSA имеет формат P1363, а ваш код C требует подписи в формате ASN.1/DER, s. здесь о связи между обоими форматами
@Topaco Замечательно, этот вклад был необходим. Я попросил у ChatGPT соответствующую функцию преобразования для преобразования моей подписи в формат DER, и теперь она работает. Я очень ценю ваш комментарий. Ты спас мне всю неделю. Я боролся с этим два дня. Большое спасибо.
Если у вас есть время, напишите в ответе, что вы нашли, чтобы другие могли это найти.
Как отметил @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 и я полагаюсь на опытный опыт. Если можно будет предоставить более подробную информацию о различных форматах подписей или о лучшей реализации, я буду признателен за это.
Делает ли освобождение
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.