简介:这是一份用C语言编写的RSA非对称加密算法工具集,面向密码学学习者、安全开发者以及需要快速集成RSA加解密功能的工程师。围绕RSA-2048等常用密钥长度,源码覆盖密钥对生成、公钥加密、私钥解密、数字签名与验签等完整流程,并涉及RSA-PSS、OAEP等填充方案,有助于理解模幂运算和大素数选择背后的数学原理。压缩包共24个文件,包含21个C源文件、2个头文件和1个Makefile;C文件分别实现密钥生成、核心加解密、签名验签、错误处理与测试等模块,头文件负责对外接口声明,Makefile便于一键编译。整体包体仅67KB,结构紧凑、模块边界清晰,适合直接阅读、移植或二次开发。文件划分细致,能够对照源码理解RSA各步骤在工程中的落地形态,各个模块依赖关系简明,从底层大数运算到上层填充与签名接口均有对应实现。已有210人学习下载,无论用于课程设计、安全实验,还是研究RSA内部机制,都是一份轻量而实用的参考资料。
1. RSA 工具、rsa-2048 和“解密”之间隔着密钥形态这道坎
拿到一个名为rsa.rar的压缩包,里面躺着public.pem、private.key,或干脆是一串带-----BEGIN PUBLIC KEY-----头尾的文本,很多人的第一反应是“写段代码把密文解开”。但实际项目里最常卡住你的不是算法本身,而是这三点:公钥是 PEM 还是 DER,是 PKCS#1 还是 SubjectPublicKeyInfo;填充方式是 PKCS1v15 还是 OAEP;密文是 Base64 文本还是裸字节。这三个不确认,openssl、Python、Java 之间互相换手时就会持续报public key not found或Could not deserialize key data。这篇就按“密钥结构 → 最小可跑代码 → 格式坑排查 → 结果验证”的顺序,把 rsa-2048 的加解密、公钥加载和跨语言互操作讲透。适合正在对接第三方接口、写安全工具脚本、或者被“RSA 解密”需求绊住的后端与运维。
2. rsa-2048 的密钥结构先立住:PEM、DER、PKCS#1 与 SubjectPublicKeyInfo
2.0.1 rsa-2048 的数学结构与文件里的样子RSA 的核心是模数 n 和指数 e。rsa-2048 指模数 n 为 2048 位,即 256 字节,这是当前仍被广泛接受的“够用”长度。公钥文件里真正存的不是一张图片或一段口令,而是 ASN.1 编码的 n、e 两个整数;私钥还要额外包含 d、p、q、dmp1、dmq1、iqmp 这些 CRT 参数。文件后缀.pem、.key、.pub并不决定格式,只有文件内容里的头尾标识才说明它属于哪一类。
| 文件头标识 | 实际含义 | 常见后缀 |
|---|---|---|
-----BEGIN RSA PRIVATE KEY----- | PKCS#1 私钥(RSAPrivateKey) | .key |
-----BEGIN PRIVATE KEY----- | PKCS#8 私钥(含算法标识) | .key、.pem |
-----BEGIN PUBLIC KEY----- | SubjectPublicKeyInfo(Java 认这种) | .pub、.pem |
-----BEGIN RSA PUBLIC KEY----- | PKCS#1 公钥(RSAPublicKey) | .pub |
Python 的cryptography库在加载公钥时,load_pem_public_key只认BEGIN PUBLIC KEY这一种,也就是 SubjectPublicKeyInfo。很多人把openssl rsa -pubout生成的公钥直接喂给load_der_public_key,却忘了它输出的是PUBLIC KEY头,这是第一个隐藏雷点。
实际操作里,我一般先用 openssl 看一眼密钥到底是什么结构,再决定代码路径。用openssl rsa -in private.key -noout -text能看到私钥的模数、指数和 CRT 参数。想看 ASN.1 结构本身,用openssl asn1parse -in public.pem -inform PEM,输出会显示一个 SEQUENCE 里嵌套 INTEGER,第一个 INTEGER 是 n,第二个是 e。对于 rsa-2048,n 的字节数正好是 256,十六进制长度 512,这个特征能帮你快速判断手里的密钥是不是 2048 位的。
“解密”这个词在真实接口里常被混用。对方说“RSA 解密”,可能指的是:用私钥解出被公钥加密的密文;用公钥验签;或者用公钥解密——但 RSA 公钥解密在标准接口里几乎不存在,通常只出现在旧系统里。把“解密”当“验签”做,或者反过来,是最浪费时间的错误。下面讲到代码时会给出区分方式:加密/解密走encrypt/decrypt,签名/验签走sign/verify,两组方法永远不能混用。
一个常见的自测方法是:先对同一句话分别做“公钥加密-私钥解密”和“私钥签名-公钥验签”,把两套流程的输入输出字段对齐。如果密文长度不等于 256 字节(2048 bit 密钥加密后密文恒等于密钥长度),那大概率不是 RSA 加密结果,而是 Base64 或其他序列化格式。
3. 用 Python 落地 RSA 解密的最小实现:public key 加载与 rsa-2048 加解密
3.0.1 环境准备与密码学库选型Python 的rsa库足够教学,但生产或工具链我更倾向cryptography。理由有三个:密钥格式自动识别做得更完整;OAEP 填充的参数可显式控制;对 PEM/DER 的转换支持更稳。安装命令是pip install cryptography,不需要额外装 openssl,但命令行验证那步仍会用到系统 openssl。
环境就绪后,先确认版本。python -c "import cryptography; print(cryptography.__version__)"至少要在 3.x 以上,避免老版本里padding.PSS和OAEP的参数行为不一致。接下来从密钥文件读取 RSA 公钥,这一步解决了标题里public key RSA的加载问题,也顺手把rsa public key not find这类报错的根因按住了。
直接用最朴素的写法,把整个流程跑通再谈优化。
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes import base64 # 1. 从 PEM 文件读入公钥 with open("public.pem", "rb") as f: public_key = serialization.load_pem_public_key(f.read()) # 2. 待加密数据 plaintext = b"hello rsa-2048" # 3. 用 OAEP 填充加密,SHA-256 摘要 ciphertext = public_key.encrypt( plaintext, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) # 4. 密文以 Base64 输出,便于在日志中传输 print(base64.b64encode(ciphertext).decode())这段代码看着简单,但四个参数决定了对端能不能解:mgf是掩码生成函数,通常和algorithm保持一致,但老系统可能用 SHA1 做 MGF1,这时候两边会互相解不开;label在绝大多数场景传None,但有个别银行接口会约定固定 label;Base64 输出是惯例,别直接print(ciphertext),二进制会乱码且没法贴到接口调试工具里。
解密侧代码与加密侧对称,唯一区别是private_key.decrypt而不是public_key.encrypt。
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes import base64 # 读取私钥,注意私钥可能带口令 with open("private.key", "rb") as f: private_key = serialization.load_pem_private_key( f.read(), password=b"your-passphrase" # 无口令则传 None ) b64_ciphertext = "base64fromLog" ciphertext = base64.b64decode(b64_ciphertext) plaintext = private_key.decrypt( ciphertext, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) print(plaintext.decode())这里最重要的一步是填充方式必须和加密侧完全一致,否则报ValueError: Decryption failed。出现这个异常时先别检查密钥,先确认两边的 OAEP 参数。PKCS1v15 是另一种填充,代码几乎一样,把padding.OAEP(...)换成padding.PKCS1v15()即可,但二者互不兼容,接口文档里写清用哪个就用哪个。
如果对端说“用公钥验签”,上面代码要整体换掉。
signature_b64 = "base64Signature" signature = base64.b64decode(signature_b64) public_key.verify( signature, b"raw-message", padding.PKCS1v15(), hashes.SHA256() ) print("verify ok")verify方法接受顺序是(signature, data, padding, hash_algorithm),一旦顺序写反,报错信息里只会说Invalid signature,不会提示你参数顺序错了。验签失败时依次排查四个点:消息原文是否包含时间戳等附加字段;签名是否是 Base64 解码过;哈希算法是 SHA1 还是 SHA256;填充算法是 PKCS1v15 还是 PSS。
isinstance(public_key, RSAPublicKey)通过后,如果是从 Java 生成的-----BEGIN RSA PUBLIC KEY-----文件加载,load_pem_public_key会直接拒绝。常见做法是先用 openssl 转一次:
openssl rsa -RSAPublicKey_in -in rsa_pub_pkcs1.pem -outform PEM -pubout -out rsa_pub_spki.pem-RSAPublicKey_in告诉 openssl 输入是 PKCS#1 公钥,-pubout输出为标准 SubjectPublicKeyInfo。转换后文件头会变成BEGIN PUBLIC KEY,Python 就能正常加载。如果不想依赖外部命令,也可以在 Python 里先读文本,替换头尾后尝试加载,但 openssl 做法更可控,也方便你在命令行里排查原始格式。
4. RSA 解密时的密钥加载报错:public key not found 与 DER 转换的 3 个高频坑
4.0.1 报错Could not deserialize key data的真正含义这句话的意思是:输入给load_pem_public_key或load_der_public_key的字节内容,按照预期编码方式解析后得不到合法 ASN.1 结构。换句话说,错误不在密钥内容本身,而在于你告诉解析器“这是 PEM”,实际喂进去的却是 DER 或裸字节;或者告诉它“这是 PKCS#1”,实际却是 SubjectPublicKeyInfo。
排查顺序我固定三步:file public.pem看文件类型;head -1 public.pem看头标识;openssl asn1parse -in public.pem -inform PEM看结构层级。三步走完,80% 的加载问题都能定位。需要说明的是,file命令对纯文本 PEM 会显示ASCII text,对 DER 会显示data或PEM RSA public key,这个输出在不同系统略有差异,重点看第一步的 MIME 描述。
BEGIN RSA PUBLIC KEY与BEGIN PUBLIC KEY混用两类公钥的 ASN.1 根节点不同,前者直接是 RSAPublicKey,后者是 PublicKeyInfo 套一层算法标识。Java 的X509EncodedKeySpec只吃后者,Python 的load_pem_public_key也只吃后者。前端 JS 库(如jsencrypt)默认生成前者,这就成了“前端公钥 Java 后端加载报错”的常见现场。
第 3 章的 openssl 转换命令在这里同样适用,但要注意分支:从 JS 拿到的是 PKCS#1 就加-RSAPublicKey_in;如果已经是 SPKI 格式,不需要做什么,直接用。另一种做法是彻底绕开格式问题,让前端直接输出整段 PEM 文本,后端先按RSA PUBLIC KEY判断,再做分支加载。
Python 里分支加载的写法:
from cryptography.hazmat.primitives import serialization pem_data = open("public.pem", "rb").read() if pem_data.startswith(b"-----BEGIN RSA PUBLIC KEY-----"): key = serialization.load_pem_rsa_rsa_public_key(pem_data) else: key = serialization.load_pem_public_key(pem_data)注意load_pem_rsa_rsa_public_key这个方法名里有双份rsa,对应“RSA PUBLIC KEY”字面。手写时容易漏一个,记成load_pem_rsa_public_key就会直接抛AttributeError。
public key retrieval is not allowed与 RSA 解密的边界划分热词里有个经常被一起搜到的报错public key retrieval is not allowed,但它在绝大多数场景下跟 RSA 算法无关,而是 JDBC 连接 MySQL 时服务端要求客户端做公钥检索却未授权。DataGrip、DBeaver 这类客户端连接报[08001] Public Key Retrieval is not allowed时,常见做法是在 JDBC URL 里加allowPublicKeyRetrieval=true&useSSL=false或useSSL=true,这是 MySQL 认证插件caching_sha2_password的链路问题,不是你的 RSA 公钥文件有问题。
如果一个问题同时涉及“RSA 解密”和“数据库”,通常分为两种独立任务:一是数据库返回了某个字段,里面是 RSA 密文,你需要先公钥解密再做 SQL 查询;二是数据库客户端本身在做密钥协商,报错发生在 TLS 握手阶段,此时去改 RSA 代码没有任何意义。判断关键点在于报错出现的时机:连库阶段报错是客户端配置问题;查询结果解析阶段报错才是你自己的解密逻辑问题。
4.0.4 私钥带口令导致解不开load_pem_private_key的password参数只接受bytes。从配置中心读到的口令通常是字符串,直接传入会报TypeError。正确处理是:
import os password = os.environ.get("RSA_KEY_PASS", "") private_key = serialization.load_pem_private_key( f.read(), password=password.encode("utf-8") if password else None )另一坑是:私钥文件实际上没加密,但工具解密时误把文件内容按带口令解析,也会报格式错误。用openssl rsa -in private.key -noout -text可以马上看出文件中是否含有Proc-Type: 4,ENCRYPTED或类似字段。有就说明带口令,没有就别传password。
rsa-2048 的加密密文恒为 256 字节,解密失败时建议先看密文长度。如果密文比 256 短,大概率是字符串编码问题,比如把 Base64 的+和/在 URL 传参时被替换成空格或%2B后未经还原就做解码。如果比 256 长,可能是密文里面混了换行符,或是对端把密文也用 Base64 后再整体加密了一次,属于双重编码。对于前者,传参后先urllib.parse.unquote再 Base64 解码;对于后者,先 Base64 解码一次,再做一次 Base64 解码,两次结果分别试即可。
5. 用 openssl 和 gpg 校验 RSA 解密结果:从单点打通到工具链闭环
5.0.1 用 openssl pkeyutl 做交叉验证Python 解出来的明文是否正确,不要只看print输出,用 openssl 独立走一遍对照最有说服力。准备一个测试文件:
echo -n "rsa-2048 cross check" > plain.txt openssl pkeyutl -encrypt -pubin -inkey public.pem \ -pkeyopt rsa_padding_mode:oaep \ -pkeyopt rsa_oaep_md:sha256 \ -pkeyopt rsa_mgf1_md:sha256 \ -in plain.txt -out cipher.bin解密侧:
openssl pkeyutl -decrypt -inkey private.key \ -pkeyopt rsa_padding_mode:oaep \ -pkeyopt rsa_oaep_md:sha256 \ -pkeyopt rsa_mgf1_md:sha256 \ -in cipher.bin -out decrypted.txt cat decrypted.txtrsa_padding_mode、rsa_oaep_md、rsa_mgf1_md三个参数与 Python 侧一一对应。注意rsa_oaep_md是 OAEP 的消息摘要,不是 MGF1 的摘要,两者可以不同,但建议显式写成一样,避免后续排查时忽略这个差异。拿到 openssl 的解密结果后,再用 Python 解密同一份cipher.bin并cmp两份明文,一致就说明整条链路参数正确。
这种做法逼着你把日志里的 Base64 密文按字节落盘,反而能提前发现输出时的编码损耗。实际业务里密文在线传输,我建议在日志里记录一个sha256(密文)摘要,排查时只要 PC 端和服务器各自算一遍摘要一致,就说明中途没有被截断或转码。
热词里还有gpg自动解密,这属于另一套基于 RSA 的实测场景。GPG 的自动解密依赖gpg-agent缓存私钥口令,而不是绕过口令,验证方法是用gpg --list-secret-keys看出的指纹:
gpg --list-secret-keys --with-subkey-fingerprint要确认手里的 RSA 公钥与私钥确实配对,不一定要完整加解密一次,可以直接比较两端 RSA 值的摘要。用 Python 做一次极简验证:
public_numbers = public_key.public_numbers() private_numbers = private_key.private_numbers() assert public_numbers.n == private_numbers.public_numbers.n print("rsa modulus match")n一致则公钥私钥必然属于同一对。这个方法在对接第三方时特别实用,不用把私钥发给对方,也不用解密,一条断言即可定位“两边密钥不匹配”的问题。
配置文件路径和参数后,可以把上面三步——openssl 加密、Python 解密、摘要比对——合成一个sh脚本,每次接口文档有变(尤其是 OAEP 的哈希算法调整)时跑一遍。脚本输出统一格式:[PASS] rsa-2048 oaep sha256 ok,失败时把 openssl 的decrypt错误码直接返回,避免在两层管道里丢失原始异常。整套验证做好之后,再回头调public key not found或decrypt failed就都是从已知正确的基线出发,比对着报错猜快得多。
本文还有配套的精品资源,点击获取