【问题标题】:How to correctly generate a RSA key pair in C?如何在 C 中正确生成 RSA 密钥对?
【发布时间】:2021-04-13 16:47:35
【问题描述】:

我正在尝试使用以下函数在 C 中生成 RSA 密钥对:

int generate_key(const int bits, char* public_key_name, char* private_key_name){
    int ret = 0;
    RSA *r = NULL;
    BIGNUM *bne = NULL;
    FILE *bp_public = NULL;
    FILE* bp_private = NULL;
    
    unsigned long   e = RSA_F4;

    // 1. generate rsa key
    bne = BN_secure_new();
    ret = BN_set_word(bne,e);
    if(ret != 1){
        goto free_all;
    }

    r = RSA_new();
    ret = RSA_generate_key_ex(r, bits, bne, NULL);
    if(ret != 1){
        goto free_all;
    }

    // 2. save public key
    bp_public = fopen(public_key_name, "w+");
    ret = PEM_write_RSAPublicKey(bp_public, r);
    fclose(bp_public);
    if(ret != 1){
        goto free_all;
    }

    // 3. save private key
    bp_private = fopen(private_key_name, "w+");
    ret = PEM_write_RSAPrivateKey(bp_private, r, NULL, NULL, 0, NULL, NULL);
    fclose(bp_private);

    // 4. free
    free_all:
    RSA_free(r);
    BN_clear_free(bne);

    return ret;
}

它工作正常,除了当我使用公钥验证签名消息时,由于EVP_VerifyFinal 而弹出错误(error:0909006C:PEM routines:get_name:no)。请注意,奇怪的是,EVP_VerifyFinal 的返回值不是0,而是-1:如果是0,那么这无疑是签名验证失败,但事实并非如此,它发生了一些其他的事情,我想不通。在下面我过去了用于签名和验证的代码。

void sign(const unsigned char* const restrict clear_buf, const unsigned long clear_size, unsigned char* restrict* const restrict sgnt_buf, unsigned long* const restrict sgnt_size, const char* const restrict prvkey_file_name){
   const EVP_MD* md;
   EVP_MD_CTX* md_ctx;
   int ret;
   FILE* prvkey_file;
   EVP_PKEY* prvkey;
   unsigned expected_sgn_size;
   unsigned tmp;
   
   if(clear_size > INT_MAX){
       fprintf(stderr, "Buffer to sign too big\n");
       exit(1);
   }
   
   if((prvkey_file = fopen(prvkey_file_name, "r")) == NULL){
       fprintf(stderr, "Error: cannot open file '%s' (missing?)\n", prvkey_file_name);
       exit(1);
   }
   prvkey = PEM_read_PrivateKey(prvkey_file, NULL, NULL, NULL);
   fclose(prvkey_file);
   expected_sgn_size = (unsigned) EVP_PKEY_size(prvkey);

   if((*sgnt_buf = (unsigned char*)malloc((size_t)expected_sgn_size)) == NULL){
       fprintf(stderr, "Error in allocating memory for signature. Error: %s\n", strerror(errno));
       exit(1);
   }
   // create the signature context:
   md = EVP_sha256();
   md_ctx = EVP_MD_CTX_new();
   if(!md_ctx){
       fprintf(stderr, "Error: EVP_MD_CTX_new returned NULL\n");
       exit(1);
   }

   if(EVP_SignInit(md_ctx, md) == 0){
       fprintf(stderr, "Error: EVP_SignInit returned %d\n", ret);
       exit(1);
   }
   if(EVP_SignUpdate(md_ctx, clear_buf, (unsigned)clear_size) == 0){
       fprintf(stderr, "Error: EVP_SignUpdate returned %d\n", ret);
       exit(1);
   }
   if(EVP_SignFinal(md_ctx, *sgnt_buf, &tmp, prvkey) == 0){
       fprintf(stderr, "Error: EVP_SignFinal returned %d\n", ret);
       exit(1);
   }
   if(tmp < expected_sgn_size){
       fprintf(stderr, "Error in signing, signature size does not match expected size\n");
       exit(1);
   }
   
   *sgnt_size = (unsigned long)tmp;
   
   // delete the digest and the private key from memory:
   EVP_MD_CTX_free(md_ctx);
   EVP_PKEY_free(prvkey);
}
void verify(const unsigned char* const restrict file_buf, unsigned long* const restrict file_size, const char* const restrict pubkey_file_name){
   // declare some useful variables:
   int ret;
   FILE* pubkey_file;
   EVP_PKEY* pubkey;
   unsigned char* sgnt_buf;
   unsigned sgnt_size;
   
   pubkey_file = fopen(pubkey_file_name, "r");
   if(!pubkey_file){
       fprintf(stderr, "Error: cannot open file '%s' (missing?)\n", pubkey_file_name);
       exit(1);
   }
   pubkey = PEM_read_PUBKEY(pubkey_file, NULL, NULL, NULL);
   sgnt_size = (unsigned) EVP_PKEY_size(pubkey);
   fclose(pubkey_file);
   
   const EVP_MD* md = EVP_sha256();
   EVP_MD_CTX* md_ctx;
   
   *file_size -= (unsigned long)sgnt_size;
   if((sgnt_buf = (unsigned char*)malloc((size_t)sgnt_size)) == NULL){
       fprintf(stderr, "Error in allocating memory for signature. Error: %s\n", strerror(errno));
       exit(1);
   }
   memcpy((void*)sgnt_buf, (void*)(file_buf + *file_size), (size_t)sgnt_size);
   
   // create the signature context:
   md_ctx = EVP_MD_CTX_new();
   if(!md_ctx){
       fprintf(stderr, "Error: EVP_MD_CTX_new returned NULL\n");
       exit(1);
   }

   if(EVP_VerifyInit(md_ctx, md) == 0){
       fprintf(stderr, "Error in EVP_VerifyInit\n");
       exit(1);
   }
   if(EVP_VerifyUpdate(md_ctx, file_buf, *file_size) == 0){
       fprintf(stderr, "Error in EVP_VerifyUpdate\n");
       exit(1); 
   }
   ret = EVP_VerifyFinal(md_ctx, sgnt_buf, sgnt_size, pubkey);
   if(ret == 0){ // it is 0 if invalid signature, -1 if some other error, 1 if success.
       fprintf(stderr, "Error: EVP_VerifyFinal failed: invalid signature\n");
       exit(1);
   } else if(ret == -1){
       fprintf(stderr, "Some error occured during signature verification\n");
       exit(1);
   }else if (ret == 1){
       // fprintf(stdout, "Signature verified\n");
   }else{
       fprintf(stderr, "I shouldn't be printed. EVP_VerifyFinal returned %d\n", ret);
       exit(1);
   }
   EVP_MD_CTX_free(md_ctx);
   EVP_PKEY_free(pubkey);
   free(sgnt_buf);
}

