IOS上不一致的Bouncy Castle ECDSA签名/验证行为

时间:2019-06-25 22:36:13

标签: c# ios xamarin.forms xamarin.ios bouncycastle

问题:

我希望在不同的平台/系统上使用相同的nuget包对签名和验证签名进行行为类似。

但是,在IOS系统上使用BouncyCastle签名和验证签名(模拟器和电话会产生相同的结果)有时(有时是很频繁地)产生看起来无效的签名。

我尝试过的事情:

为了确保我没有做错任何事情,我编写了一个简短的测试程序,该程序将随机生成100组数据,对每组进行10000次签名,并验证每个签名。在Windows上实现所需行为的纯BouncyCastle实现-可以工作,一百万次中的一百万次。

此代码在Android上测试时似乎也可以使用;同样,有100%的时间。

但是,在IOS上进行测试时,无法复制相同的结果。

这是在IOS上进行的结果测试(每轮测试包括关闭模拟器,然后通过Visual Studio运行IOS程序。然后,该程序生成一个新的私钥/公钥对,并对一组随机的数据字节进行签名10000次):

该测试最初仅在iPhone XR上进行,但我认为这可能与特定型号的手机有关,因此我也使用iPhone X进行了测试。以前的测试有更多结果,但即使在物理iPhone XR上进行了测试,它们的外观也几乎与下面列出的示例相同。

iPhone XR IOS 12.2

  • 第1轮测试: 10001 ,签名已通过 10001?
  • 的验证?
  • 第2轮测试: 10000 ,签名通过了 10001吗?
  • 第3轮测试: 10000 ,签名通过了 10000
  • 验证
  • 第4轮测试: 0 ,签名通过了 10000
  • 验证

我不知道测试回合1和2如何以10001次而不是10000次运行-开始测试后,与流程的交互为零,直到达到退出断点为止。

iPhone X IOS 12.2

  • 第1轮测试: 9996 ,签名通过了 10000
  • 验证
  • 第二轮测试: 10000 ,签名通过了 10004吗?
  • 第3轮测试: 0 ,签名通过了 10000
  • 个验证

第2轮测试设法以某种方式执行了额外的4个测试,与记录的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);
}

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

1 个答案:

答案 0 :(得分:0)

正如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的字节数组长度没有理论下限的事实。

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