.NET 8中EMV发卡行公钥恢复RSA函数单元测试失败求助
EMV发卡行公钥恢复功能单元测试失败排查
问题背景
在.NET 8中实现EMV发卡行公钥恢复功能,CA验证步骤已通过,但自定义PerformRSA函数执行恢复算法后,结果不满足断言(首字节应为0x6A,末字节应为0xBC),自有卡片数据测试同样失败。测试数据取自javaemvreader的IssuerPublicKeyCertificate.java测试用例。
测试代码
// 来自javaemvreader测试用例 var modulus = Convert.FromHexString("be9e1fa5e9a803852999c4ab432db28600dcd9dab76dfaaa47355a0fe37b1508ac6bf38860d3c6c2e5b12a3caaf2a7005a7241ebaa7771112c74cf9a0634652fbca0e5980c54a64761ea101a114e0f0b5572add57d010b7c9c887e104ca4ee1272da66d997b9a90b5a6d624ab6c57e73c8f919000eb5f684898ef8c3dbefb330c62660bed88ea78e909aff05f6da627b"); var data = Convert.FromHexString("8b3901f6253048a8b2cb08974a4245d90e1f0c4a2a69bca469615a71db21ee7b3aa94200cfaedcd6f0a7d9ad0bf79213b6a418d7a49d234e5c9715c9140d87940f2e04d6971f4a204c927a455d4f8fc0d6402a79a1ce05aa3a526867329853f5ac2feb3c6f59ff6c453a7245e39d73451461725795ed73097099963b82ebf7203c1f78a529140c182dbbe6b42ae00c02"); var hash = Convert.FromHexString("ee1511cec71020a9b90443b37b1d5f6e703030f6"); var rid = Convert.FromHexString("A000000003"); var index = (byte)149; byte[] exp = [0x03]; // CA验证步骤(已通过) var ms = new MemoryStream(); ms.Write(rid); ms.WriteByte(index); ms.Write(modulus); ms.Write(exp); var toHash = ms.ToArray(); var hashed = Crypto.SHA1(toHash); Assert.True(Enumerable.SequenceEqual(hash, hashed)); // 公钥恢复算法测试(失败) var recovered = Crypto.PerformRSA(data, exp, modulus); Assert.Equal(0x6A, recovered[0]); Assert.Equal(0xBC, recovered.Last());
自定义PerformRSA实现代码
public static byte[] PerformRSA(byte[] dataBytes, byte[] expBytes, byte[] modBytes) { int inBytesLength = dataBytes.Length; if (expBytes[0] >= (byte)0x80) { // 给指数前置0x00 byte[] tmp = new byte[expBytes.Length + 1]; tmp[0] = (byte)0x00; Array.Copy(expBytes, 0, tmp, 1, expBytes.Length); expBytes = tmp; } if (modBytes[0] >= (byte)0x80) { // 给模数前置0x00 byte[] tmp = new byte[modBytes.Length + 1]; tmp[0] = (byte)0x00; Array.Copy(modBytes, 0, tmp, 1, modBytes.Length); modBytes = tmp; } if (dataBytes[0] >= (byte)0x80) { // 给签名数据前置0x00,避免最高位被解释为符号位 byte[] tmp = new byte[dataBytes.Length + 1]; tmp[0] = (byte)0x00; Array.Copy(dataBytes, 0, tmp, 1, dataBytes.Length); dataBytes = tmp; } System.Numerics.BigInteger exp = new(expBytes); System.Numerics.BigInteger mod = new(modBytes); System.Numerics.BigInteger data = new(dataBytes); byte[] result = System.Numerics.BigInteger.ModPow(data, exp, mod).ToByteArray(); if (result.Length == (inBytesLength + 1) && result[0] == (byte)0x00) { // 移除结果开头的0x00 byte[] tmp = new byte[inBytesLength]; Array.Copy(result, 1, tmp, 0, inBytesLength); result = tmp; } return result; }
问题排查与修复
核心问题在于字节序不匹配和结果格式处理缺失:
- EMV规范中所有RSA相关数据(模数、指数、签名)采用大端(Big-Endian)存储,但.NET
BigInteger默认解析小端(Little-Endian)字节数组,导致数值解析错误。 - 现有代码未处理计算结果的字节序反转,也没有严格对齐EMV要求的公钥长度。
修复后的PerformRSA代码
public static byte[] PerformRSA(byte[] dataBytes, byte[] expBytes, byte[] modBytes) { // 大端字节数组转BigInteger适配方法 static BigInteger BigEndianToBigInteger(byte[] bytes) { byte[] reversed = (byte[])bytes.Clone(); Array.Reverse(reversed); // 若最高位为1,追加0x00避免解析为负数 if ((bytes[0] & 0x80) != 0) { Array.Resize(ref reversed, reversed.Length + 1); reversed[reversed.Length - 1] = 0x00; } return new BigInteger(reversed); } // 解析大端格式的参数 BigInteger exp = BigEndianToBigInteger(expBytes); BigInteger mod = BigEndianToBigInteger(modBytes); BigInteger data = BigEndianToBigInteger(dataBytes); // 执行RSA模幂运算 BigInteger resultNum = BigInteger.ModPow(data, exp, mod); // 将计算结果转回大端字节数组 byte[] resultBytes = resultNum.ToByteArray(); // 移除末尾的符号位0x00 if (resultBytes.Length > 0 && resultBytes[^1] == 0x00) { Array.Resize(ref resultBytes, resultBytes.Length - 1); } // 反转得到大端格式 Array.Reverse(resultBytes); // 对齐到模数长度(EMV公钥恢复后长度需匹配模数) int expectedLength = modBytes.Length; if (resultBytes.Length < expectedLength) { byte[] padded = new byte[expectedLength]; Array.Copy(resultBytes, 0, padded, expectedLength - resultBytes.Length, resultBytes.Length); resultBytes = padded; } else if (resultBytes.Length > expectedLength) { byte[] truncated = new byte[expectedLength]; Array.Copy(resultBytes, resultBytes.Length - expectedLength, truncated, 0, expectedLength); resultBytes = truncated; } return resultBytes; }
修复说明
- 字节序转换:通过反转字节数组,将EMV大端数据适配为.NET
BigInteger要求的小端格式,同时处理符号位避免负数解析。 - 结果格式还原:计算完成后反转字节数组,恢复为EMV要求的大端格式,并通过补0或截断确保长度与模数一致,符合EMV公钥恢复的格式规范。
内容的提问来源于stack exchange,提问作者Tolu Ogunremi
相关产品推荐
相关产品推荐

