Skip to content

Commit de66ee7

Browse files
author
Thomas Kerin
committed
Add phpecc backed Schnorr implementation
1 parent b34b72c commit de66ee7

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature;
6+
7+
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Adapter\EcAdapter;
8+
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PrivateKey;
9+
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PublicKey;
10+
use BitWasp\Buffertools\BufferInterface;
11+
12+
class SchnorrSigner
13+
{
14+
/**
15+
* @var EcAdapter
16+
*/
17+
private $adapter;
18+
19+
public function __construct(EcAdapter $ecAdapter)
20+
{
21+
$this->adapter = $ecAdapter;
22+
}
23+
24+
/**
25+
* @param PrivateKey $privateKey
26+
* @param BufferInterface $message32
27+
* @return Signature
28+
*/
29+
public function sign(PrivateKey $privateKey, BufferInterface $message32): Signature
30+
{
31+
$G = $this->adapter->getGenerator();
32+
$n = $G->getOrder();
33+
$k = $this->hashPrivateData($privateKey, $message32, $n);
34+
$R = $G->mul($k);
35+
36+
if (gmp_cmp(gmp_jacobi($R->getY(), $G->getCurve()->getPrime()), 1) !== 0) {
37+
$k = gmp_sub($G->getOrder(), $k);
38+
}
39+
40+
$e = $this->hashPublicData($R->getX(), $privateKey->getPublicKey(), $message32, $n);
41+
$s = gmp_mod(gmp_add($k, gmp_mod(gmp_mul($e, $privateKey->getSecret()), $n)), $n);
42+
return new Signature($this->adapter, $R->getX(), $s);
43+
}
44+
45+
/**
46+
* @param \GMP $n
47+
* @return string
48+
*/
49+
private function tob32(\GMP $n): string
50+
{
51+
return $this->adapter->getMath()->intToFixedSizeString($n, 32);
52+
}
53+
54+
/**
55+
* @param PrivateKey $privateKey
56+
* @param BufferInterface $message32
57+
* @param \GMP $n
58+
* @return \GMP
59+
*/
60+
private function hashPrivateData(PrivateKey $privateKey, BufferInterface $message32, \GMP $n): \GMP
61+
{
62+
$hasher = hash_init('sha256');
63+
hash_update($hasher, $this->tob32($privateKey->getSecret()));
64+
hash_update($hasher, $message32->getBinary());
65+
return gmp_mod(gmp_init(hash_final($hasher, false), 16), $n);
66+
}
67+
68+
/**
69+
* @param \GMP $Rx
70+
* @param PublicKey $publicKey
71+
* @param BufferInterface $message32
72+
* @param \GMP $n
73+
* @param string|null $rxBytes
74+
* @return \GMP
75+
*/
76+
private function hashPublicData(\GMP $Rx, PublicKey $publicKey, BufferInterface $message32, \GMP $n, string &$rxBytes = null): \GMP
77+
{
78+
$hasher = hash_init('sha256');
79+
$rxBytes = $this->tob32($Rx);
80+
hash_update($hasher, $rxBytes);
81+
hash_update($hasher, $publicKey->getBinary());
82+
hash_update($hasher, $message32->getBinary());
83+
return gmp_mod(gmp_init(hash_final($hasher, false), 16), $n);
84+
}
85+
86+
public function verify(BufferInterface $message32, PublicKey $publicKey, Signature $signature): bool
87+
{
88+
$G = $this->adapter->getGenerator();
89+
$n = $G->getOrder();
90+
$p = $G->getCurve()->getPrime();
91+
92+
if (gmp_cmp($signature->getR(), $p) >= 0 || gmp_cmp($signature->getR(), $n) >= 0) {
93+
return false;
94+
}
95+
96+
$RxBytes = null;
97+
$e = $this->hashPublicData($signature->getR(), $publicKey, $message32, $n, $RxBytes);
98+
$R = $G->mul($signature->getS())->add($publicKey->tweakMul(gmp_sub($G->getOrder(), $e))->getPoint());
99+
100+
$jacobiNotOne = gmp_cmp(gmp_jacobi($R->getY(), $p), 1) !== 0;
101+
$rxNotEquals = !hash_equals($RxBytes, $this->tob32($R->getX()));
102+
if ($jacobiNotOne || $rxNotEquals) {
103+
return false;
104+
}
105+
return true;
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace BitWasp\Bitcoin\Tests\Crypto\EcAdapter;
6+
7+
use BitWasp\Bitcoin\Crypto\EcAdapter\EcAdapterFactory;
8+
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\SchnorrSigner;
9+
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\Signature;
10+
use BitWasp\Bitcoin\Key\Factory\PrivateKeyFactory;
11+
use BitWasp\Bitcoin\Key\Factory\PublicKeyFactory;
12+
use BitWasp\Bitcoin\Math\Math;
13+
use BitWasp\Bitcoin\Tests\AbstractTestCase;
14+
use BitWasp\Buffertools\Buffer;
15+
use Mdanter\Ecc\EccFactory;
16+
17+
class PhpeccSchnorrSignerTest extends AbstractTestCase
18+
{
19+
public function getCompliantSignatureFixtures(): array
20+
{
21+
return [
22+
[
23+
/*$privKey = */ "0000000000000000000000000000000000000000000000000000000000000001",
24+
/*$pubKey = */ "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
25+
/*$msg32 = */ "0000000000000000000000000000000000000000000000000000000000000000",
26+
/*$sig64 = */ "787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF67031A98831859DC34DFFEEDDA86831842CCD0079E1F92AF177F7F22CC1DCED05",
27+
],
28+
[
29+
/*$privKey = */ "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF",
30+
/*$pubKey = */ "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
31+
/*$msg32 = */ "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
32+
/*$sig64 = */ "2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD",
33+
],
34+
[
35+
/*$privKey = */ "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C7",
36+
/*$pubKey = */ "03FAC2114C2FBB091527EB7C64ECB11F8021CB45E8E7809D3C0938E4B8C0E5F84B",
37+
/*$msg32 = */ "5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
38+
/*$sig64 = */ "00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BE00880371D01766935B92D2AB4CD5C8A2A5837EC57FED7660773A05F0DE142380",
39+
],
40+
];
41+
}
42+
43+
/**
44+
* @dataProvider getCompliantSignatureFixtures
45+
* @param string $privKey
46+
* @param string $pubKey
47+
* @param string $msg32
48+
* @param string $sig64
49+
* @throws \Exception
50+
*/
51+
public function testSignatureFixtures(string $privKey, string $pubKey, string $msg32, string $sig64)
52+
{
53+
$ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1());
54+
$privFactory = new PrivateKeyFactory($ecAdapter);
55+
$priv = $privFactory->fromHexCompressed($privKey);
56+
$pub = $priv->getPublicKey();
57+
$msg = Buffer::hex($msg32);
58+
$schnorrSigner = new SchnorrSigner($ecAdapter);
59+
$signature = $schnorrSigner->sign($priv, $msg);
60+
61+
$math = $ecAdapter->getMath();
62+
$r = $math->intToFixedSizeString($signature->getR(), 32);
63+
$s = $math->intToFixedSizeString($signature->getS(), 32);
64+
$this->assertEquals(strtolower($sig64), bin2hex($r.$s));
65+
$this->assertTrue($schnorrSigner->verify($msg, $pub, $signature));
66+
}
67+
68+
public function getVerificationFixtures(): array
69+
{
70+
return [
71+
[
72+
/*$pubKey = */ "03DEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34",
73+
/*$msg32 = */ "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703",
74+
/*$sig64 = */ "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6302A8DC32E64E86A333F20EF56EAC9BA30B7246D6D25E22ADB8C6BE1AEB08D49D",
75+
],
76+
];
77+
}
78+
79+
/**
80+
* @dataProvider getVerificationFixtures
81+
* @param string $pubKey
82+
* @param string $msg32
83+
* @param string $sig64
84+
* @throws \Exception
85+
*/
86+
public function testPositiveVerification(string $pubKey, string $msg32, string $sig64)
87+
{
88+
$ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1());
89+
$pubKeyFactory = new PublicKeyFactory($ecAdapter);
90+
$pub = $pubKeyFactory->fromHex($pubKey);
91+
$msg = Buffer::hex($msg32);
92+
$schnorrSigner = new SchnorrSigner($ecAdapter);
93+
$sigBuf = Buffer::hex($sig64);
94+
$r = $sigBuf->slice(0, 32)->getGmp();
95+
$s= $sigBuf->slice(32, 64)->getGmp();
96+
$signature = new Signature($ecAdapter, $r, $s);
97+
$this->assertTrue($schnorrSigner->verify($msg, $pub, $signature));
98+
}
99+
100+
public function getNegativeVerificationFixtures(): array
101+
{
102+
return [
103+
[
104+
/*$pubKey = */ "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
105+
/*$msg32 = */ "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
106+
/*$sig64 = */ "2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1DFA16AEE06609280A19B67A24E1977E4697712B5FD2943914ECD5F730901B4AB7",
107+
/*$reason = */ "incorrect R residuosity",
108+
],
109+
[
110+
/*$pubKey = */ "03FAC2114C2FBB091527EB7C64ECB11F8021CB45E8E7809D3C0938E4B8C0E5F84B",
111+
/*$msg32 = */ "5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
112+
/*$sig64 = */ "00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BED092F9D860F1776A1F7412AD8A1EB50DACCC222BC8C0E26B2056DF2F273EFDEC",
113+
/*$reason = */ "negated message hash",
114+
],
115+
[
116+
/*$pubKey = */ "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
117+
/*$msg32 = */ "0000000000000000000000000000000000000000000000000000000000000000",
118+
/*$sig64 = */ "787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF68FCE5677CE7A623CB20011225797CE7A8DE1DC6CCD4F754A47DA6C600E59543C",
119+
/*$reason = */ "negated s value",
120+
],
121+
[
122+
/*$pubKey = */ "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
123+
/*$msg32 = */ "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
124+
/*$sig64 = */ "2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD",
125+
/*$reason = */ "negated public key",
126+
],
127+
];
128+
}
129+
130+
/**
131+
* @dataProvider getNegativeVerificationFixtures
132+
* @param string $pubKey
133+
* @param string $msg32
134+
* @param string $sig64
135+
* @throws \Exception
136+
*/
137+
public function testNegativeVerification(string $pubKey, string $msg32, string $sig64)
138+
{
139+
$ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1());
140+
$pubKeyFactory = new PublicKeyFactory($ecAdapter);
141+
$pub = $pubKeyFactory->fromHex($pubKey);
142+
$msg = Buffer::hex($msg32);
143+
$schnorrSigner = new SchnorrSigner($ecAdapter);
144+
$sigBuf = Buffer::hex($sig64);
145+
$r = $sigBuf->slice(0, 32)->getGmp();
146+
$s= $sigBuf->slice(32, 64)->getGmp();
147+
$signature = new Signature($ecAdapter, $r, $s);
148+
$this->assertFalse($schnorrSigner->verify($msg, $pub, $signature));
149+
}
150+
}

0 commit comments

Comments
 (0)