However, Signing and Verifying Signatures using BouncyCastle on an IOS system (Simulator and phone produce the same results) occasionally (and by occasionally I mean quite frequently) produces what appears to be invalid signatures.
In an attempt to make sure I wasn't doing anything wrong, I wrote a short test program which would randomly generate 100 sets of data, sign each set 10000 times, and verify each signature. A pure BouncyCastle implementation of the desired behavior on Windows - and it worked, 1 million out of 1 million times.
This code also appears to work when tested on Android; again, 100% of the time.
However, when it comes to testing it on IOS, the same results cannot be replicated.
Here are the results testing on IOS (Each test round consisted of shutting down the simulator, then running the IOS program via Visual Studio. Then, the program generates a new private/public key pair and signs a random set of data bytes 10000 times):
The testing was originally only on the IPhone XR, but I thought that it might be a problem with the specific model of phone, so I also did the test using the IPhone X. There are more results from previous testing, but they pretty much look the same as the samples listed below, even if the test was done on a physical IPhone XR.
I have no idea how Test Rounds 1 & 2 came out with 10001 runs instead of 10000 - After starting the test, there was zero interaction with the process until it hit the exit breakpoint.
Test round 2 somehow managed to perform an extra four tests, corresponding with 4 failing tests recorded - The only thing that changed during this test was that I set a breakpoint mid loop in order to check on the progress, and then hit continue
Additionally, we decided to check the endian-ness of both systems; what if they were mismatched? The search, however, was unfruitful, as a quick check in the immediate window of the debugger via BitConverter.IsLittleEndian
showed that both systems were using Little Endian format.
The code used to test, modified to sign only 10000 times with one set of data
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);
}
As Peter Dettman pointed out, the private key was not being created using the signed version of the BigInteger
constructor.
Along with changing that line to use the signed version of the constructor, the test code was also modified to move the initialization of the signer into the body of the for loop.
if (barr.Length == 31)
{
barr = new byte[32];
Array.Copy(theInt.PositiveValue.ToByteArrayUnsigned(), 0, barr, 1, 31);
}
was also changed to
if (barr.Length < 32)
{
barr = new byte[32 - barr.Length].Concat(barr).ToArray();
}
in order to account for the fact that there is no theoretical lower limit to the byte array length of barr
.
After these changes, the code appears to be functioning properly in all cases.