Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/Identities/PublicKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ArkEcosystem\Crypto\Binary\Bytes;
use ArkEcosystem\Crypto\Utils\Secp256k1;
use BN\BN;
use Elliptic\EC\KeyPair;
use InvalidArgumentException;
use Throwable;
Expand Down Expand Up @@ -168,4 +169,67 @@ public function verify($hash, $signature): bool
return false;
}
}

/**
* Verify a 64-byte Schnorr signature (r || s) over the given 32-byte hash.
*
* This is the pre-BIP340 Schnorr scheme from bcrypto's `secp256k1.schnorrSign`,
* which ARK Core uses for transactions and ARK Vault uses for signed messages:
* e = sha256(r || compressed public key || hash), and R = sG - eP must have an
* x coordinate equal to r and a y coordinate that is a quadratic residue.
*
* @param Bytes|string $hash
* @param Bytes|string $signature
*
* @return bool
*/
public function verifySchnorr($hash, $signature): bool
{
$hash = $hash instanceof Bytes ? $hash->getBinary() : $hash;
$signature = $signature instanceof Bytes ? $signature->getHex() : $signature;

if (strlen($signature) !== 128 || ! ctype_xdigit($signature)) {
return false;
}

$curve = Secp256k1::curve();
$r = new BN(substr($signature, 0, 64), 16);
$s = new BN(substr($signature, 64), 16);

if ($r->cmp($curve->curve->p) >= 0 || $s->cmp($curve->n) >= 0) {
return false;
}

$point = $this->keyPair->getPublic();
$e = (new BN(hash('sha256', hex2bin(substr($signature, 0, 64)).$this->getBinary().$hash), 16))->umod($curve->n);
$R = $curve->g->mul($s)->add($point->mul($curve->n->sub($e)));

if ($R->isInfinity()) {
return false;
}

if (gmp_jacobi(gmp_init($R->getY()->toString(16), 16), gmp_init($curve->curve->p->toString(16), 16)) !== 1) {
return false;
}

return $R->getX()->cmp($r) === 0;
}

/**
* Verify a signature over the given 32-byte hash, as Schnorr when it is 64
* bytes long and as DER encoded ECDSA otherwise.
*
* @param Bytes|string $hash
* @param Bytes|string $signature
*
* @return bool
*/
public function verifySchnorrOrECDSA($hash, $signature): bool
{
$signature = $signature instanceof Bytes ? $signature->getHex() : $signature;

return strlen($signature) === 128
? $this->verifySchnorr($hash, $signature)
: $this->verify($hash, $signature);
}
}
3 changes: 1 addition & 2 deletions src/Transactions/Types/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ public function verifyECDSA(Bytes $bytes, string $publicKey, string $signature):

public function verifySchnorr(Bytes $bytes, string $publicKey, string $signature): bool
{
//TODO
return false;
return PublicKey::fromHex($publicKey)->verifySchnorr(Hash::sha256($bytes), $signature);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static function sign(string $message, string $passphrase): self
*/
public function verify(): bool
{
return PublicKey::fromHex($this->publicKey)->verify(Hash::sha256($this->message), $this->signature);
return PublicKey::fromHex($this->publicKey)->verifySchnorrOrECDSA(Hash::sha256($this->message), $this->signature);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions tests/Unit/CoreCompatibilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@
$this->assertFalse(PublicKey::fromHex($vector['publicKey'])->verify(hash('sha256', $vector['message']['message'].' ', true), $vector['message']['signature']));
})->with('core vectors');

it('should verify schnorr signed messages like the ones ark vault produces', function (array $vector) {
$message = Message::new([
'signatory' => $vector['publicKey'],
'signature' => $vector['message']['schnorrSignature'],
'message' => $vector['message']['message'],
]);

$this->assertTrue($message->verify());
})->with('core vectors');

