Я работаю над приложением, производительность которого критична.
В этом приложении у меня есть много сообщений (то есть несколько тысяч), которые необходимо подписать (и, конечно, проверить) отдельно одним и тем же закрытым ключом / открытым ключом. Я использую библиотеку OpenSSL.
Наивный подход с функциями DSA (см. Ниже) потребует десятки секунд, чтобы подписать, что нехорошо. Я пытался использоватьDSA_sign_setup()
функция, чтобы ускорить процесс, но я не могу понять, как правильно его использовать.
Я также попробовал ECDSA, но я потерял в получении правильной конфигурации.
Как правильно сделать это, если я действительно забочусь об эффективности?
#include <openssl/dsa.h>
#include <openssl/engine.h>
#include <stdio.h>
#include <openssl/evp.h>
int N=3000;
int main()
{
DSA *set=DSA_new();
int a;
a=DSA_generate_parameters_ex(set,1024,NULL,1,NULL,NULL,NULL);
printf("%d\n",a);
a=DSA_generate_key(set);
printf("%d\n",a);
unsigned char msg[]="I am watching you!I am watching you!";
unsigned char sign[256];
unsigned int size;
for(int i=0;i<N;i++)
a=DSA_sign(1,msg,32,sign,&size,set);
printf("%d %d\n",a,size);
}
С помощью DSA_sign_setup()
способ, предложенный выше, на самом деле совершенно небезопасен, и, к счастью, разработчики OpenSSL сделали структуру DSA непрозрачной, чтобы разработчики не могли попытаться прорваться.
DSA_sign_setup()
генерирует новый случайный одноразовый номер (который является своего рода эфемерным ключом на подпись). Должно никогда использоваться повторно под тем же долгосрочным секретным ключом. Никогда.
Теоретически вы все еще можете быть относительно безопасны, повторно используя один и тот же одноразовый номер для одного и того же сообщения, но как только одна и та же комбинация личного ключа и одноразового номера будет повторно использована для двух разных сообщений, вы просто откроете всю информацию, необходимую злоумышленнику для получения вашего секретного ключа ( увидеть Sony fail0verflow что в основном происходит из-за той же ошибки повторного использования nonce с ECDSA).
К сожалению, DSA работает медленно, особенно сейчас, когда требуются более длинные ключи: для ускорения работы вашего приложения вы можете попробовать использовать ECDSA (например, с кривой NISTP256, но без повторного использования одноразовых номеров) или Ed25519 (однозначный одноразовый номер).
Обновить: Вот доказательство того, как программно генерировать подписи с помощью OpenSSL.
Предпочтительным способом является использование EVP_DigestSign
API так как он абстрагируется, какой вид асимметричного ключа используется.
Следующий пример расширяет PoC в эта вики-страница OpenSSL: Я протестировал это, используя закрытый ключ DSA или NIST P-256, с OpenSSL 1.0.2, 1.1.0 и 1.1.1-pre6.
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#define KEYFILE "private_key.pem"#define N 3000
#define BUFFSIZE 80
EVP_PKEY *read_secret_key_from_file(const char * fname)
{
EVP_PKEY *key = NULL;
FILE *fp = fopen(fname, "r");
if(!fp) {
perror(fname); return NULL;
}
key = PEM_read_PrivateKey(fp, NULL, NULL, NULL);
fclose(fp);
return key;
}
int do_sign(EVP_PKEY *key, const unsigned char *msg, const size_t mlen,
unsigned char **sig, size_t *slen)
{
EVP_MD_CTX *mdctx = NULL;
int ret = 0;
/* Create the Message Digest Context */
if(!(mdctx = EVP_MD_CTX_create())) goto err;
/* Initialise the DigestSign operation - SHA-256 has been selected
* as the message digest function in this example */
if(1 != EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, key))
goto err;
/* Call update with the message */
if(1 != EVP_DigestSignUpdate(mdctx, msg, mlen)) goto err;
/* Finalise the DigestSign operation */
/* First call EVP_DigestSignFinal with a NULL sig parameter to
* obtain the length of the signature. Length is returned in slen */
if(1 != EVP_DigestSignFinal(mdctx, NULL, slen)) goto err;
/* Allocate memory for the signature based on size in slen */
if(!(*sig = OPENSSL_malloc(*slen))) goto err;
/* Obtain the signature */
if(1 != EVP_DigestSignFinal(mdctx, *sig, slen)) goto err;
/* Success */
ret = 1;
err:
if(ret != 1)
{
/* Do some error handling */
}
/* Clean up */
if(*sig && !ret) OPENSSL_free(*sig);
if(mdctx) EVP_MD_CTX_destroy(mdctx);
return ret;
}
int main()
{
int ret = EXIT_FAILURE;
const char *str = "I am watching you!I am watching you!";
unsigned char *sig = NULL;
size_t slen = 0;
unsigned char msg[BUFFSIZE];
size_t mlen = 0;
EVP_PKEY *key = read_secret_key_from_file(KEYFILE);
if(!key) goto err;
for(int i=0;i<N;i++) {
if ( snprintf((char *)msg, BUFFSIZE, "%s %d", str, i+1) < 0 )
goto err;
mlen = strlen((const char*)msg);
if (!do_sign(key, msg, mlen, &sig, &slen)) goto err;
OPENSSL_free(sig); sig = NULL;
printf("\"%s\" -> siglen=%lu\n", msg, slen);
}
printf("DONE\n");
ret = EXIT_SUCCESS;
err:
if (ret != EXIT_SUCCESS) {
ERR_print_errors_fp(stderr);
fprintf(stderr, "Something broke!\n");
}
if (key)
EVP_PKEY_free(key);
exit(ret);
}
Генерация ключа:
# Generate a new NIST P-256 private key
openssl ecparam -genkey -name prime256v1 -noout -out private_key.pem
Я запустил и ваш оригинальный пример, и мой код на моей машине (Intel Skylake) и на Raspberry Pi 3. В обоих случаях ваш оригинальный пример не занимает десятки секунд.
Учитывая, что, очевидно, вы видите огромное улучшение производительности при использовании небезопасный DSA_sign_setup()
Подход в OpenSSL 1.0.2 (который внутренне потребляет новую случайность, в дополнение к некоторой довольно дорогой модульной арифметике), я подозреваю, что у вас действительно может быть проблема с PRNG, который замедляет генерацию новых случайных одноразовых номеров и оказывает большее влияние, чем модульные арифметические операции.
Если это так, то вы можете определенно извлечь выгоду из использования Ed25519, поскольку в этом случае одноразовый номер является детерминированным, а не случайным (он генерируется с использованием безопасных хеш-функций и сочетания закрытого ключа и сообщения).
К сожалению, это означает, что вам нужно будет дождаться выпуска OpenSSL 1.1.1 (надеюсь, этим летом).
Чтобы использовать Ed25519 (который будет поддерживаться изначально начиная с OpenSSL 1.1.1), необходимо изменить приведенный выше пример, так как в OpenSSL 1.1.1 нет поддержки Ed25519ph и вместо использования Init
/Update
/Final
API потоковой передачи вам нужно будет назвать одноразовым EVP_DigestSign()
интерфейс (см. документация).
Полный отказ от ответственности: Следующий абзац — бесстыдная заглушка для моего libsuola исследовательский проект, так как я могу определенно выиграть от тестирования реальных приложений от других пользователей.
Кроме того, если вы не можете ждать, я являюсь разработчиком OpenSSL ENGINE
называется libsuola
это добавляет поддержку Ed25519 в OpenSSL 1.0.2, 1.1.0 (а также 1.1.1 с использованием альтернативных реализаций). Он все еще экспериментальный, но он использует сторонние реализации (libsodium, HACL *, donna) для криптографии, и пока мое тестирование (в исследовательских целях) еще не выявило выдающихся ошибок.
Чтобы ответить на некоторые комментарии, я скомпилировал и выполнил оригинальный пример OP, слегка измененную версию, исправляющую некоторые ошибки и утечки памяти, и мой пример того, как использовать EVP_DigestSign
API, все скомпилировано с использованием OpenSSL 1.1.0h (скомпилировано как общая библиотека с настраиваемым префиксом из архива выпуска с параметрами конфигурации по умолчанию).
Полную информацию можно найти на этот смысл, который включает в себя точные версии, которые я тестировал, Makefile, содержащий все подробности о том, как были скомпилированы примеры и как выполнялся тест, и сведения о моей машине (кратко, это четырехъядерный процессор i5-6500 @ 3.20 ГГц и freq scaling / Turbo Boost отключен из программного обеспечения и из UEFI).
Как видно из make_output.txt
:
Running ./op_example
time ./op_example >/dev/null
0.32user 0.00system 0:00.32elapsed 100%CPU (0avgtext+0avgdata 3452maxresident)k
0inputs+0outputs (0major+153minor)pagefaults 0swaps
Running ./dsa_example
time ./dsa_example >/dev/null
0.42user 0.00system 0:00.42elapsed 100%CPU (0avgtext+0avgdata 3404maxresident)k
0inputs+0outputs (0major+153minor)pagefaults 0swaps
Running ./evp_example
time ./evp_example >/dev/null
0.12user 0.00system 0:00.12elapsed 99%CPU (0avgtext+0avgdata 3764maxresident)k
0inputs+0outputs (0major+157minor)pagefaults 0swaps
Это показывает, что использование ECDSA через NIST P-256 через EVP_DigestSign
API в 2,66 раза быстрее, чем исходный пример OP, и в 3,5 раза быстрее исправленной версии.
В качестве последнего дополнительного примечания код в этом ответе также вычисляет дайджест SHA256 исходного текста, в то время как исходный код OP и «фиксированная» версия его пропускают.
Поэтому ускорение, продемонстрированное вышеизложенными соотношениями, является еще более значительным!
TL; DR: Правильный способ эффективно использовать цифровые подписи в OpenSSL через EVP_DigestSign
API: пытаюсь использовать DSA_sign_setup()
способ, предложенный выше, неэффективен в OpenSSL 1.1.0 и 1.1.1 и является неправильно (как в полностью нарушая безопасность DSA и раскрывая закрытый ключ) в ≤1.0.2. Я полностью согласен с тем, что Документация по DSA API вводит в заблуждение и должно быть исправлено; к сожалению, функция DSA_sign_setup()
не может быть полностью удален, так как второстепенные выпуски должны сохранять двоичную совместимость, поэтому символ должен оставаться там даже для предстоящего выпуска 1.1.1 (но это хороший кандидат на удаление в следующем основном выпуске).
Я решил удалить этот ответ, потому что он ставит под угрозу усилия команды OpenSSL по обеспечению безопасности их программного обеспечения.
Код, который я разместил, все еще виден, если вы посмотрите на редактирование, но НЕ ИСПОЛЬЗУЙТЕ ЕГО, ЭТО НЕ БЕЗОПАСНО. Если вы делаете, вы рискуете разоблачение вашего личного ключа.
Пожалуйста, не говорите, что вас не предупредили. По факту, относиться к этому как к предупреждению если вы используете DSA_sign_setup()
в вашем собственном коде, потому что вы не должны быть. Ответ Ромена выше содержит более подробную информацию об этом. Спасибо.
Если сообщения большие, принято их хешировать и подписывать. Это намного быстрее. Конечно, вам нужно передать сообщение, хэш и подпись, и процесс проверки должен включать как повторное хеширование и проверку на равенство, так и проверку цифровой подписи.