Skip to content

Commit dd61fed

Browse files
authored
Add sign/verify schnorr using bouncy castle (#404)
1 parent 1bf52da commit dd61fed

6 files changed

Lines changed: 317 additions & 43 deletions

File tree

src/Blockcore/NBitcoin/BouncyCastle/math/BigInteger.cs

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,47 +1523,41 @@ internal bool RabinMillerTest(int certainty, Random random, bool randomlySelecte
15231523
// return true;
15241524
// }
15251525
//
1526-
// private static int Jacobi(
1527-
// BigInteger a,
1528-
// BigInteger b)
1529-
// {
1530-
// Debug.Assert(a.sign >= 0);
1531-
// Debug.Assert(b.sign > 0);
1532-
// Debug.Assert(b.TestBit(0));
1533-
// Debug.Assert(a.CompareTo(b) < 0);
1534-
//
1535-
// int totalS = 1;
1536-
// for (;;)
1537-
// {
1538-
// if (a.sign == 0)
1539-
// return 0;
1540-
//
1541-
// if (a.Equals(One))
1542-
// break;
1543-
//
1544-
// int e = a.GetLowestSetBit();
1545-
//
1546-
// int bLsw = b.magnitude[b.magnitude.Length - 1];
1547-
// if ((e & 1) != 0 && ((bLsw & 7) == 3 || (bLsw & 7) == 5))
1548-
// totalS = -totalS;
1549-
//
1550-
// // TODO Confirm this is faster than later a1.Equals(One) test
1551-
// if (a.BitLength == e + 1)
1552-
// break;
1553-
// BigInteger a1 = a.ShiftRight(e);
1554-
//// if (a1.Equals(One))
1555-
//// break;
1556-
//
1557-
// int a1Lsw = a1.magnitude[a1.magnitude.Length - 1];
1558-
// if ((bLsw & 3) == 3 && (a1Lsw & 3) == 3)
1559-
// totalS = -totalS;
1560-
//
1561-
//// a = b.Mod(a1);
1562-
// a = b.Remainder(a1);
1563-
// b = a1;
1564-
// }
1565-
// return totalS;
1566-
// }
1526+
public static int Jacobi(BigInteger a, BigInteger b)
1527+
{
1528+
Debug.Assert(a.sign >= 0);
1529+
Debug.Assert(b.sign > 0);
1530+
Debug.Assert(b.TestBit(0));
1531+
Debug.Assert(a.CompareTo(b) < 0);
1532+
1533+
int totalS = 1;
1534+
for (; ; )
1535+
{
1536+
if (a.sign == 0)
1537+
return 0;
1538+
1539+
if (a.Equals(One))
1540+
break;
1541+
1542+
int e = a.GetLowestSetBit();
1543+
1544+
int bLsw = b.magnitude[b.magnitude.Length - 1];
1545+
if ((e & 1) != 0 && ((bLsw & 7) == 3 || (bLsw & 7) == 5))
1546+
totalS = -totalS;
1547+
1548+
if (a.BitLength == e + 1)
1549+
break;
1550+
BigInteger a1 = a.ShiftRight(e);
1551+
1552+
int a1Lsw = a1.magnitude[a1.magnitude.Length - 1];
1553+
if ((bLsw & 3) == 3 && (a1Lsw & 3) == 3)
1554+
totalS = -totalS;
1555+
1556+
a = b.Remainder(a1);
1557+
b = a1;
1558+
}
1559+
return totalS;
1560+
}
15671561

15681562
public long LongValue
15691563
{
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using NBitcoin.BouncyCastle.Asn1.X9;
8+
using NBitcoin.BouncyCastle.Math.EC.Custom.Sec;
9+
using NBitcoin.BouncyCastle.Math.EC;
10+
using NBitcoin.BouncyCastle.Asn1;
11+
using NBitcoin.BouncyCastle.Math;
12+
using NBitcoin.DataEncoders;
13+
14+
namespace NBitcoin.Crypto
15+
{
16+
/// <summary>
17+
/// Schnorr Signatures using Bouncy Castle
18+
/// Implementation taken from NBitcoin
19+
/// </summary>
20+
public class SchnorrSignature
21+
{
22+
public BigInteger R { get; }
23+
public BigInteger S { get; }
24+
25+
public static SchnorrSignature Parse(string hex)
26+
{
27+
var bytes = Encoders.Hex.DecodeData(hex);
28+
return new SchnorrSignature(bytes);
29+
}
30+
31+
public SchnorrSignature(byte[] bytes)
32+
{
33+
if (bytes.Length != 64)
34+
throw new ArgumentException(paramName: nameof(bytes), message:"Invalid schnorr signature length.");
35+
36+
R = new BigInteger(1, bytes, 0, 32);
37+
S = new BigInteger(1, bytes, 32, 32);
38+
39+
}
40+
41+
public SchnorrSignature(BigInteger r, BigInteger s)
42+
{
43+
R = r;
44+
S = s;
45+
}
46+
public byte[] ToBytes()
47+
{
48+
return Utils.BigIntegerToBytes(R, 32).Concat(Utils.BigIntegerToBytes(S, 32));
49+
}
50+
}
51+
52+
public class SchnorrSigner
53+
{
54+
private static X9ECParameters Secp256k1 = NBitcoin.BouncyCastle.Crypto.EC.CustomNamedCurves.Secp256k1;
55+
private static BigInteger PP = ((SecP256K1Curve)Secp256k1.Curve).QQ;
56+
57+
public SchnorrSignature Sign(uint256 m, Key secret)
58+
{
59+
return Sign(m, new BigInteger(1, secret.ToBytes()));
60+
}
61+
62+
public SchnorrSignature Sign(uint256 m, BigInteger secret)
63+
{
64+
var k = new BigInteger(1, Hashes.SHA256(Utils.BigIntegerToBytes(secret, 32).Concat(m.ToBytes())));
65+
var R = Secp256k1.G.Multiply(k).Normalize();
66+
var Xr = R.XCoord.ToBigInteger();
67+
var Yr = R.YCoord.ToBigInteger();
68+
if (BigInteger.Jacobi(Yr, PP) != 1)
69+
k = Secp256k1.N.Subtract(k);
70+
71+
var P = Secp256k1.G.Multiply(secret);
72+
var keyPrefixedM = Utils.BigIntegerToBytes(Xr, 32).Concat(P.GetEncoded(true), m.ToBytes());
73+
var e = new BigInteger(1, Hashes.SHA256(keyPrefixedM));
74+
75+
var s = k.Add(e.Multiply(secret)).Mod(Secp256k1.N);
76+
return new SchnorrSignature(Xr, s);
77+
}
78+
79+
public bool Verify(uint256 m, PubKey pubkey, SchnorrSignature sig)
80+
{
81+
if (sig.R.CompareTo(PP) >= 0 || sig.S.CompareTo(Secp256k1.N) >= 0)
82+
return false;
83+
var e = new BigInteger(1, Hashes.SHA256(Utils.BigIntegerToBytes(sig.R, 32).Concat(pubkey.ToBytes(), m.ToBytes()))).Mod(Secp256k1.N);
84+
var q = pubkey.ECKey.GetPublicKeyParameters().Q.Normalize();
85+
var P = Secp256k1.Curve.CreatePoint(q.XCoord.ToBigInteger(), q.YCoord.ToBigInteger());
86+
87+
var R = Secp256k1.G.Multiply(sig.S).Add(P.Multiply(Secp256k1.N.Subtract(e))).Normalize();
88+
89+
if (R.IsInfinity
90+
|| R.XCoord.ToBigInteger().CompareTo(sig.R) != 0
91+
|| BigInteger.Jacobi(R.YCoord.ToBigInteger(), PP) != 1)
92+
return false;
93+
94+
return true;
95+
}
96+
97+
public static bool BatchVerify(uint256[] m, PubKey[] pubkeys, SchnorrSignature[] sigs, BigInteger[] rnds)
98+
{
99+
if (m.Length != pubkeys.Length || pubkeys.Length != sigs.Length || sigs.Length != rnds.Length + 1)
100+
throw new ArgumentException("Invalid array lengths");
101+
if (rnds.Any(r => r.CompareTo(BigInteger.Zero) <= 0 || r.CompareTo(Secp256k1.N) >= 0))
102+
throw new ArgumentException("Random numbers are out of range");
103+
var s = BigInteger.Zero;
104+
var r1 = Secp256k1.Curve.Infinity;
105+
var r2 = Secp256k1.Curve.Infinity;
106+
for (var i = 0; i < sigs.Count(); i++)
107+
{
108+
var sig = sigs[i];
109+
if (sig.R.CompareTo(PP) >= 0 || sig.S.CompareTo(Secp256k1.N) >= 0)
110+
return false;
111+
112+
var e = new BigInteger(1, Hashes.SHA256(Utils.BigIntegerToBytes(sig.R, 32).Concat(pubkeys[i].ToBytes(), m[i].ToBytes()))).Mod(Secp256k1.N);
113+
var c = sig.R.Pow(3).Add(BigInteger.ValueOf(7)).Mod(PP);
114+
var y = c.ModPow(PP.Add(BigInteger.One).Divide(BigInteger.ValueOf(4)), PP);
115+
if (!y.ModPow(BigInteger.Two, PP).Equals(c))
116+
return false;
117+
118+
var a = i == 0 ? BigInteger.One : rnds[i - 1];
119+
s = s.Add(sig.S.Multiply(a)).Mod(Secp256k1.N);
120+
121+
var R = Secp256k1.Curve.CreatePoint(sig.R, y);
122+
r1 = r1.Add(R.Multiply(a));
123+
124+
var P = pubkeys[i].ECKey.GetPublicKeyParameters().Q.Normalize();
125+
r2 = r2.Add(P.Multiply(e.Multiply(a)));
126+
}
127+
return Secp256k1.G.Multiply(s).Equals(r1.Add(r2));
128+
}
129+
}
130+
}

src/Blockcore/NBitcoin/Key.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ public ECDSASignature Sign(uint256 hash)
9898
return this._ECKey.Sign(hash);
9999
}
100100

101+
public SchnorrSignature SignSchnorr(uint256 hash)
102+
{
103+
var signer = new SchnorrSigner();
104+
return signer.Sign(hash, this);
105+
106+
}
107+
101108
/// <summary>
102109
/// Hashes and signs a message, returning the signature.
103110
/// </summary>

src/Blockcore/NBitcoin/PubKey.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public PubKey(byte[] bytes, bool @unsafe)
6161

6262
private ECKey _ECKey;
6363

64-
private ECKey ECKey
64+
internal ECKey ECKey
6565
{
6666
get
6767
{
@@ -167,6 +167,17 @@ public BitcoinScriptAddress GetScriptAddress(Network network)
167167
return new BitcoinScriptAddress(redeem.Hash, network);
168168
}
169169

170+
public bool Verify(uint256 hash, SchnorrSignature sig)
171+
{
172+
if (sig == null)
173+
throw new ArgumentNullException(nameof(sig));
174+
if (hash == null)
175+
throw new ArgumentNullException(nameof(hash));
176+
177+
SchnorrSigner signer = new SchnorrSigner();
178+
return signer.Verify(hash, this, sig);
179+
}
180+
170181
public bool Verify(uint256 hash, ECDSASignature sig)
171182
{
172183
return this.ECKey.Verify(hash, sig);

src/Blockcore/NBitcoin/Utils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ private static void Write(MemoryStream ms, byte[] bytes)
366366
ms.Write(bytes, 0, bytes.Length);
367367
}
368368

369-
internal static Array BigIntegerToBytes(BigInteger b, int numBytes)
369+
internal static byte[] BigIntegerToBytes(BigInteger b, int numBytes)
370370
{
371371
if (b == null)
372372
{
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using NBitcoin.BouncyCastle.Math;
6+
using NBitcoin.Crypto;
7+
using NBitcoin.DataEncoders;
8+
#if HAS_SPAN
9+
using NBitcoin.Secp256k1;
10+
#endif
11+
using Xunit;
12+
13+
namespace NBitcoin.Tests
14+
{
15+
public class SchnorrSignerTests
16+
{
17+
[Fact]
18+
public void SingningTest()
19+
{
20+
var vectors = new string[][]{
21+
new []{"Test vector 1",
22+
"0000000000000000000000000000000000000000000000000000000000000001",
23+
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
24+
"0000000000000000000000000000000000000000000000000000000000000000",
25+
"787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF67031A98831859DC34DFFEEDDA86831842CCD0079E1F92AF177F7F22CC1DCED05"},
26+
new []{"Test vector 2",
27+
"B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF",
28+
"02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
29+
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
30+
"2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD"},
31+
new []{"Test vector 3",
32+
"C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C7",
33+
"03FAC2114C2FBB091527EB7C64ECB11F8021CB45E8E7809D3C0938E4B8C0E5F84B",
34+
"5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
35+
"00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BE00880371D01766935B92D2AB4CD5C8A2A5837EC57FED7660773A05F0DE142380"}
36+
};
37+
38+
foreach (var vector in vectors)
39+
{
40+
var privatekey = new Key(Encoders.Hex.DecodeData(vector[1]));
41+
var publicKey = new PubKey(Encoders.Hex.DecodeData(vector[2]));
42+
var message = Parseuint256(vector[3]);
43+
var expectedSignature = SchnorrSignature.Parse(vector[4]);
44+
45+
var signature = privatekey.SignSchnorr(message);
46+
Assert.Equal(expectedSignature.ToBytes(), signature.ToBytes());
47+
48+
Assert.True(publicKey.Verify(message, expectedSignature));
49+
Assert.True(privatekey.PubKey.Verify(message, expectedSignature));
50+
}
51+
}
52+
53+
[Fact]
54+
public void ShouldPassVerifycation()
55+
{
56+
var publicKey = new PubKey(Encoders.Hex.DecodeData("03DEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34"));
57+
var message = Parseuint256("4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703");
58+
var signature = SchnorrSignature.Parse("00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6302A8DC32E64E86A333F20EF56EAC9BA30B7246D6D25E22ADB8C6BE1AEB08D49D");
59+
Assert.True(publicKey.Verify(message, signature));
60+
}
61+
62+
private uint256 Parseuint256(string hex)
63+
{
64+
var message = uint256.Parse(hex);
65+
return new uint256(message.ToBytes(false));
66+
}
67+
68+
[Fact]
69+
public void ShouldFailVerifycation()
70+
{
71+
var vectors = new string[][]{
72+
new []{"Test vector 5",
73+
"02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
74+
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
75+
"2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1DFA16AEE06609280A19B67A24E1977E4697712B5FD2943914ECD5F730901B4AB7",
76+
"incorrect R residuosity"},
77+
new []{"Test vector 6",
78+
"03FAC2114C2FBB091527EB7C64ECB11F8021CB45E8E7809D3C0938E4B8C0E5F84B",
79+
"5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
80+
"00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BED092F9D860F1776A1F7412AD8A1EB50DACCC222BC8C0E26B2056DF2F273EFDEC",
81+
"negated message hash"},
82+
new []{"Test vector 7",
83+
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
84+
"0000000000000000000000000000000000000000000000000000000000000000",
85+
"787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF68FCE5677CE7A623CB20011225797CE7A8DE1DC6CCD4F754A47DA6C600E59543C",
86+
"negated s value"},
87+
new []{"Test vector 8",
88+
"03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
89+
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
90+
"2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD",
91+
"negated public key"}
92+
};
93+
94+
foreach (var vector in vectors)
95+
{
96+
var publicKey = new PubKey(Encoders.Hex.DecodeData(vector[1]));
97+
var message = uint256.Parse(vector[2]);
98+
var expectedSignature = SchnorrSignature.Parse(vector[3]);
99+
var reason = vector[4];
100+
101+
Assert.False(publicKey.Verify(message, expectedSignature), reason);
102+
}
103+
}
104+
105+
[Fact]
106+
public void ShouldPassBatchVerifycation()
107+
{
108+
var vectors = new string[][]{
109+
new []{
110+
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
111+
"0000000000000000000000000000000000000000000000000000000000000000",
112+
"787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF67031A98831859DC34DFFEEDDA86831842CCD0079E1F92AF177F7F22CC1DCED05"},
113+
new []{
114+
"02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
115+
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
116+
"2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD"},
117+
new []{
118+
"03FAC2114C2FBB091527EB7C64ECB11F8021CB45E8E7809D3C0938E4B8C0E5F84B",
119+
"5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
120+
"00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BE00880371D01766935B92D2AB4CD5C8A2A5837EC57FED7660773A05F0DE142380"}
121+
};
122+
123+
var messages = vectors.Select(v => Parseuint256(v[1])).ToArray();
124+
var pubkeys = vectors.Select(v => new PubKey(Encoders.Hex.DecodeData(v[0]))).ToArray();
125+
var signatures = vectors.Select(v => SchnorrSignature.Parse(v[2])).ToArray();
126+
127+
var randoms = Enumerable.Range(0, 2).Select(x => BigInteger.Arbitrary(256)).ToArray();
128+
var ok = SchnorrSigner.BatchVerify(messages, pubkeys, signatures, randoms);
129+
Assert.True(ok);
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)