RSA 这套东西在 C# 里最有意思的地方在于算法本身十几年没变过但密钥的表达格式换了一茬又一茬。你从对端系统拿到的往往是一个.pem文本文件或者一串-----BEGIN PUBLIC KEY-----开头的字符串而你手上跑的 .NET Framework 老项目RSACryptoServiceProvider只认 XML。于是“PEM 转 XML”这件小事成了无数对接项目里的第一道坎。这篇文章就把这条链路完整走一遍PEM 文件怎么拆、XML 里那些Modulus、Exponent、DP、InverseQ分别对应什么、公钥加密和私钥签名在代码里怎么写、分段长度怎么算、以及我在实际对接中踩过的那些坑。不管你是刚接手一个对接任务的初中级开发还是想把手上的加密工具类整理干净的老手下面这些内容都能直接拿去用。1. 先把问题摸清楚为什么 PEM 要转 XML1.1 两套密钥表达体系的由来PEM 和 XML 说的其实是同一件事只是用了两种“方言”。PEM 本质上是DER 编码的二进制 Base64 文本 头尾标记这套东西来自 OpenSSL 生态几乎所有非 Windows 阵营的加密工具、服务端、证书体系都在用。XML 那套RSAKeyValue则是微软早期在 .NET Framework 里定下的序列化格式RSACryptoServiceProvider的ToXmlString()和FromXmlString()就是围绕它设计的。两套东西拆到底描述的数学对象完全一样一个模数 n一个公开指数 e私钥再额外加 d、p、q、dp、dq、qInv。区别只在于怎么把这些大整数摆成字节数组——PEM 里用 ASN.1 的INTEGER大端无符号编码XML 里用 Base64 直接对同一批大端字节做文本化。弄明白这一点转换这件事就不再神秘了转的是包装不是数据本身。我见过不少人一上来就去搜现成的转换工具网站把私钥粘到网页上转一下。这个做法风险极高——私钥一旦离开你自己的机器就等于交出去了。自己写二十行代码就能搞定的事没必要冒这个险。1.2 什么场景下必须做这层转换最常见的是三种情况。第一种老项目对接新平台。对方给的文档写得清清楚楚“使用 RSA 私钥签名SHA256withRSA密钥格式 PEM”而你手上是个 .NET Framework 4.5 的项目RSACryptoServiceProvider.FromXmlString()是唯一顺手的入口于是必须先转成 XML。这种场景下你还不能随便升级框架因为项目里可能挂着一堆老依赖。第二种配置文件的形态限制。有些系统的密钥配置项就是一段 XML塞在web.config或者某个appsettings里中途换格式会牵动一堆部署脚本。我在一个项目里就遇到过这种情况运维那边的密钥注入流程已经固化成 XML 模板了代码侧只能配合。第三种跨语言协作。同一套密钥Java 那边读 PEM 读得好好的C# 这边要跟它对齐总得有一边做转换。这时候统一在 C# 侧转成 XML 内部使用对外仍然按 PEM 交付是改动面最小的方案。反过来说如果你的项目跑在 .NET 5 及以上RSA.Create()直接就有ImportFromPem()那就根本不需要转 XML 这一步。硬转反而多了一层出错机会。所以选型的第一步不是“怎么写转换代码”而是“我这套运行时到底支不支持原生读 PEM”。1.3 方案选型三条路线怎么挑路线依赖适用运行时优点代价原生ImportFromPem无.NET 5 / .NET Core 3.0代码最短官方维护老框架用不了BouncyCastleNuGet 包全平台支持加密私钥、各种曲线多一个依赖包体积变大手写 ASN.1 解析无全平台零依赖、原理透明需要自己处理边界情况我个人的取舍原则很直接能上 .NET 5 就用原生 API一行搞定被钉死在 .NET Framework 又不想引第三方包就手写解析RSA 的结构其实很简单两百行以内能写完且可读性不差需要处理-----BEGIN ENCRYPTED PRIVATE KEY-----这种带密码的私钥那就别自己造轮子直接上 BouncyCastle。这里提前说一个很多人忽略的点转换方向最好做成双向的。今天你把 PEM 转成 XML明天对接另一个平台可能又要 XML 转 PEM。把RSAParameters作为中间层两边各写一个转换函数中间这层统一后面加什么格式都只是补一个函数的事。2. PEM 文件的解剖课头尾、Base64 与 DER2.1 四种常见PEM头部对应什么拿到一个 PEM 文件第一件事是看头尾那一行。这一行决定了里面装的是什么结构猜错了后面全错。-----BEGIN PUBLIC KEY-----X.509 的SubjectPublicKeyInfo里面是算法标识 一个 BIT STRINGBIT STRING 里才是真正的RSAPublicKey结构。这是目前最主流的公钥格式。-----BEGIN RSA PUBLIC KEY-----裸的 PKCS#1RSAPublicKey只有 n 和 e 两个整数没有外层包装。老一些的库会产出这种。-----BEGIN PRIVATE KEY-----PKCS#8 的PrivateKeyInfo外层带算法标识真正的私钥在privateKey这个 OCTET STRING 里内容还是 PKCS#1 的RSAPrivateKey。现在新生成的私钥基本都是这个格式。-----BEGIN RSA PRIVATE KEY-----裸的 PKCS#1RSAPrivateKey九个整数平铺。OpenSSL 老版本默认产出。这四种头部在代码里必须分开处理。我见过最典型的错误是拿到-----BEGIN PRIVATE KEY-----的 PKCS#8 文件按 PKCS#1 的结构去解析结果解出来的第一个 INTEGER 是版本号 0被当成了模数后面全乱套报错信息还特别含糊——“数据无效”。所以解析之前先做头部判别是必须的第一道工序。注意还有一种-----BEGIN ENCRYPTED PRIVATE KEY-----那是用口令加密过的 PKCS#8不能直接解必须先解密。这个不在本文范围内遇到了直接用 BouncyCastle 处理别硬啃。2.2 DER里的ASN.1结构把 PEM 的头尾行去掉把中间的 Base64 解出来得到的就是 DER 字节流。DER 是 ASN.1 的一种编码规则格式很规整每个元素都是Tag Length Value三段式。Tag 一个字节常见的有0x30SEQUENCE表示一个复合结构、0x02INTEGER大整数、0x03BIT STRING、0x04OCTET STRING。Length 稍微麻烦一点小于 128 时用一个字节直接表示大于等于 128 时首字节的高位置 1低 7 位表示后面还有几个字节用来存长度。比如0x82 0x01 0x0A表示长度是 266 字节。这个“长格式长度”是手写解析器最容易写漏的地方2048 位密钥的 SEQUENCE 长度一般都在 128 以上一定会走到长格式。PKCS#1 私钥解出来的 SEQUENCE 展开就是九个 INTEGER顺序固定version, modulus(n), publicExponent(e), privateExponent(d), prime1(p), prime2(q), exponent1(dp), exponent2(dq), coefficient(qInv)PKCS#8 则是三层外层 SEQUENCE 包含version、AlgorithmIdentifier、privateKeyOCTET STRING。把privateKey里的字节掏出来再按上面的 PKCS#1 结构解一遍。公钥那边X.509 的 BIT STRING 里掏出来的内容是SEQUENCE { modulus, publicExponent }只有两个整数。2.3 密钥长度与XML字段长度的对应关系XML 的RSAKeyValue里每个元素存的是对应整数的大端字节序 Base64不带符号位。这里有个硬性约束必须记住P、Q、DP、DQ、InverseQ的长度必须严格等于Modulus长度的一半向上取整。2048 位密钥Modulus是 256 字节那这五个字段每个都必须是 128 字节。少了要左边补 0x00多了要从左边删掉 0x00。为什么这么严格因为 .NET 在ImportParameters时是按定长切分去做 CRT 运算的长度不对就直接抛CryptographicException。从 DER 里解出来的整数是变长的ASN.1 的 INTEGER 会去掉前导零所以解完之后必须做一次定长补齐。这个补齐逻辑是转换代码里最容易漏掉的一环也是“转换成功了但导入失败”的头号原因。D字段的长度则等于Modulus的长度同样需要补齐。Exponent一般就是 3 字节的0x01 0x00 0x01也就是 65537不用补。3. 实操从PEM到XML的完整转换3.1 路线A.NET 5 内置API如果你的项目能跑在 .NET 5 及以上这段代码就够了不需要任何第三方库using System.Security.Cryptography; public static string PemToXml(string pem, bool isPrivate) { using var rsa RSA.Create(); rsa.ImportFromPem(pem); // 自动识别 PUBLIC KEY / RSA PUBLIC KEY / PRIVATE KEY / RSA PRIVATE KEY var p rsa.ExportParameters(isPrivate); return ToXml(p, isPrivate); }ImportFromPem的好处是会自己看头尾行决定怎么解PKCS#1 和 PKCS#8 都能吃。ExportParameters(true)拿私钥参数false拿公钥参数。注意如果你手上只有公钥 PEM调ExportParameters(true)会抛异常因为私钥参数不存在。所以isPrivate这个标志要由调用方明确传进来不能靠 try-catch 猜。反过来如果只需要密钥参数而不需要 XML.NET 5还有更直接的ImportFromPem加ExportRSAPrivateKeyPem()可以省掉 XML 这层。3.2 路线BBouncyCastle老框架或者需要处理加密私钥时BouncyCastle 是稳妥选择。装BouncyCastle.Cryptography这个包然后using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using Org.BouncyCastle.Asn1; using System.Security.Cryptography; public static RSAParameters ParsePrivateKey(string pem) { var der Convert.FromBase64String(StripPemHeader(pem)); var keyParam PrivateKeyFactory.CreateKey(der); var rsaKey (RsaPrivateCrtKeyParameters)keyParam; return new RSAParameters { Modulus rsaKey.Modulus.ToByteArrayUnsigned(), Exponent rsaKey.PublicExponent.ToByteArrayUnsigned(), D rsaKey.Exponent.ToByteArrayUnsigned(), P rsaKey.P.ToByteArrayUnsigned(), Q rsaKey.Q.ToByteArrayUnsigned(), DP rsaKey.DP.ToByteArrayUnsigned(), DQ rsaKey.DQ.ToByteArrayUnsigned(), InverseQ rsaKey.QInv.ToByteArrayUnsigned() }; }这里有个我强烈推荐的做法不要用PemReader.ReadObject()去读。因为它对不同类型的输入返回的对象类型不一样PKCS#1 返回AsymmetricCipherKeyPair或RsaPrivateCrtKeyParametersPKCS#8 返回PrivateKeyInfo你得写一堆is判断。更稳的方式是自己剥掉头尾、解 Base64 拿 DER 字节直接丢给PrivateKeyFactory.CreateKey(der)——它内部会自己识别 PKCS#1 和 PKCS#8返回统一类型。这一步能省掉大量类型判断代码。ToByteArrayUnsigned()这个方法是关键它返回的是去掉符号位的大端字节数组正好符合RSAParameters的要求。如果误用ToByteArray()会多出一个前导 0x00 或者出现负数补码导入必定失败。3.3 路线C手写ASN.1解析零依赖不想引包的话可以自己写。核心就是一个能读 TLV 的小工具类internal sealed class DerReader { private readonly byte[] _buf; private int _pos; public DerReader(byte[] buf) { _buf buf; _pos 0; } public byte PeekTag() _buf[_pos]; // 读一个 TLV返回 tag 和内容切片 public (byte Tag, byte[] Value) Read() { byte tag _buf[_pos]; int len _buf[_pos]; if ((len 0x80) ! 0) { int n len 0x7F; len 0; for (int i 0; i n; i) len (len 8) | _buf[_pos]; } var value new byte[len]; Buffer.BlockCopy(_buf, _pos, value, 0, len); _pos len; return (tag, value); } public bool HasMore _pos _buf.Length; }拿它解 PKCS#1 私钥就是一个 SEQUENCE 里连续读九个 INTEGERpublic static RSAParameters ParsePkcs1Private(byte[] der) { var outer new DerReader(der); var seq outer.Read(); // 0x30 var inner new DerReader(seq.Value); inner.Read(); // version丢弃 var n inner.Read().Value; var e inner.Read().Value; var d inner.Read().Value; var p inner.Read().Value; var q inner.Read().Value; var dp inner.Read().Value; var dq inner.Read().Value; var qInv inner.Read().Value; int half (n.Length 1) / 2; return new RSAParameters { Modulus n, Exponent e, D PadLeft(d, n.Length), P PadLeft(p, half), Q PadLeft(q, half), DP PadLeft(dp, half), DQ PadLeft(dq, half), InverseQ PadLeft(qInv, half) }; } private static byte[] PadLeft(byte[] src, int len) { var t src.SkipWhile(b b 0x00).ToArray(); // 去掉 ASN.1 的符号位前导零 if (t.Length len) throw new CryptographicException(密钥分量长度异常); if (t.Length len) return t; var r new byte[len]; Buffer.BlockCopy(t, 0, r, len - t.Length, t.Length); return r; }PadLeft里那个SkipWhile很关键因为 ASN.1 的 INTEGER 是带符号的最高位为 1 时编码器会在前面加一个 0x00 防止被解释成负数。解出来的P有可能就是 129 字节而不是 128直接塞进RSAParameters会报错。先去掉前导零再定长补齐这一步不能省。PKCS#8 的处理就是多剥一层外层 SEQUENCE 读出版本、算法标识、OCTET STRING把 OCTET STRING 的 Value 再喂给上面的ParsePkcs1Private。逻辑不复杂写一次能用很久。3.4 RSAParameters 与 XML 的互转有了RSAParameters转 XML 就是拼字符串。注意元素顺序微软的解析器对顺序有要求私钥必须是Modulus, Exponent, P, Q, DP, DQ, InverseQ, Dpublic static string ToXml(RSAParameters p, bool includePrivate) { var sb new StringBuilder(RSAKeyValue); void Add(string name, byte[] v) sb.Append().Append(name).Append() .Append(Convert.ToBase64String(v)) .Append(/).Append(name).Append(); Add(Modulus, p.Modulus); Add(Exponent, p.Exponent); if (includePrivate) { Add(P, p.P); Add(Q, p.Q); Add(DP, p.DP); Add(DQ, p.DQ); Add(InverseQ, p.InverseQ); Add(D, p.D); } return sb.Append(/RSAKeyValue).ToString(); }反方向解析 XML 时RSACryptoServiceProvider.FromXmlString()是最省事的但它只在 Windows 上有效.NET Core 3.0 之前甚至直接抛PlatformNotSupportedException。跨平台场景我一般用System.Security.Cryptography.Xml或者自己用XDocument解析public static RSAParameters FromXml(string xml) { var doc XDocument.Parse(xml); byte[] Get(string n) Convert.FromBase64String( doc.Descendants(n).First().Value); var p new RSAParameters { Modulus Get(Modulus), Exponent Get(Exponent) }; if (doc.Descendants(D).Any()) { int half (p.Modulus.Length 1) / 2; p.D PadLeft(Get(D), p.Modulus.Length); p.P PadLeft(Get(P), half); p.Q PadLeft(Get(Q), half); p.DP PadLeft(Get(DP), half); p.DQ PadLeft(Get(DQ), half); p.InverseQ PadLeft(Get(InverseQ), half); } return p; }这里我特意用了同样的PadLeft因为有些系统导出的 XML 会把前导零去掉长度就不是定长的了。多补一次不会错少补一次跑不通。3.5 转换结果的验证方法转完之后不要急着往业务里接先做一次自检用转换出来的私钥签一段字再用转出来的公钥验签。两边都成功说明转换的正确性基本没问题因为签名验签同时用到了 n、d、e 三个关键分量。如果验签失败问题一定出在转换环节不用去怀疑业务代码。我习惯在单元测试里放一个固定密钥的往返测试PEM → XML → RSAParameters → XML → PEM最后比对原始 PEM 的 Modulus 是否一致。这比肉眼检查靠谱得多。4. 公钥加密与私钥签名把算法用对4.1 加密与签名不是对称的用法初学者最容易搞混的一点是方向问题。用 RSA 做加密是拿对方的公钥加密对方用自己的私钥解密用 RSA 做签名是拿自己的私钥签名对方用你的公钥验签。这两个流程用的是同一对数学关系但密钥的持有方向正好相反。在实际对接里经常是四把密钥同时在用你持有自己的私钥用于给请求签名和对方的公钥用于验证对方返回的签名对方持有你的公钥和自己的私钥。配置表里如果只写了两个字段“publicKey / privateKey”很容易被误配成同一对结果就是签名能过但加密解不开或者反过来。我现在的做法是配置文件里明确写myPrivateKey、theirPublicKey、myPublicKey、theirPrivateKey如果确实需要双向命名到位了就不会配错。另一个容易翻车的地方是用途隔离。同一对密钥既用来做签名又用来做加密密码学上不是好习惯虽然功能上能跑。如果对端支持尽量要求它对签名和加密使用不同的密钥对。4.2 填充模式选择与长度上限计算RSA 加密前要对明文做填充主流两种PKCS#1 v1.5和OAEP。前者兼容性好几乎所有的老系统都支持后者安全性更高但需要两端都支持。对接老系统时你往往没有选择权文档说用 PKCS#1 v1.5 就只能用 v1.5。长度上限必须自己算清楚。RSA 一次能加密的明文长度是填充方式单块最大明文长度2048 位密钥256 字节的实际值PKCS#1 v1.5模长 - 11245 字节OAEP SHA-1模长 - 42214 字节OAEP SHA-256模长 - 66190 字节超出这个长度直接抛CryptographicException: 数据太长。注意是字节数不是字符数中文按 UTF-8 编码一个字符占 3 字节245 字节只够放七八十个汉字。这个细节很多人栽过——本地测试用的是短英文串一切正常上线后用户输入了一长段备注直接崩。对应代码using var rsa RSA.Create(); rsa.ImportParameters(pubParams); // PKCS#1 v1.5 byte[] cipher rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1); // OAEP SHA-256 byte[] cipher2 rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256);对端说“用 PKCS1 填充”通常指的是 v1.5说“用 OAEP”要追问清楚是 SHA-1 还是 SHA-256这两种不兼容配错了解密端会报“解码失败”这类含糊的错误。4.3 签名与验签的实现要点签名的标准流程是先对原文做哈希再用私钥对哈希值做运算。SignData这个方法帮我们把两步合在一起了只需要指定哈希算法和填充方式using var rsa RSA.Create(); rsa.ImportParameters(privParams); byte[] data Encoding.UTF8.GetBytes(payload); byte[] signature rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); string sign Convert.ToBase64String(signature);验签反过来using var rsa RSA.Create(); rsa.ImportParameters(pubParams); bool ok rsa.VerifyData(data, Convert.FromBase64String(sign), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);三个参数必须两端完全一致哈希算法、填充方式、待签字节。前两个不一致会直接验签失败第三个最容易出问题——我在 5.3 节细说。在 .NET Framework 上做 SHA256 签名有个历史遗留坑RSACryptoServiceProvider默认的 CSP 类型不支持 SHA256会抛“指定的算法无效”或者干脆给你算出一个错的签名。解决办法是在构造时指定增强型 CSPvar csp new CspParameters { ProviderType 24 }; // PROV_RSA_AES using var rsa new RSACryptoServiceProvider(csp); rsa.FromXmlString(privXml); byte[] sig rsa.SignData(data, SHA256);这个ProviderType 24是我排查了大半天才翻出来的文档里提得很隐晦。只要你在 .NET Framework 4.x 上做 SHA256 或以上的签名加这行基本能解决大部分“签名无效”的怪问题。4.4 大报文怎么办分段与混合加密超过单块上限的数据有两种处理方式。分段加密把原文按模长 - 11切开逐块加密每块输出的密文长度都等于模长最后拼接。解密时按模长切块逐块解。这种做法实现简单但密文体积会膨胀到原文的好几倍2048 位密钥处理 10KB 数据会得到约 11KB 密文传输开销不小。const int blockSize 245; // 2048 位 PKCS1 v1.5 var output new MemoryStream(); for (int i 0; i data.Length; i blockSize) { int len Math.Min(blockSize, data.Length - i); var chunk new byte[len]; Buffer.BlockCopy(data, i, chunk, 0, len); var enc rsa.Encrypt(chunk, RSAEncryptionPadding.Pkcs1); output.Write(enc, 0, enc.Length); }混合加密才是更推荐的做法随机生成一个 32 字节的 AES 密钥用 RSA 加密这个密钥用 AES 加密正文把“加密后的密钥 密文”一起发出去。这种方式性能好、体积小也是 TLS 等协议的标准思路。缺点是两端都要实现 AES 部分对接成本比纯 RSA 高。如果对端文档里写的是“RSA 加密请求体”一般就是指分段这时候按它的规则来就行别自作主张换方案。5. 踩坑实录与排查速查表5.1 密钥格式类报错“数据无效”或“ASN1 corrupted data”八成是头尾行没剥干净或者把 PKCS#8 当成 PKCS#1 解了。排查方法很简单把 Base64 解出来的前几个字节打印出来看0x30开头是正常的 SEQUENCE如果第一个 INTEGER 读出来是 0 而且后面跟的东西不像大整数基本可以确定是 PKCS#8 或者根本不是密钥。“证书或密钥格式不正确”检查 PEM 里有没有混入中文空格、全角字符或者复制时把换行丢了。Convert.FromBase64String能自动忽略\r\n和空格但忽略不了全角空格。有一次对端给的密钥里粘进了一个 U00A0 不间断空格肉眼完全看不出来查了两小时才发现。“找不到指定的模块”在 Linux 容器里跑 .NET Framework 的代码不会有这个问题但反过来在 .NET Core 里用了RSACryptoServiceProvider的某些重载就可能碰上。跨平台的项目统一用RSA.Create()加ImportParameters能绕开一大类平台相关的问题。私钥导入成功但签名结果不对检查 XML 里是不是缺了P、Q、DP、DQ、InverseQ。只给 n、e、d 三个值在数学上是成立的但 .NET 的实现会走 CRT 快速路径缺了这些分量要么报错要么在某些版本上静默算出错误结果。所以私钥 XML必须八项齐全。5.2 加密解密类报错“要加密的数据超过了此密钥允许的最大长度”回到 4.2 节的表算清楚字节数该分段就分段。注意中文的 UTF-8 字节数。“解密失败”或返回乱码九成是填充方式不匹配或者密钥配错了对象。先确认对端用的是 v1.5 还是 OAEP再看公钥是不是对方的。另外密文在传输过程中如果被 URL 编码、Base64 换行、或者被 JSON 转义过转回来可能已经变了样。建议密文一律做一次 URL-safe Base64 处理避免、/、在 URL 里出问题。“参数不正确”常见于ImportParameters时Modulus长度和P长度不匹配。检查P是不是严格等于Modulus.Length / 2。5.3 签名验签类报错验签失败是最高频的问题排查顺序建议这样走第一步确认待签字节完全一致。这是最难查的一类。你把参数按a1b2拼对方可能按a1b2末尾多一个 或者按字典序排过或者对值做了 URL 编码而你没做。我现在的习惯是在调试阶段把待签的原始字符串打进日志生产环境记得关掉或者脱敏双方对着这一串字比对一眼就能看出差别。第二步确认编码。Encoding.UTF8.GetBytes和Encoding.Default.GetBytes在中文环境下结果不同老项目里用Default的很常见。还有 BOM 的问题用File.ReadAllText读进来的内容有时候会带 BOM签名原文里凭空多三个字节。第三步确认 Base64 的处理。对方给的签名如果是 URL-safe Base64需要用-换、_换/再补。如果签名经过 URL 编码先解码再转字节。第四步确认哈希和填充。SHA256 对 SHA1、PKCS1 对 PSS这四个组合里配错了任何一个都验不过。5.4 一张速查表现象最可能的原因快速验证方式ImportFromPem 抛格式异常头尾行不完整或含不可见字符打印前 64 字节十六进制看是否 0x30 开头导入成功但签名验不过待签原文不一致把原文打日志双方逐字符比对签名长度不对应为模长用错了填充或算法2048 位签名固定 256 字节加密超长报错明文超过单块上限按 UTF-8 计算字节数解密得到乱码填充模式不匹配试 v1.5 与 OAEP 两种.NET Framework 上 SHA256 签名异常CSP 类型不支持构造时指定 ProviderType 24公钥 PEM 调 ExportParameters(true) 抛异常公钥本就没有私钥分量用 false或改用公钥专用流程5.5 我自己的几条硬规矩第一个密钥绝不硬编码在源码里。哪怕是个内部工具也放进配置中心或者受保护的配置文件并且加上读取权限控制。源码仓库的提交历史是会一直留着的密钥进了历史就得整个轮换。第二个转换逻辑做成独立工具类配单元测试。PEM 转 XML 这种代码一次写对之后很少改动但它一旦出错排查成本极高。我把几个典型的 PEM 样本PKCS#1 私钥、PKCS#8 私钥、X.509 公钥、PKCS#1 公钥都放进了测试工程每次改完跑一遍往返测试心里踏实。第三个日志里不打印私钥和签名原文。待签字符串在调试环境可以打生产环境只打它的哈希前八位够定位问题又不泄露内容。第四个接口对接阶段先做一次“最小验证”用一对自己生成的密钥走完整的签名验签、加密解密流程确保代码逻辑没问题再换成对端给的密钥测一次确保密钥本身没问题。这两步分开做出问题时能立刻定位是代码还是密钥的锅。我吃过这个亏——两边密钥格式都拿到了代码也写好了一测就失败折腾半天最后发现是对端给的密钥本身是另一套环境的。第五个RSA 密钥长度别低于 2048 位。1024 位的密钥现在的算力下已经不安全了新对接的项目如果对方还提供 1024 位尽量协商升级。老系统改不动的另说但至少要心里有数。代码写到这里基本就完整了。我个人在实际操作中的体会是这类加密对接的难点从来不在算法本身而在于格式的边界和两端约定的一致性。把 PEM 的结构拆明白、把长度补齐的逻辑写对、把待签原文对齐剩下的都是常规编码工作。真正让人加班的永远是那个多出来的换行符和那个看不见的全角空格。
企业数字化 ERP 产品动态
相关推荐
Redis核心数据结构解析与Java实战指南 1. Redis 基础解析:从原理到实战作为一名长期使用 Redis 的 Java 开发者,我经常被问到:"Redis 到底快在哪里?为什么我们项目要用它?" 这个问题看似简单,但真正理解 Redis 的核心优势,… · 2026/11/3 8:25:08
SAP EWM110定制主线:部署集成、策略配置与易漏点 简介:SAP EWM110基础定制参与者手册(课程版本17)面向SAP EWM顾问、仓库信息化实施人员及供应链系统学习者,围绕扩展仓库管理系统的基础配置与业务落地,帮助读者建立从系统环境搭建到仓内作业优化的配置框架。压缩包内为… · 2026/11/3 8:24:07
电力系统虚假数据注入攻击原理与防御实践 1. 电力系统安全防线的新威胁:虚假数据注入攻击去年某区域电网调度中心发生了一起诡异事件——状态估计系统持续显示线路负荷正常,实际却发生了多台变压器过载跳闸。事后溯源发现,攻击者通过入侵量测终端,向SCADA系统注入了精心构… · 2026/11/3 8:24:07
前端小白也能掌握的Agent开发,升级你的产品体验与职业价值 本文深入浅出地介绍了Agent开发的核心概念与前端在其中的关键作用,指出Agent开发并非前端技能的边缘延伸,而是对其原有能力的升级与拓展。文章强调前端在理解用户意图、状态管理、交互设计及业务流程方面的优势,为前端转向Agent开发提供了清晰… · 2026/11/3 19:24:22
10 个惊艳 3D 场景盘点:Qwen3.5-4B-3D-Coder 官方作品画廊深度赏析 人工智能大模型代码模型微调本地部署3D渲染Qwen 【免费下载链接】Qwen3.5-4B-3D-Coder 项目地址: https://ai.gitcode.com/hf_mirrors/ccore/Qwen3.5-4B-3D-Coder 点击查看 免费下载 Qwen3.5-4B-3D-Coder 是一个由 CCORE 基于 Qwen3.5-4B 独立微调的 4B 代码大模型… · 2026/11/3 19:24:22
脑科学前沿 | 聚焦:脑结构图谱与脑功能解码(大样本影像、记忆重放、脑深部核团、BCI 解码) 脑科学前沿 | 今日速览 2026.10.10 今日收录:10篇| 聚焦:脑结构图谱与脑功能解码(大样本影像、记忆重放、脑深部核团、BCI 解码) 情报周期:2026.10.07 – 2026.10.10(72小时) 今日推荐… · 2026/11/3 19:24:22
【Java】线程概念 1.Thread 类中 start() 和 run() 的区别方法本身区别(1)start()a)是启动线程的方法,属于Thread的原生方法。b)调用后会向操作系统申请创建一个新的独立线程,由 JVM 自动调用该线程的run()方法。c࿰… · 2026/11/3 19:24:22
全网都在扒“作者是谁“,没人关心它干了啥:百度这个开源OCR,被一段八卦盖住了真本事 全网都在扒"作者是谁",没人关心它干了啥:百度这个开源OCR,被一段八卦盖住了真本事 【免费下载链接】Unlimited-OCR-2 项目地址: https://ai.gitcode.com/hf_mirrors/baidu/Unlimited-OCR-2
今年6月,百度在GitHu… · 2026/11/3 19:23:22
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46