在网上我找到了一些解决方案,但它们不在 C 语言中,或者/并且它们已被弃用并不能解决问题(我在此处找到的一个解决方案会生成一个私钥,它甚至不会为某些人签名原因...)

【问题讨论】:

  • 您使用 PEM_write_RSAPublicKey 编写 pubkey,这是“旧”算法特定的 PKCS1 格式,并尝试使用 PEM_read_PUBKEY 读取它,它需要基于 X.509 的通用格式,它不会不行;您的“密钥”实际上是空的,尽管您没有检测到这一点,并且使用空/不存在的密钥(可能还有空签名缓冲区,因为 malloc(0) 依赖于实现)进行验证不起作用。 In general whenever an OpenSSL libcrypto routine returns a failure value you should display the error stack.
  • 另外您应该知道 RSA 签名始终是密钥的大小,即EVP_PKEY_size(x),但对于其他算法(如 DSA 和 ECDSA)而言并非如此,因此假设签名必须是并且这是固定的尺寸限制了您的使用。
  • @dave_thompson_085 好的,我(想我)明白了,但是我应该在代码中修改什么?我是 OpenSSL 的新手
  • 最简单的解决方案是使用PEM_write_RSA_PUBKEY 编写公钥,这将与您的读取兼容。

标签: c openssl rsa


【解决方案1】:

您的验证码做出了不正确的假设。

如果您阅读了EVP_PKEY_size 文档,则说明:

VP_PKEY_size() 返回适合输出的最大大小 几乎所有可以用 pkey 完成的操作的缓冲区。

sign 函数根据来自EVP_SignFinal 的返回值正确返回报告的签名大小。

验证功能假设签名的大小是 EVP_PKEY_size 的返回值。这很可能是不正确的。

您需要更改“存储”签名的方式,以便还将其大小存储在某处。您可以确认大小看起来正确,因为它必须始终

例如如果您的存储格式如下:

[文件数据][签名][大小]EOF

您可以将验证更改为如下所示:

void verify(const unsigned char* const file_buf, unsigned long* const file_size, const char* const pubkey_file_name){
   // declare some useful variables:
   ...
   unsigned sgnt_size;
   unsigned max_sgnt_size;
   
   ...

   max_sgnt_size = (unsigned) EVP_PKEY_size(pubkey);

   ...

   if(*file_size < sizeof(unsigned)) {
       fprintf(stderr, "Error: File size to small\n");
       exit(1);
   }

   *file_size -= sizeof(unsigned);
   sgnt_size = *(const unsigned *)(file_buf + *file_size);
   if(sgnt_size > max_sgnt_size) {
       fprintf(stderr, "Error: Invalid signature size\n");
       exit(1);
   }

   if(sgnt_size <= *file_size) {
       fprintf(stderr, "Error: Invalid file size\n");
       exit(1);
   }

   *file_size -= (unsigned long)sgnt_size;

   ...
}

【讨论】:

  • 这通常是个问题,但不是 RSA; RSA 签名(和密码)始终是模数的(相同)大小。参见例如RFC 8017 第 4 节。