有效创建数字签名的正确方法是什么?我可以使用DSA_sign_setup()吗?

What is the proper way to efficiently create digital signatures? Can I use DSA_sign_setup()?

本文关键字:DSA sign setup 可以使 我可以 数字签名 创建 是什么 方法 有效      更新时间:2023-10-16

我正在开发一个性能至关重要的应用程序。

在这个应用程序中,我有很多消息(即数千条)需要使用相同的私钥/公钥分别签名(当然和验证)。我正在使用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("%dn",a);
a=DSA_generate_key(set);
printf("%dn",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 %dn",a,size);
}

以上面提出的方式使用DSA_sign_setup()实际上是完全不安全的,幸运的是,OpenSSL开发人员使DSA结构不透明,因此开发人员无法尝试强行

DSA_sign_setup()生成一个新的随机随机数(这是每个签名的临时密钥)。它永远不应该在同一个长期密钥下重复使用。从来没有

从理论上讲,您仍然可以相对安全地为同一消息重用相同的随机数,但是一旦私钥和随机数的相同组合被重用于两个不同的消息,您就会泄露攻击者检索密钥所需的所有信息(参见Sony fail0verflow,这基本上是由于在ECDSA中重用随机数的相同错误)。

不幸的是,DSA很慢,特别是现在需要更长的密钥:为了加快应用程序的速度,您可以尝试使用ECDSA(例如,使用曲线NISTP256,仍然没有随机数重用)或Ed25519(确定性随机数)。


使用EVP_DigestSignAPI 进行概念验证

更新:这是如何使用 OpenSSL 以编程方式生成签名的概念证明。 首选方法是使用EVP_DigestSignAPI,因为它抽象出正在使用哪种非对称密钥。

以下示例扩展了此 OpenSSL wiki 页面中的 PoC:我使用 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=%lun", msg, slen);
}
printf("DONEn");
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

性能/随机性

我在我(英特尔 Skylake)机器和树莓派 3 上运行了您的原始示例和代码。在这两种情况下,您的原始示例都不需要几十秒。 鉴于显然您在 OpenSSL 1.0.2 中使用不安全DSA_sign_setup()方法(除了一些有点昂贵的模块化算术之外,它还在内部消耗新的随机性)时看到了巨大的性能改进,我怀疑您实际上可能对 PRNG 有问题,它正在减慢新的随机随机数的生成速度,并且比模块化算术运算的影响更大。 如果是这种情况,您肯定会从使用 Ed25519 中受益,因为在这种情况下,随机数是确定性的而不是随机的(它是使用安全哈希函数并结合私钥和消息生成的)。 不幸的是,这意味着您需要等到OpenSSL 1.1.1发布(希望在今年夏天)。

在 Ed25519 上

要使用 Ed25519(从 OpenSSL 1.1.1开始本机支持),需要修改上述示例,因为在 OpenSSL 1.1.1 中不支持 Ed25519ph,而不是使用Init/Update/Final流 API,您需要调用一次性EVP_DigestSign()接口(请参阅文档)。

完全免责声明下一段是我的libsuola研究项目的无耻插件,因为我绝对可以从其他用户的实际应用程序测试中受益

或者,如果您迫不及待,我是一个名为libsuola的 OpenSSLENGINE的开发人员,该在 OpenSSL 1.0.2、1.1.0(以及使用替代实现的 1.1.1)中添加了对 Ed25519 的支持。它仍处于实验阶段,但它使用第三方实现(libsodium,HACL*,donna)用于加密部分,到目前为止,我的测试(用于研究目的)尚未发现突出的错误。

OP原样与矿的对标比较

为了解决一些评论,我编译并执行了OP的原始示例,一个略微修改的版本修复了一些错误和内存泄漏,以及如何使用EVP_DigestSignAPI的示例,所有这些都针对OpenSSL 1.1.0h编译(编译为共享库,使用默认配置参数从发布压缩包中自定义前缀)。

完整的细节可以在这个要点中找到,其中包括我基准测试的确切版本,Makefile包含有关如何编译示例以及如何运行基准测试的所有详细信息,以及有关我的机器的详细信息(简而言之,它是一个四核i5-6500 @ 3.20GHz,并且频率缩放/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

这表明,通过EVP_DigestSignAPI 在 NIST P-256 上使用 ECDSA 比原始 OP 的示例快 2.66 倍,比更正后的版本快 3.5 倍。

作为后期的补充说明,此答案中的代码还计算输入明文的 SHA256 摘要,而 OP 的原始代码和"固定"版本跳过它。 因此,上面报告的比率所证明的加速比更加显着!


TL;DR:在OpenSSL中有效使用数字签名的正确方法是通过EVP_DigestSignAPI:尝试以上述方式使用DSA_sign_setup()在OpenSSL 1.1.0和1.1.1中无效,并且在≤1.0.2中是错误的(如完全破坏DSA的安全性并泄露私钥)。我完全同意DSA API文档具有误导性,应该修复;不幸的是,函数DSA_sign_setup()无法完全删除,因为次要版本必须保留二进制兼容性,因此即使对于即将发布的 1.1.1 版本,该符号也需要保留在那里(但在下一个主要版本中是一个很好的候选者)。

我决定删除这个答案,因为它损害了OpenSSL团队确保软件安全的努力。

如果您查看编辑,我发布的代码仍然可见,但不使用它,它是不安全的。 如果这样做,则可能会暴露您的私钥

请不要说你没有被警告。 事实上,如果您在自己的代码中使用DSA_sign_setup(),请将其视为警告,因为您不应该这样做。 Romen上面的回答对此有更多细节。 谢谢。

如果消息很大,通常对它们进行安全哈希并对哈希进行签名。这要快得多。当然,您需要传输消息、哈希和签名,并且检查过程必须包括重新哈希和检查相等性以及数字签名验证。