dataset('core vectors', function () {
$fixture = json_decode(file_get_contents(__DIR__.'/../fixtures/core-vectors.json'), true);

Expand Down
50 changes: 49 additions & 1 deletion tests/Unit/Identities/PublicKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
namespace ArkEcosystem\Tests\Crypto\Unit\Identities;

use ArkEcosystem\Crypto\Binary\Bytes;
use ArkEcosystem\Crypto\Identities\PrivateKey;
use ArkEcosystem\Crypto\Identities\PublicKey as TestClass;
use ArkEcosystem\Crypto\Utils\Secp256k1;
use BN\BN;
use InvalidArgumentException;

it('should get the public key from passphrase', function () {
Expand Down Expand Up @@ -62,7 +65,7 @@
$fixture = $this->getFixture('identity');

$uncompressed = TestClass::fromHex($fixture['data']['publicKey']);
$point = \ArkEcosystem\Crypto\Utils\Secp256k1::curve()->keyFromPublic($fixture['data']['publicKey'], 'hex')->getPublic(false, 'hex');
$point = Secp256k1::curve()->keyFromPublic($fixture['data']['publicKey'], 'hex')->getPublic(false, 'hex');

$this->assertSame($uncompressed->getHex(), TestClass::fromHex($point)->getHex());
});
Expand Down Expand Up @@ -93,3 +96,48 @@
$this->assertFalse($publicKey->verify($hash, 'deadbeef'));
$this->assertFalse($publicKey->verify(hash('sha256', 'Other', true), $this->getFixture('message-v2')['data']['signature']));
});

it('should verify a schnorr signature made by ark core and ark vault', function () {
$publicKey = TestClass::fromHex('034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192');
$signature = '2521d2ebe067546920bec3becc581d4d8186d4d2431980b34834ad3d3945be6ed32d10b6499307ba499b72448c971516a9e2be96c79cbf2ad5948b06c1ab242a';

$this->assertTrue($publicKey->verifySchnorr(hash('sha256', 'Hello World', true), $signature));
$this->assertTrue($publicKey->verifySchnorr(new Bytes(hash('sha256', 'Hello World', true)), Bytes::fromHex($signature)));
$this->assertTrue($publicKey->verifySchnorrOrECDSA(hash('sha256', 'Hello World', true), $signature));
$this->assertFalse($publicKey->verifySchnorr(hash('sha256', 'Hello World!', true), $signature));
});

it('should still verify ecdsa signatures when schnorr or ecdsa is accepted', function () {
$publicKey = TestClass::fromHex('034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192');

$this->assertTrue($publicKey->verifySchnorrOrECDSA(hash('sha256', 'Hello World', true), $this->getFixture('message-v2')['data']['signature']));
});

it('should reject malformed schnorr signatures', function (string $signature) {
$publicKey = TestClass::fromHex('034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192');

$this->assertFalse($publicKey->verifySchnorr(hash('sha256', 'Hello World', true), $signature));
})->with([
'too short' => [str_repeat('ab', 63)],
'not hex' => [str_repeat('zz', 64)],
'r outside field' => [str_repeat('f', 64).str_repeat('1', 64)],
's outside order' => [str_repeat('1', 64).str_repeat('f', 64)],
'tampered s' => ['2521d2ebe067546920bec3becc581d4d8186d4d2431980b34834ad3d3945be6ed32d10b6499307ba499b72448c971516a9e2be96c79cbf2ad5948b06c1ab242c'],
]);

it('should reject a schnorr signature whose nonce point has the wrong y', function () {
$privateKey = PrivateKey::fromPassphrase('this is a top secret passphrase');
$publicKey = $privateKey->getPublicKey();
$hash = hash('sha256', 'Hello World', true);
$signature = '2521d2ebe067546920bec3becc581d4d8186d4d2431980b34834ad3d3945be6ed32d10b6499307ba499b72448c971516a9e2be96c79cbf2ad5948b06c1ab242a';

// s' = 2ed - s makes s'G - eP the negation of R: same x as r, but a y that is not a quadratic residue.
$n = Secp256k1::curve()->n;
$r = substr($signature, 0, 64);
$e = (new BN(hash('sha256', hex2bin($r).$publicKey->getBinary().$hash), 16))->umod($n);
$d = new BN($privateKey->getHex(), 16);
$s = new BN(substr($signature, 64), 16);
$s2 = $e->mul($d)->muln(2)->sub($s)->umod($n);

$this->assertFalse($publicKey->verifySchnorr($hash, $r.str_pad($s2->toString(16), 64, '0', STR_PAD_LEFT)));
});
27 changes: 27 additions & 0 deletions tests/Unit/Transactions/TransactionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,30 @@ function getTransactionForTransaction($file = 'transfer-sign'): Transfer

