ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

拆解rsa.rar:RSA密钥生成、加密填充与签名验签核心实现解析

拆解rsa.rar:RSA密钥生成、加密填充与签名验签核心实现解析 简介rsa.rar 压缩包内是一套面向 RSA 非对称加密算法的工程实现源码适合需要深入研究 RSA 原理、从事安全开发或完成密码学课程设计的开发者。整个资源共 24 个文件包括 21 个 C 源文件、2 个头文件和 1 个 MakefileC 源文件主要实现密钥生成、加密解密、数字签名、OAEP/PSS 填充等功能模块头文件用于接口声明Makefile 则方便批量化编译代码覆盖了 RSA 工具集的主要链路。资源整体约 67KB结构紧凑便于阅读和二次编译通过学习这些源码可以直观理解公钥/私钥的生成过程、模幂运算在加解密中的应用以及签名验证的安全机制对于需要落地 RSA 算法的项目或课程设计也能提供可参考的实现蓝本和排错思路。同时可以留意补位逻辑、错误码定义等工程细节有助于避开常见实现陷阱。目前已有 210 人学习对密码学入门和进阶实践都有一定参考价值。1. 一个 rsa.rar 源码包为什么值得拆开看做后端或安全相关开发的人多多少少都遇到过public key解析失败、rsa public key not find、证书签名验证不过这类问题。表面上是 OpenSSL 调用出了岔子实际上往往是对 RSA 底层那几个文件的职责没完全分清。rsa.rar 这个压缩包正好把一串rsa_*.c源码摆在面前比如rsa_gen.c、rsa_crpt.c、rsa_sign.c、rsa_pss.c、rsa_locl.h它们不是某个可以直接双击运行的工具而是一套接近 OpenSSL 早期结构的 RSA 实现集合。对想搞懂 RSA-2048 密钥生成、加密填充、签名验签细节的人来说这包源码能省掉大量翻文档的时间。这个包适合三类人一是要在嵌入式环境或受限平台自己裁剪 RSA 实现的开发者二是做加密模块集成被public key retrieval is not allowed这类连接层报错折腾过的工程师三是想系统补一遍 RSA 数学原理和工程实现之间差距的学习者。下面我按密钥生成、加解密、签名、编译排错这个顺序把包里的文件对应到具体功能上并且给出可复现的调用方式和参数说明。2. rsa_gen.c 与 rsa_locl.hRSA 密钥对生成背后的大素数逻辑2.1 先看 rsa_locl.h 里的密钥结构体rsa.rar 里的rsa_locl.h是内部头文件定义了 RSA 对象的底层结构。OpenSSL 后来把公钥和私钥拆成独立结构体但早期版本把关键参数全放在一个结构体里。通常包含以下成员成员名含义典型长度RSA-2048n模数两个大素数 p、q 的乘积2048 bit256 字节e公钥指数655370x10001d私钥指数约 2048 bitp素数因子1024 bitq素数因子1024 bitdmp1d mod (p-1)1024 bitdmq1d mod (q-1)1024 bitiqmpq 模 p 的逆元1024 bit其中dmp1、dmq1、iqmp是给中国剩余定理CRT加速用的。rsa_gen.c生成密钥对时如果支持 CRT 就会同时算出这三个值后续私钥操作能快 2 到 4 倍。你在代码里看到RSA_FLAG_EXT_PKEY之类的标志位就是告诉引擎不用内部默认的 CRT 参数而是用外部硬件或专用芯片提供的私钥运算。2.2 从 rsa_gen.c 拆 e、d、n 的关系RSA 密钥生成的核心逻辑集中在rsa_gen.c。它的流程是先找两个大素数 p 和 q然后算 n p * q再计算 φ(n) (p-1)(q-1)接着选一个公钥指数 e最后用扩展欧几里得算法求 d使 e*d ≡ 1 mod φ(n)。这段逻辑看起来简单但工程实现里三个点容易写错素数随机性不够、e 与 φ(n) 不互质、d 计算后没有做正数校正。套进 OpenSSL 风格的写法生成 RSA-2048 密钥对的常见做法是这样#include openssl/rsa.h #include openssl/bn.h RSA *rsa RSA_new(); BIGNUM *e BN_new(); BN_set_word(e, RSA_F4); /* 65537 */ int bits 2048; if (RSA_generate_key_ex(rsa, bits, e, NULL) ! 1) { /* 失败时查 rsa_err.c 里的错误队列 */ ERR_print_errors_fp(stderr); return NULL; }这里bits 2048指定模数长度对应 RSA-2048。RSA_F4是 OpenSSL 定义的费马数 F4即 65537。第三个参数NULL是回调函数用于显示进度或中止操作通常不需要。生成后可以访问rsa-n、rsa-e、rsa-d查看具体 BIGNUM 值。2.3 为什么公钥指数固定取 65537rsa_gen.c里如果没有特殊指定e 的默认值通常是RSA_F4。原因有三层安全冗余65537 是一个大质数和任意 p-1、q-1 不互质的概率极低低到可以忽略。计算效率65537 的二进制形式是0b10000000000000001只有两个 1 位。模幂运算时平方次数固定乘法次数只有两次比随机 2048 位指数快几个数量级。兼容性几乎所有 RSA 实现都把 65537 当作默认公钥指数跨平台互认时不用额外校验。有些旧系统用 3 作为 e但e3在未填充明文时容易遭受低指数攻击。rsa_gen.c在生成密钥时会校验 e 必须是奇数且大于 1如果你的代码里使用BN_set_word(e, 3)也能通过但我不建议这么干。2.3.1 Miller-Rabin 素性测试的必要性生成 p、q 时rsa_gen.c不会傻等随机数正好是素数而是用 Miller-Rabin 测试迭代判断。OpenSSL 内部会用 64 轮迭代保证误判率低于 2^-128。如果你要写自己的版本注意不能只试一次 Miller-Rabin小素数过滤比如先查 2048 以内的小素数表也要做否则生产环境会偶发生成弱密钥。下面的伪代码展示了rsa_gen.c生成素数候选的基本套路BIGNUM *p NULL; do { BN_rand(p, bits/2, BN_RAND_TOP_TWO, BN_RAND_BOTTOM_ODD); } while (!BN_is_prime_ex(p, 64, ctx, NULL));参数说明bits/2是 p 和 q 必须接近一半模长且要满足 |p-q| 足够大否则攻击者可能用费马分解直接拆开 n。BN_RAND_TOP_TWO强制最高位为 1保证生成的数接近指定比特长度BN_RAND_BOTTOM_ODD强制最低位为 1保证是奇数减少尝试次数。这里64是 Miller-Rabin 迭代轮数安全性越高轮数越多但生成时间会线性增长。3. rsa_crpt.c 与填充模式RSA-2048 加密和解密的真实工作流3.1 模幂运算与 CRT 优化rsa_crpt.c是 RSA 加解密的核心实现文件。RSA 加密就是计算c m^e mod n解密就是m c^d mod n。这里的mod n运算在 RSA-2048 里意味着指数和模数都是 2048 位直接用蒙哥马利模幂也能工作但私钥操作通常要走 CRT。用 CRT 时私钥运算不是直接算c^d mod n而是拆成两个约 1024 位的运算m1 c^dmp1 mod p; m2 c^dmq1 mod q; h (iqmp * (m1 - m2)) mod p; m m2 q * h;这就是rsa_locl.h里为什么要有dmp1、dmq1、iqmp。实际代码里OpenSSL 的RSA_private_decrypt会自动判断 RSA 对象是否包含这些 CRT 参数如果包含就走 CRT 路径否则 fallback 到普通模幂。用 CRT 对性能提升明显但对侧信道攻击更敏感因为模减运算m1 - m2可能为负需要条件加 p。3.2 从 rsa_pk1.c 到 rsa_oaep.c两种主流填充RSA 加密不能直接输入任意长度的数据RSA-2048 最多处理 256 字节块而且裸的 RSA 加密是确定性的相同明文会产生相同密文无法抵御选择明文攻击。所以标准用法是先用填充模式包一层。rsa_pk1.c实现 PKCS#1 v1.5 填充rsa_oaep.c实现 OAEP 填充。用RSA_public_encrypt加密时填充模式就是第四个参数#include openssl/rsa.h unsigned char msg[] system-design; unsigned char encrypted[RSA_size(rsa)]; int encrypted_len; encrypted_len RSA_public_encrypt(strlen((char*)msg) 1, msg, encrypted, rsa, RSA_PKCS1_OAEP_PADDING); if (encrypted_len -1) { ERR_print_errors_fp(stderr); }RSA_size(rsa)返回 256 字节对应 RSA-2048 的模长。RSA_PKCS1_OAEP_PADDING表示使用 OAEP 填充它能加密的最大明文长度是RSA_size() - 2 * hash_len - 2通常 214 字节。RSA_PKCS1_PADDING是 v1.5 填充可加密到 245 字节但没有随机性。RSA_NO_PADDING只能处理正好 256 字节的整数块只在协议层已经做过处理时才用。解密侧对应RSA_private_decryptunsigned char decrypted[RSA_size(rsa)]; int decrypted_len RSA_private_decrypt(encrypted_len, encrypted, decrypted, rsa, RSA_PKCS1_OAEP_PADDING); if (decrypted_len -1) { /* 最常见错误RSA_padding_check_PKCS1_OAEP_mgf1 */ }填充模式必须和加密时一致。很多人踩过坑加密用 OAEP解密用了默认的RSA_PKCS1_PADDING最终报block type is not 02或padding check failed。rsa.rar 里的rsa_pk1.c和rsa_oaep.c分别实现了两套 padding 检查逻辑建议新项目统一使用 OAEP。3.3 public key 的加载DER/PEM 解析rsa_ameth.c、rsa_asn1.c处理公钥/私钥的 ASN.1 编码。实际工程里 RSA 公钥通常以 PEM 格式存储PEM 文件本质是 base64 编码的 DER 结构。加载公钥时常见的错误是public key retrieval is not allowed这个报错常出现在 MySQL 或 DataGrip 的 client 端因为服务端缓存公钥的机制默认关闭客户端去取公钥时被拒绝。如果你不是连数据库而是在自己代码里解析公钥则更常见的错误是expecting: PUBLIC KEY也就是 PEM 内容被误读。用 OpenSSL 加载 PEM 公钥的标准写法#include openssl/pem.h #include openssl/rsa.h FILE *fp fopen(public.pem, r); RSA *rsa PEM_read_RSA_PUBKEY(fp, NULL, NULL, NULL); fclose(fp); if (rsa NULL) { ERR_print_errors_fp(stderr); return -1; }注意PEM_read_RSA_PUBKEY读取的是-----BEGIN PUBLIC KEY-----格式也就是 SubjectPublicKeyInfo 结构。如果你的文件是-----BEGIN RSA PUBLIC KEY-----需要改用PEM_read_RSAPublicKey。两个函数都定义在rsa_ameth.c周围的 ASN.1 层混淆它们会得到rsa public key not find或解析失败。4. rsa_sign.c 与 rsa_pss.c数字签名从 v1.5 到 PSS 的演进4.1 签名验签的数学镜像RSA 签名本质上和加密共享同一套数学公式但方向相反。签名用私钥s hash^d mod n验签用公钥hash s^e mod n。由于 d 是私密参数只有持有私钥的人能生成签名而任何人都能验签。但直接对哈希值做裸运算并不安全因此需要规定哈希算法、哈希前缀和填充方式。rsa_sign.c负责组装签名消息。它的输入不是完整原文而是原文的摘要值。OpenSSL 的RSA_sign接口签名如下#include openssl/rsa.h #include openssl/evp.h EVP_MD_CTX *mdctx EVP_MD_CTX_new(); EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL); EVP_DigestUpdate(mdctx, msg, msglen); EVP_DigestFinal_ex(mdctx, digest, digest_len); unsigned char signature[RSA_size(rsa)]; unsigned int sig_len 0; int ok RSA_sign(NID_sha256, digest, digest_len, signature, sig_len, rsa);第一个参数NID_sha256是哈希算法的 NID对象标识符它会被编码进 DigestInfo 结构里。验签时RSA_verify会读取这个 NID再用相同的哈希算法重新计算摘要ok RSA_verify(NID_sha256, digest, digest_len, signature, sig_len, rsa);参数不能弄错尤其是digest_len。如果你传入的不是EVP_DigestFinal_ex输出的长度而是sizeof(digest)64 字节rsa_sign.c会认为你用的是 SHA-512从而在解包时失败。4.2 为什么 rsa_sign.c 里能看到哈希前缀早期的rsa_sign.c在签名前要手动拼接DigestInfo也就是哈希算法 OID 加哈希值。现在 OpenSSL 3.0 之后建议用EVP_DigestSign*系列接口自动处理 NID 和前缀。但如果你在维护老代码RSA_sign内部仍然会构造 BER 编码的 DigestInfo这解释了为什么签名长度总是比裸哈希长几十字节。rsa_x931.c实现了 X9.31 签名方案它使用固定的填充格式0x6B开头兼容一些金融行业旧设备。这个文件里的代码不适合新项目但它展示了 RSA 签名填充的另一个分支确定性填充没有随机性。而rsa_pss.c走的是另一条完全不同的路。4.3 PSS 的盐长度与随机性RSA-PSS 是更安全的签名方案它引入随机盐salt让相同消息在不同签名下产生不同输出抵抗伪造攻击。rsa_pss.c的核心函数是RSA_padding_add_PKCS1_PSS和RSA_verify_PKCS1_PSS。实际应用中通常直接用EVP_PKEY_CTX控制#include openssl/evp.h #include openssl/rsa.h EVP_PKEY *pkey EVP_PKEY_new(); EVP_PKEY_assign_RSA(pkey, rsa); EVP_MD_CTX *ctx EVP_MD_CTX_new(); EVP_DigestSignInit(ctx, NULL, EVP_sha256(), NULL, pkey); EVP_PKEY_CTX *pctx EVP_MD_CTX_pkey_ctx(ctx); EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING); EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, RSA_PSS_SALTLEN_DIGEST); EVP_DigestSignUpdate(ctx, msg, msglen); size_t sig_len RSA_size(rsa); EVP_DigestSignFinal(ctx, signature, sig_len);RSA_PKCS1_PSS_PADDING就是函数名里的 PSS。EVP_PKEY_CTX_set_rsa_pss_saltlen控制盐长度三个常用取值如下取值行为适用场景RSA_PSS_SALTLEN_DIGEST盐长等于哈希摘要长度SHA-256 为 32 字节双方约定好的固定盐长RSA_PSS_SALTLEN_AUTO签发时按最大长度验证时自动探测兼容不同实现的最稳选项RSA_PSS_SALTLEN_MAX使用密钥允许的最大盐长但不一定符合 RFC 标准验签时如果报salt length too short多半是对方用RSA_PSS_SALTLEN_AUTO签发而你验证时设成了RSA_PSS_SALTLEN_DIGEST。rsa_pss.c里的代码在验证循环中会对 emBits 和 emLen 做边界检查有时候问题不在盐长度而是哈希算法长度与密钥不匹配比如 RSA-1024 配 SHA-512这时会直接返回RSA_R_DATA_TOO_LARGE_FOR_KEY_SIZE。5. 编译 rsa_test.c 与 rsa_err.c一套可落地的自检与排错方法5.1 用 Makefile 跑通测试程序rsa.rar 里带了一个Makefile可以直接把rsa_test.c编译成独立测试程序。rsa_test.c通常包含基本的密钥生成、加密、解密、签名、验签步骤。在 Linux 上编译时注意链接 OpenSSL 库因为源码依赖 OpenSSL 的 BIGNUM 和 ASN.1 基础设施。常见的编译命令make -f Makefile rsa_test ./rsa_test rsa -b 2048 -e 65537如果 Makefile 里没有定义参数也可以直接手动编译gcc -o rsa_test rsa_test.c rsa_gen.c rsa_crpt.c rsa_pk1.c rsa_oaep.c \ rsa_sign.c rsa_pss.c rsa_err.c rsa_lib.c \ -lcrypto -lssl-lcrypto是 OpenSSL 的加密库里面提供BN_*、ERR_*等基础函数。如果链接时报undefined reference to BN_new说明头文件或库路径不对用pkg-config --cflags --libs openssl获取正确的编译选项。5.2 三个高频错误对应到源码位置错误现象大概率位置解决思路rsa public key not findrsa_ameth.c的 PEM 读取函数确认公钥文件是PUBLIC KEY还是RSA PUBLIC KEY换用PEM_read_RSA_PUBKEY/PEM_read_RSAPublicKeypadding check failedrsa_pk1.c/rsa_oaep.c的 padding 校验加密和解密必须使用完全相同的填充模式检查RSA_private_decrypt最后一个参数digital envelope routines::unsupported签名参数组合查看rsa_pss.c中盐长度设置或改用EVP_DigestSign*代替RSA_sign5.2.1 校验 RSA-2048 密钥对是否匹配拿到一对公私钥后可以用下面的命令快速验证它们是否属于同一个密钥对openssl rsa -in private.pem -pubout -out pub_from_pri.pem openssl pkey -pubin -in public.pem -text -noout openssl pkey -pubin -in pub_from_pri.pem -text -noout第一条命令把私钥里的公钥部分导出然后对比两个公钥的modulus。如果 modulus 不一致说明公私钥不配对。这个操作比写代码调用RSA_check_key更直观RSA_check_key在rsa_lib.c里会验证 npq、e*d ≡ 1 mod λ(n) 等数学约束但它要求私钥对象里同时包含 p、q、dmp1 等 CRT 参数。5.2.2 公因子攻击的规避rsa_test.c还会测试两个独立生成的 RSA 密钥对是否共享素数。如果两个不同证书的 n 有公共因子 p则通过计算gcd(n1, n2)可以直接得到 p进而算出私钥。这就是网络攻击案例中 RSA 公因子攻击的原理。规避方法很简单每次生成密钥必须用足够强熵源的随机数rsa_gen.c里不要用固定的素数表也不要用rand()作为熵源。测试时用下面这行命令快速检查两个公钥文件是否共享因子openssl rsa -pubin -in public1.pem -modulus -noout | tail -c 32 openssl rsa -pubin -in public2.pem -modulus -noout | tail -c 32如果两个 modulus 末尾 32 字节完全一样说明 n 相同若只有部分相同则有可能共享其中一个素数因子。更高精度的做法是写程序调用BN_gcd逐一比较但日常验证用openssl命令已经能发现最明显的弱公钥。最后如果某个公钥的 e 不是 65537也值得检查一下很多弱随机数生成器会导致 e 异常偏小。本文还有配套的精品资源点击获取
返回列表