Использование DSA_sign_setup()
способом, предложенным выше, на самом деле совершенно небезопасно, и, к счастью, разработчики OpenSSL сделали структуру DSA непрозрачной, чтобы разработчики не могли попытаться прорваться.
DSA_sign_setup()
генерирует новый случайный одноразовый номер(это своего рода эфемерный ключ на подпись).Он никогда не должен использоваться повторно под тем же долгосрочным секретным ключом. Никогда .
Теоретически все еще можно относительно безопасно повторно использовать один и тот же одноразовый номер для того же сообщения, но как только одна и та же комбинация личного ключа и одноразового номера будет повторно использована для двух разных сообщений, которые вы простораскрыть всю информацию, которая необходима злоумышленнику для получения вашего секретного ключа (см. Sony fail0verflow , что в основном связано с той же ошибкой повторного использования одноразового номера с ECDSA).
К сожалению, DSA работает медленно, особенно теперь, когда требуются более длинные ключи: для ускорения работы вашего приложения вы можете попробовать использовать ECDSA (например, с кривой NISTP256, по-прежнему без повторного использования одноразового номера) или Ed25519 (детерминированный одноразовый номер).
Подтверждение концепции с использованием EVP_DigestSign
API
Обновление: - вот подтверждение концепции того, как программно генерировать подписи с 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 (надеюсь, этим летом).
On Ed25519
Для использования Ed25519 (который будет поддерживаться изначально, начиная с OpenSSL 1.1.1) приведенный выше пример необходимо изменить, так как в OpenSSL 1.1.1 отсутствует поддержка Ed25519ph, и вместо использования потокового API Init
/ Update
/ Final
вам потребуется вызывать одноразовыйEVP_DigestSign()
интерфейс (см. документацию ).
Полный отказ от ответственности : следующий абзац - бесстыдная заглушка для моего libsuola исследованияпроект, поскольку я определенно мог бы извлечь выгоду из тестирования реальных приложений от других пользователей .
В качестве альтернативы, если вы не можете ждать, я являюсь разработчиком OpenSSL ENGINE
под названием libsuola
, который добавляетподдержка Ed25519 в OpenSSL 1.0.2, 1.1.0 (а также 1.1.1 с использованием альтернативных реализаций).Он все еще экспериментальный, но он использует сторонние реализации (libsodium, HACL *, donna) для криптографии, и пока мое тестирование (в исследовательских целях) еще не выявило выдающихся ошибок.
Сравнительное сравнение OPоригинальный пример и мой
Чтобы обратиться к некоторым комментариям, я скомпилировал и выполнил оригинальный пример OP, слегка измененную версию, исправляющую некоторые ошибки и утечки памяти, и мой пример того, как использовать EVP_DigestSign
API, всескомпилирован с использованием OpenSSL 1.1.0h (скомпилирован как общая библиотека с пользовательским префиксом из архива выпуска с параметрами конфигурации по умолчанию).
Полную информацию можно найти в этом гисте , который включает в себя точные версии, которые я тестировал, Makefile, содержащий все подробности о том, как были скомпилированы примеры и как выполнялся тест, и сведения о моеммашина (кратко, это четырехъядерный процессор i5-6500 @ 3.20 ГГц, а масштабирование частоты / 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 - через API EVP_DigestSign
: попытка использовать DSA_sign_setup()
способом, предложенным выше, неэффективнав OpenSSL 1.1.0 и 1.1.1 и неправильно (как в , полностью нарушающем безопасность DSA и раскрывающим закрытый ключ ) в ≤1.0.2.Я полностью согласен с тем, что документация DSA API вводит в заблуждение и должна быть исправлена;к сожалению, функция DSA_sign_setup()
не может быть полностью удалена, так как второстепенные выпуски должны сохранять двоичную совместимость, поэтому символ должен оставаться там даже в предстоящем выпуске 1.1.1 (но является хорошим кандидатом на удаление в следующем основном выпуске).