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

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

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

在这个应用程序中,我有很多消息(即几千条)需要使用相同的私有 key/public 密钥分别签名(当然还要验证)。我正在使用 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);
}

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

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

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

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

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

DSA_sign_setup() 生成一个新的 运行dom 随机数(即每个签名的临时密钥)。它应该 永远不会 在相同的长期密钥下重复使用。 从不.

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

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


使用 EVP_DigestSign API

的概念证明

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

以下示例扩展了 this OpenSSL wiki page 中的 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=%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

Performance/Randomness

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

Ed25519

要使用 Ed25519(从 OpenSSL 1.1.1 开始本机支持),需要修改上面的示例,因为在 OpenSSL 1.1.1 中不支持 Ed25519ph,而不是使用 Init/Update/Final 流式传输 API 您需要调用一次性 EVP_DigestSign() 接口(参见 documentation)。

完全免责声明下一段是我的libsuola研究项目的无耻插曲,因为我绝对可以从真实测试中受益-来自其他用户的世界应用.

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

OP原始例子和我的基准比较

为了解决一些评论,我编译并执行了 OP 的原始示例,这是一个稍微修改过的版本,修复了一些错误和内存泄漏,以及我如何使用 EVP_DigestSign API 的示例,全部针对 OpenSSL 1.1.0h 编译(使用默认配置参数从发布 tarball 编译为自定义前缀的共享库)。

可以在 this gist 找到完整的详细信息,其中包括我进行基准测试的确切版本、包含示例编译方式和基准测试方式的所有详细信息的 Makefile 运行,以及关于我的机器的详细信息(简而言之,它是四核 i5-6500 @ 3.20GHz,软件和 UEFI 禁用了频率 scaling/Turbo 提升)。

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_DigestSign API 在 NIST P-256 上使用 ECDSA 比原始 OP 示例快 2.66 倍,比更正版本快 3.5 倍。

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


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