return Deserializer::new($fixture['serialized'])->deserialize();
}

it('should verify schnorr signed transactions from ark core', function (string $fixture) {
$data = json_decode(file_get_contents($fixture), true);
$transaction = Deserializer::new($data['serialized'])->deserialize();

$this->assertTrue($transaction->verify());

if (isset($data['data']['secondSignature'])) {
$this->assertTrue($transaction->secondVerify(PublicKey::fromPassphrase('this is a top secret second passphrase')->getHex()));
}
})->with(function () {
$fixtures = array_filter(
glob(__DIR__.'/../../fixtures/transactions-schnorr/*/*.json'),
fn (string $fixture) => ! str_contains($fixture, 'multiSign'),
);

return array_combine(array_map(fn (string $fixture) => basename($fixture, '.json'), $fixtures), $fixtures);
});

it('should not verify a schnorr signed transaction that was tampered with', function () {
$data = json_decode(file_get_contents(__DIR__.'/../../fixtures/transactions-schnorr/transfer/transfer-sign.json'), true);
$transaction = Deserializer::new($data['serialized'])->deserialize();

$transaction->data['amount'] = '1';

$this->assertFalse($transaction->verify());
});
36 changes: 24 additions & 12 deletions tests/fixtures/core-vectors.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
},
"message": {
"message": "",
"signature": "304402205b285ad467fd032b0ef9e6406f192eb2edc367a7fbe9ff9e33431b9a6f4ee76102203f3bd03796f2578cedb5139783d4e47675f6f90f740572a415d940550f36c9e2"
"signature": "304402205b285ad467fd032b0ef9e6406f192eb2edc367a7fbe9ff9e33431b9a6f4ee76102203f3bd03796f2578cedb5139783d4e47675f6f90f740572a415d940550f36c9e2",
"schnorrSignature": "ab784987d61aef53ee6e1af8cce96bb049a842f8a197c354cc7f8fc0e1c0bdac41c4d82a9f0c8c1830201b145db0e414b838b93e40ec74b15950c0b4ab683ac9"
}
},
{
Expand All @@ -32,7 +33,8 @@
},
"message": {
"message": "Hello World",
"signature": "30450221009a23b27787b23b9308b2ef8e3d5e768c7ff8e8b5de80b20d5289d4f7ad881286022011faf6f400f800fee2e582c30248b469be534dc2dd76feb13215d6d245160095"
"signature": "30450221009a23b27787b23b9308b2ef8e3d5e768c7ff8e8b5de80b20d5289d4f7ad881286022011faf6f400f800fee2e582c30248b469be534dc2dd76feb13215d6d245160095",
"schnorrSignature": "8211f34d3d11aeaaeb72bd4c5da2d3d0f1b43e42b0653a04848346fc4f59c11b412217f0a7521c0bf27fe481d415e7d4e99782ab34028983f6d41b8c6932dc84"
}
},
{
Expand All @@ -49,7 +51,8 @@
},
"message": {
"message": "ark-claim:boldninja:ARMy9u1XvrZ124JzQq3oeJpjmBEnYkyU7D:1792155604",
"signature": "3044022034a3e423587f4c92a3bc4e696f8604c4d89d1feab42907bbd0f16f224b72449202201dcda8786d06a834098c0edff7e608509b81889df3b5064b8629e59089f2f5e9"
"signature": "3044022034a3e423587f4c92a3bc4e696f8604c4d89d1feab42907bbd0f16f224b72449202201dcda8786d06a834098c0edff7e608509b81889df3b5064b8629e59089f2f5e9",
"schnorrSignature": "b4fb0bd6c3db3fe53c996bb55531feef96f65b8fd38715268050d0d9e21cf750b41c200834625a69cb45da996eeea028cab4c71966e58fc86cc1d1ecc91603e7"
}
},
{
Expand All @@ -66,7 +69,8 @@
},
"message": {
"message": "ünïcödé ✓ 🚀",
"signature": "3045022100d95f5a84b9d754293e7de8df07259801bc6872737305be0cff779f4bad318a490220687334e3faf48df2562e860ef3c8dd8034e57544852f9f1bd0d77ab1865f7e5a"
"signature": "3045022100d95f5a84b9d754293e7de8df07259801bc6872737305be0cff779f4bad318a490220687334e3faf48df2562e860ef3c8dd8034e57544852f9f1bd0d77ab1865f7e5a",
"schnorrSignature": "83e99761e1f0f3e4a8f166473f7119f41fb2c163686532dafd3ab984aef3ab5037940fc6c667c5f15e6a14b6c646f7c38c2ba4be231cabeb027de8f3b49d6f75"
}
},
{
Expand All @@ -83,7 +87,8 @@
},
"message": {
"message": "line\nbreak",
"signature": "3044022035e806622839fcc1bd0037a64c125b4f2433ec44de30ae684e7cdfef9509f0ae022013d21f74b649c900946399ed3fd97dd69be53d3351a8ac7a87eb038d75de0167"
"signature": "3044022035e806622839fcc1bd0037a64c125b4f2433ec44de30ae684e7cdfef9509f0ae022013d21f74b649c900946399ed3fd97dd69be53d3351a8ac7a87eb038d75de0167",
"schnorrSignature": "41414f472416c2517b768e0d7f2054de047414ff571ac52d676d5bea33bcf5abc9a6be9350aea585ee3fa891b72563d0701ee8bdf46b365451f9b9fba2d6d15b"
}
},
{
Expand All @@ -100,7 +105,8 @@
},
"message": {
"message": "",
"signature": "30450221008b55d9fb41a5fd0a076ef7c4fbefb36a961cafd8e4be00bcbcfd874e6827642402207d8438db15dbc7da0bda1e82e58434053e946d51a88906e9a9f89484831b93f4"
"signature": "30450221008b55d9fb41a5fd0a076ef7c4fbefb36a961cafd8e4be00bcbcfd874e6827642402207d8438db15dbc7da0bda1e82e58434053e946d51a88906e9a9f89484831b93f4",
"schnorrSignature": "785f9de9e03cd1156e5c45cea58d8a4411094bd5283395080797447c08495ace1a9cbff303d32d2ac6124f1d626e6d7885dc956a5971b2970fb6154a6ca5f5fb"
}
},
{
Expand All @@ -117,7 +123,8 @@
},
"message": {
"message": "Hello World",
"signature": "304402201e6983b40b24f6f9dfc830adae58e2aa914cf5feaf84912196af1432f8ecf4db022064e8dc87204e97bc4b3f6e3f4a57dea143c48d90fbdc89dadd0f25b5d22eda92"
"signature": "304402201e6983b40b24f6f9dfc830adae58e2aa914cf5feaf84912196af1432f8ecf4db022064e8dc87204e97bc4b3f6e3f4a57dea143c48d90fbdc89dadd0f25b5d22eda92",
"schnorrSignature": "6fef0c292bfb2158a07aaf5663bf12f800ef21765bdfae499edca33dbd96cf873f7e28a0f3e0b2a5810cc997f3f023c701aee74c5caa7c1d700c23cc5dda5c9b"
}
},
{
Expand All @@ -134,7 +141,8 @@
},
"message": {
"message": "ark-claim:boldninja:ARMy9u1XvrZ124JzQq3oeJpjmBEnYkyU7D:1792155604",
"signature": "30450221008d67df32ff7dcbf03c516c1ab3013f69b90a87425db3bc20cc562d85c0b745c602207d39e12f24a62d682778d283092eebbea7b42cbfae9fda9e871c16fa4df48ea3"
"signature": "30450221008d67df32ff7dcbf03c516c1ab3013f69b90a87425db3bc20cc562d85c0b745c602207d39e12f24a62d682778d283092eebbea7b42cbfae9fda9e871c16fa4df48ea3",
"schnorrSignature": "aeccd93b2aeb1772006dbc0f224f2351d9417d50c9f7100a7b239c1a83673ebc720f25cfb4dff6d016e16dd7cb791ef00710454fe4c88eb32339d4023c9636ac"
}
},
{
Expand All @@ -151,7 +159,8 @@
},
"message": {
"message": "ünïcödé ✓ 🚀",
"signature": "3045022100c2919b4b4f1a1780bd1afdacad63a610154952ec946049aae5d6d0923d64b32002201517fd512c0e2bc1455ae9e628128fd0e57fa5714264ba4a36aa000c75b94402"
"signature": "3045022100c2919b4b4f1a1780bd1afdacad63a610154952ec946049aae5d6d0923d64b32002201517fd512c0e2bc1455ae9e628128fd0e57fa5714264ba4a36aa000c75b94402",
"schnorrSignature": "ba6e854206d6ecc3a84aac6661b73bbe3a66963ee750e341ac65c41db02f65e7254533e9b35db82412f2eaec19a143c7689276fd007f25d979613841d162f6dd"
}
},
{
Expand All @@ -168,7 +177,8 @@
},
"message": {
"message": "line\nbreak",
"signature": "304402201fc87138473cdd6ca2e738cbd0acd0d7b6bfd21582162672c631c6c062e4e3ee02201968842936cc142f40d9fabf5757e5a9580ad4ea620e38c63e2f04fcde4ad6fa"
"signature": "304402201fc87138473cdd6ca2e738cbd0acd0d7b6bfd21582162672c631c6c062e4e3ee02201968842936cc142f40d9fabf5757e5a9580ad4ea620e38c63e2f04fcde4ad6fa",
"schnorrSignature": "9702762577b3146e30a82e55c201c3d47d6d14bbd702231e35dbf74959cc6b4bf9cafa2fbdbac68a7423907b8d5a7335cbc754d2848cd80b41ebdaf7c1d62961"
}
},
{
Expand All @@ -185,7 +195,8 @@
},
"message": {
"message": "",
"signature": "3045022100a92f4b286875f419fccbecb2a298c344b1c3ad1ae9606c78e2ef8156e8547d8302206f2de0e3515a71b10ba291978ff7bb3a995ba45332362ed6246868e93c0f6649"
"signature": "3045022100a92f4b286875f419fccbecb2a298c344b1c3ad1ae9606c78e2ef8156e8547d8302206f2de0e3515a71b10ba291978ff7bb3a995ba45332362ed6246868e93c0f6649",
"schnorrSignature": "1d0766d9e5a9c2cb370243799e503175d37262ecd1a15925410b90add3364bde6da23e3b7ca3fc85a1659aea011a08a22dffd8997efbb73f0a601d993999e403"
}
},
{
Expand All @@ -202,7 +213,8 @@
},
"message": {
"message": "Hello World",
"signature": "3044022051ce8d97d116e903fbe4df52cd0d3b459468cd91e87d3231774882863afd87e502202e1aa9563dcd2c6fab88155583e6fc2fabfa3a1388b9be1678439e7dc6ce9109"
"signature": "3044022051ce8d97d116e903fbe4df52cd0d3b459468cd91e87d3231774882863afd87e502202e1aa9563dcd2c6fab88155583e6fc2fabfa3a1388b9be1678439e7dc6ce9109",
"schnorrSignature": "375a6057b5f548b9b5ae9e2f823015f6c48b2705698fd158d4ef9571330fe32ad090721451a4aeb7caeeae08083d0205bcca223b5a1ff9c8d61265e5f4f96d9a"
}
}
]
Expand Down
Loading