首页 > 解决方案 > 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 轮和第 2 轮测试是如何以 10001 次而不是 10000 次运行的 - 开始测试后,与进程的交互为零,直到它到达退出断点。

iPhone X IOS 12.2

第 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);
}

那么,如何解决这个问题呢?

标签: c#iosxamarin.formsxamarin.iosbouncycastle

解决方案


正如 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

在这些更改之后,代码似乎在所有情况下都能正常运行。


推荐阅读