c# - IOS 上不一致的 Bouncy Castle ECDSA 签名/验证行为
问题描述
问题:
我希望在不同平台/系统上使用相同的 nuget 包签名和验证签名以类似的方式运行。但是,在 IOS 系统上使用 BouncyCastle 签名和验证签名(模拟器和手机会产生相同的结果)偶尔(我的意思是偶尔会非常频繁)会产生看似无效的签名。
我尝试了什么:
为了确保自己没有做错任何事情,我编写了一个简短的测试程序,它会随机生成 100 组数据,对每组数据进行 10000 次签名,并验证每个签名。在 Windows 上实现所需行为的纯 BouncyCastle 实现 - 它在 100 万次中成功了 100 万次。
在 Android 上测试时,此代码似乎也可以工作;再次,100%的时间。
但是,在 IOS 上进行测试时,无法复制相同的结果。
下面是 IOS 上的测试结果(每轮测试包括关闭模拟器,然后通过 Visual Studio 运行 IOS 程序。然后,程序生成一个新的私钥/公钥对,并对一组随机数据字节签名 10000 次) :
本来只在iPhone XR上测试的,但我觉得可能是具体型号的手机有问题,所以我也用iPhone X做了测试。之前的测试结果比较多,但是看起来都差不多与下面列出的示例相同,即使测试是在物理 iPhone XR 上完成的。
iPhone XR IOS 12.2
- 测试第 1 轮:10001个签名在 10001 个中通过验证?
- 测试第 2 轮:10001个签名中的10000个通过验证?
- 第 3 轮测试:10000个签名通过验证,共10000个
- 测试第 4 轮:10000个签名通过验证
我不知道第 1 轮和第 2 轮测试是如何以 10001 次而不是 10000 次运行的 - 开始测试后,与进程的交互为零,直到它到达退出断点。
iPhone X IOS 12.2
- 测试第 1 轮:9996个签名通过验证,共10000个
- 第 2 轮测试:10004个签名中有10000个通过验证?
- 第 3 轮测试:10000个签名中有0个通过验证
第 2 轮测试以某种方式设法执行了额外的四个测试,对应于记录的 4 个失败测试 - 在此测试期间唯一改变的是我设置了一个断点中间循环以检查进度,然后点击继续
此外,我们决定检查两个系统的字节序;如果它们不匹配怎么办?然而,搜索没有结果,因为在调试器的即时窗口中快速检查BitConverter.IsLittleEndian
显示两个系统都使用 Little Endian 格式。
用于测试的代码,修改为仅用一组数据签名10000次
static void SigningTest(byte[] data, byte[] pubkey, byte[] privkey)
{
var curve = SecNamedCurves.GetByName("secp256r1");
var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);
var d = new Org.BouncyCastle.Math.BigInteger(privkey);
var xx = new Org.BouncyCastle.Math.BigInteger(1, pubkey.Take(32).ToArray());
var yy = new Org.BouncyCastle.Math.BigInteger(1, pubkey.Skip(32).ToArray());
var q = curve.Curve.CreatePoint(xx, yy);
var publicParams = new ECPublicKeyParameters(q, domain);
var privateParams = new ECPrivateKeyParameters(d, domain);
var cipherkp = new AsymmetricCipherKeyPair(publicParams, privateParams);
var signer = SignerUtilities.GetSigner("SHA256withECDSA");
signer.Init(true, cipherkp.Private);
var ccount = 0;
var icount = 0;
for (var i = 0; i < 10000; i++)
{
signer.BlockUpdate(data, 0, data.Length);
var signature = signer.GenerateSignature();
var der = Asn1Object.FromByteArray(signature) as DerSequence;
var arrList = new List<byte[]>();
foreach (DerInteger theInt in der)
{
var barr = theInt.PositiveValue.ToByteArrayUnsigned();
if (barr.Length == 31)
{
barr = new byte[32];
Array.Copy(theInt.PositiveValue.ToByteArrayUnsigned(), 0, barr, 1, 31);
}
arrList.Add(barr);
}
var realsig = new byte[64];
Array.Copy(arrList[0], realsig, arrList[0].Length);
Array.Copy(arrList[1], 0, realsig, arrList[0].Length, arrList[1].Length);
if (Verify(data, pubkey, realsig))
{
ccount++;
}
else
{
icount++;
}
}
// Add something here like System.Diagnostics.Debugger.Break() so that a break point can be set.
}
static bool Verify(byte[] data, byte[] publicKey, byte[] signature)
{
var curve = SecNamedCurves.GetByName("secp256r1");
var x = new byte[32];
var y = new byte[32];
Array.Copy(publicKey, 0, x, 0, 32);
Array.Copy(publicKey, 32, y, 0, 32);
var derSignature = new DerSequence(
new DerInteger(new Org.BouncyCastle.Math.BigInteger(1, signature.Take(32).ToArray())),
new DerInteger(new Org.BouncyCastle.Math.BigInteger(1, signature.Skip(32).Take(32).ToArray()))
)
.GetDerEncoded();
var xx = new Org.BouncyCastle.Math.BigInteger(1, publicKey.Take(32).ToArray());
var yy = new Org.BouncyCastle.Math.BigInteger(1, publicKey.Skip(32).ToArray());
var domainparams = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());
var ecp = curve.Curve.CreatePoint(xx, yy);
var pubkeyparams = new ECPublicKeyParameters(ecp, domainparams);
var verifier = SignerUtilities.GetSigner("SHA256withECDSA");
verifier.Init(false, pubkeyparams);
verifier.BlockUpdate(data, 0, data.Length);
return verifier.VerifySignature(derSignature);
}
那么,如何解决这个问题呢?
解决方案
正如 Peter Dettman 所指出的,私钥不是使用BigInteger
构造函数的签名版本创建的。
除了更改该行以使用构造函数的签名版本外,还修改了测试代码以将签名者的初始化移动到 for 循环的主体中。
if (barr.Length == 31)
{
barr = new byte[32];
Array.Copy(theInt.PositiveValue.ToByteArrayUnsigned(), 0, barr, 1, 31);
}
也被改为
if (barr.Length < 32)
{
barr = new byte[32 - barr.Length].Concat(barr).ToArray();
}
为了说明 . 的字节数组长度没有理论上的下限这一事实barr
。
在这些更改之后,代码似乎在所有情况下都能正常运行。
推荐阅读
- jquery - 加载带有承诺的脚本,jQuery 没有加载
- python - 我想在 CMD 窗口中显示来自其他应用程序(LabVIEW)的结果
- ios - 改变 SwiftUI .toolbar 的一面
- amazon-web-services - EC2 Instance Connect 挂起 aws-cli 调用
- flutter - Flutter:如何将参数传递给 FutureBuilder 未来实例?
- python-3.x - 如何在 Python 中创建重复的日期
- spotify - 如何定义从 preview_url (Spotify Web API) 获得的曲目的 30 秒?
- macos - 在 macOS 上使用 cmake 强制以 .so 版本而不是 .dylib 构建共享库
- rust - 如何将 Into 的引用传递给另一个函数?
- python - 在主线程外运行 fastapi gunicorn