Skip to content

Commit 58d419a

Browse files
authored
Merge pull request #18 from make-software/fix-secp256k1-sign
Fixed Secp256K1 sign method
2 parents e9759d1 + 42a83c2 commit 58d419a

10 files changed

Lines changed: 168 additions & 92 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,25 @@ jobs:
1919
uses: shivammathur/setup-php@v2
2020
with:
2121
php-version: '7.4'
22+
ini-values: extension=secp256k1.so
23+
24+
- name: Clone libsecp256k1 repository
25+
uses: GuillaumeFalourd/clone-github-repo-action@v2
26+
with:
27+
owner: 'bitcoin-core'
28+
repository: 'secp256k1'
29+
30+
- name: Configure secp256k1
31+
run: cd secp256k1 && ./autogen.sh && ./configure --enable-experimental --enable-module-{ecdh,recovery} && make && sudo make install && cd ..
32+
33+
- name: Clone secp256k1-php repository
34+
uses: GuillaumeFalourd/clone-github-repo-action@v2
35+
with:
36+
owner: 'Bit-Wasp'
37+
repository: 'secp256k1-php'
38+
39+
- name: Install secp256k1-php
40+
run: cd secp256k1-php/secp256k1 && phpize && ./configure --with-secp256k1 && make && sudo make install && cd ..
2241

2342
- uses: actions/checkout@v3
2443

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@ The PHP SDK allows developers to interact with the Casper Network using PHP. Thi
66
composer require make-software/casper-php-sdk
77
```
88

9+
### IMPORTANT
10+
For using `Secp256K1` keys you need to install and enable [secp256k1-php](https://github.com/Bit-Wasp/secp256k1-php) extension:
11+
```bash
12+
git clone git@github.com:bitcoin-core/secp256k1 && \
13+
cd secp256k1 && \
14+
./autogen.sh && \
15+
./configure --enable-experimental --enable-module-{ecdh,recovery} && \
16+
make && sudo make install && \
17+
cd ../
18+
```
19+
```bash
20+
git clone git@github.com:Bit-Wasp/secp256k1-php && \
21+
cd secp256k1-php/secp256k1 && \
22+
phpize && \
23+
./configure --with-secp256k1 && \
24+
make && sudo make install && \
25+
cd ../../
26+
```
27+
Enable extension by adding the following line to your `php.ini` file
28+
```
29+
extension=secp256k1.so
30+
```
31+
932
## Usage
1033
### Creating RpcClient
1134
Create `RpcClient` by passing node url to constructor

src/Rpc/RpcClient.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,11 @@ private function rpcCallMethod(string $method, array $params = array()): array
414414

415415
$decodedResponse = json_decode($response, true);
416416

417-
if (isset($decodedResponse['error'])) {
418-
throw new RpcError($decodedResponse['error']['message'], $decodedResponse['error']['code']);
417+
if ($decodedResponse === null || isset($decodedResponse['error'])) {
418+
$message = $decodedResponse['error']['message'] ?? 'Empty response';
419+
$code = $decodedResponse['error']['code'] ?? 0;
420+
421+
throw new RpcError($message, $code);
419422
}
420423

421424
$this->lastApiVersion = $decodedResponse['result']['api_version'];

src/Service/DeployService.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ public static function makeDeploy(
6464
public static function signDeploy(Deploy $deploy, AsymmetricKey $key): Deploy
6565
{
6666
$signer = CLPublicKeySerializer::fromString(
67-
KeysUtil::addPrefixToPublicKey($key->getSignatureAlgorithm(), $key->getPublicKey())
67+
KeysUtil::addPrefixToPublicKey(
68+
$key->getSignatureAlgorithm(),
69+
$key->getPublicKey()
70+
)
6871
);
6972
$signature = KeysUtil::addPrefixToPublicKey(
7073
$key->getSignatureAlgorithm(),

src/Util/CEP57ChecksumUtil.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
class CEP57ChecksumUtil
66
{
7-
private const BLAKE2B_HASH_LENGTH = 64;
87
private const HEX_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
98

109
/**
@@ -44,7 +43,7 @@ public static function hasChecksum(string $hex): bool
4443
public static function encode(array $bytes): string
4544
{
4645
$nibbles = self::byteToNibbles($bytes);
47-
$hashBits = self::bytesToBitsCycle(HashUtil::blake2bHash($bytes, self::BLAKE2B_HASH_LENGTH));
46+
$hashBits = self::bytesToBitsCycle(HashUtil::blake2bHash($bytes));
4847

4948
$bitIndex = 0;
5049
foreach ($nibbles as $nibble) {

src/Util/Crypto/AsymmetricKey.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ abstract public function sign(string $message): string;
8888
/**
8989
* Verify hex-encoded signature
9090
*
91-
* @param string $signature
91+
* @param string $hexSignature
9292
* @param string $message
9393
* @return bool
9494
*/
95-
public abstract function verify(string $signature, string $message): bool;
95+
public abstract function verify(string $hexSignature, string $message): bool;
9696
}

src/Util/Crypto/Ed25519Key.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ public function sign(string $message): string
8282
*
8383
* @throws \SodiumException
8484
*/
85-
public function verify(string $signature, string $message): bool
85+
public function verify(string $hexSignature, string $message): bool
8686
{
87-
$signature = ByteUtil::hexToString($signature);
88-
return sodium_crypto_sign_verify_detached($signature, $message, $this->publicKey);
87+
$hexSignature = ByteUtil::hexToString($hexSignature);
88+
return sodium_crypto_sign_verify_detached($hexSignature, $message, $this->publicKey);
8989
}
9090

9191
/**

src/Util/Crypto/Secp256K1Key.php

Lines changed: 73 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,33 @@
55
use Casper\Util\ByteUtil;
66

77
use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface;
8-
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
9-
use Mdanter\Ecc\Crypto\Signature\Signature;
10-
use Mdanter\Ecc\Crypto\Signature\SignatureInterface;
11-
use Mdanter\Ecc\Crypto\Signature\Signer;
12-
use Mdanter\Ecc\Crypto\Signature\SignHasher;
138
use Mdanter\Ecc\EccFactory;
14-
use Mdanter\Ecc\Math\GmpMathInterface;
15-
use Mdanter\Ecc\Primitives\GeneratorPoint;
16-
use Mdanter\Ecc\Random\RandomGeneratorFactory;
179
use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
1810
use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer;
1911
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
2012
use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer;
21-
use Phactor\Math;
2213

2314
/**
24-
* Secp255K1 key implementation
15+
* Secp256K1 key implementation
2516
*/
2617
final class Secp256K1Key extends AsymmetricKey
2718
{
28-
use Math;
29-
19+
protected const RESULT_OK = 1;
3020
protected const HASHING_ALGORITHM = 'sha256';
3121

32-
protected GmpMathInterface $adapter;
33-
34-
protected GeneratorPoint $generator;
35-
36-
protected PemPrivateKeySerializer $privateKeySerializer;
37-
38-
protected PemPublicKeySerializer $publicKeySerializer;
39-
40-
protected PrivateKeyInterface $secpPrivateKey;
41-
42-
protected PublicKeyInterface $secpPublicKey;
22+
protected PrivateKeyInterface $privateKeyObject;
4323

44-
public function __construct(PrivateKeyInterface $privateKey = null)
24+
public function __construct(PrivateKeyInterface $privateKeyObject = null)
4525
{
46-
$this->adapter = EccFactory::getAdapter();
47-
$this->generator = EccFactory::getSecgCurves()->generator256k1();
48-
$this->privateKeySerializer = new PemPrivateKeySerializer(new DerPrivateKeySerializer($this->adapter));
49-
$this->publicKeySerializer = new PemPublicKeySerializer(new DerPublicKeySerializer($this->adapter));
26+
$this->privateKeyObject = $privateKeyObject ??
27+
EccFactory::getSecgCurves()
28+
->generator256k1()
29+
->createPrivateKey();
5030

51-
$this->secpPrivateKey = $privateKey ?? $this->generator->createPrivateKey();
52-
$privateKeyString = ByteUtil::hexToString($this->encodeHex((string) $this->secpPrivateKey->getSecret(), false));
31+
$privateKey = ByteUtil::hexToString(gmp_strval($this->privateKeyObject->getSecret(), 16));
32+
$publicKey= ByteUtil::hexToString($this->getCompressedPublicKeyHex());
5333

54-
$this->secpPublicKey = $this->secpPrivateKey->getPublicKey();
55-
$xHex = str_pad($this->encodeHex((string) $this->secpPublicKey->getPoint()->getX(), false), 64, "0", STR_PAD_LEFT);
56-
$compressedPublicKeyPrefix = $this->Modulo((string) $this->secpPublicKey->getPoint()->getY(), '2') === '1' ? '03' : '02';
57-
$publicKeyCompressedString = ByteUtil::hexToString($compressedPublicKeyPrefix . $xHex);
58-
59-
parent::__construct($publicKeyCompressedString, $privateKeyString, self::ALGO_SECP255K1);
34+
parent::__construct($publicKey, $privateKey, self::ALGO_SECP255K1);
6035
}
6136

6237
/**
@@ -65,11 +40,9 @@ public function __construct(PrivateKeyInterface $privateKey = null)
6540
*/
6641
public static function createFromPrivateKeyFile(string $pathToPrivateKey): self
6742
{
68-
$adapter = EccFactory::getAdapter();
69-
$privateKeySerializer = new PemPrivateKeySerializer(new DerPrivateKeySerializer($adapter));
70-
7143
return new self(
72-
$privateKeySerializer->parse(file_get_contents($pathToPrivateKey))
44+
(new PemPrivateKeySerializer(new DerPrivateKeySerializer()))
45+
->parse(file_get_contents($pathToPrivateKey))
7346
);
7447
}
7548

@@ -79,7 +52,8 @@ public static function createFromPrivateKeyFile(string $pathToPrivateKey): self
7952
*/
8053
public function exportPublicKeyInPem(): string
8154
{
82-
return $this->publicKeySerializer->serialize($this->secpPublicKey) . PHP_EOL;
55+
return (new PemPublicKeySerializer(new DerPublicKeySerializer()))
56+
->serialize($this->privateKeyObject->getPublicKey()) . PHP_EOL;
8357
}
8458

8559
/**
@@ -88,73 +62,94 @@ public function exportPublicKeyInPem(): string
8862
*/
8963
public function exportPrivateKeyInPem(): string
9064
{
91-
return $this->privateKeySerializer->serialize($this->secpPrivateKey) . PHP_EOL;
65+
return (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))
66+
->serialize($this->privateKeyObject) . PHP_EOL;
9267
}
9368

9469
/**
9570
* @inheritDoc
9671
* @param string $message
9772
* @return string
73+
*
74+
* @throws \Exception
9875
*/
9976
public function sign(string $message): string
10077
{
101-
$hasher = new SignHasher(self::HASHING_ALGORITHM, $this->adapter);
102-
$hash = $hasher->makeHash($message, $this->generator);
78+
$context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
79+
80+
$signature = null;
81+
$signResult = secp256k1_ecdsa_sign(
82+
$context,
83+
$signature,
84+
hash(self::HASHING_ALGORITHM, $message, true),
85+
$this->privateKey
86+
);
10387

104-
$random = RandomGeneratorFactory::getRandomGenerator();
105-
$randomK = $random->generate($this->generator->getOrder());
88+
if ($signResult !== self::RESULT_OK) {
89+
throw new \Exception("Failed to create signature");
90+
}
10691

107-
$signature = (new Signer($this->adapter))
108-
->sign($this->secpPrivateKey, $hash, $randomK);
92+
$signatureSerialized = '';
93+
secp256k1_ecdsa_signature_serialize_compact($context, $signatureSerialized, $signature);
10994

110-
return $this->signatureToHex($signature);
95+
return ByteUtil::stringToHex($signatureSerialized);
11196
}
11297

11398
/**
11499
* @inheritDoc
115-
* @param string $signature
100+
* @param string $hexSignature
116101
* @param string $message
117102
* @return bool
103+
*
104+
* @throws \Exception
118105
*/
119-
public function verify(string $signature, string $message): bool
106+
public function verify(string $hexSignature, string $message): bool
120107
{
121-
try {
122-
$signature = $this->hexToSignature($signature);
108+
$context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
123109

124-
$hasher = new SignHasher(self::HASHING_ALGORITHM);
125-
$hash = $hasher->makeHash($message, $this->generator);
110+
$publicKey = null;
111+
$publicKeyParseResult = secp256k1_ec_pubkey_parse($context, $publicKey, $this->publicKey);
126112

127-
return (new Signer($this->adapter))
128-
->verify($this->secpPublicKey, $signature, $hash);
129-
} catch (\Exception $e) {
130-
return false;
113+
if ($publicKeyParseResult !== self::RESULT_OK) {
114+
throw new \Exception("Failed to parse public key");
131115
}
132-
}
133116

134-
private function signatureToHex(SignatureInterface $signature): string
135-
{
136-
$r = $signature->getR();
137-
$s = $signature->getS();
117+
$signature = null;
118+
$signatureParseResult = secp256k1_ecdsa_signature_parse_compact(
119+
$context,
120+
$signature,
121+
ByteUtil::hexToString($hexSignature)
122+
);
123+
124+
if ($signatureParseResult !== self::RESULT_OK) {
125+
throw new \Exception("Failed to parse DER signature");
126+
}
138127

139-
return gmp_strval($r, 16) . gmp_strval($s, 16);
128+
$isVerified = secp256k1_ecdsa_verify(
129+
$context,
130+
$signature,
131+
hash(self::HASHING_ALGORITHM, $message, true),
132+
$publicKey
133+
);
134+
135+
return $isVerified === self::RESULT_OK;
140136
}
141137

142-
private function hexToSignature(string $hex): SignatureInterface
138+
private function getCompressedPublicKeyHex(): string
143139
{
144-
$hex = mb_strtolower($hex);
140+
$xPointValue = $this->privateKeyObject
141+
->getPublicKey()
142+
->getPoint()
143+
->getX();
145144

146-
if (strpos($hex, '0x') >= 0) {
147-
$count = 1;
148-
$hex = str_replace('0x', '', $hex, $count);
149-
}
150-
151-
if (mb_strlen($hex) !== 128) {
152-
throw new \InvalidArgumentException('Binary string was not correct.');
153-
}
145+
$yPointValue = $this->privateKeyObject
146+
->getPublicKey()
147+
->getPoint()
148+
->getY();
154149

155-
$r = mb_substr($hex, 0, 64);
156-
$s = mb_substr($hex, 64, 64);
150+
$xPointValueHex = str_pad(gmp_strval($xPointValue, 16), 64, '0', STR_PAD_LEFT);
151+
$prefix = gmp_strval(gmp_mod($yPointValue, 2)) === '1' ? '03' : '02';
157152

158-
return new Signature(gmp_init($r, 16), gmp_init($s, 16));
153+
return $prefix . $xPointValueHex;
159154
}
160155
}

tests/Functional/CEP57ChecksumUtilTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class CEP57ChecksumUtilTest extends TestCase
1414
*/
1515
public function testHasCheckSum(): void
1616
{
17-
$hexWithCheckSum = 'de8649985929090b7CB225e35a5A7B4087fb8fcB3D18C8C9a58DA68e4edA8a2E';
17+
$hexWithCheckSum = 'dE8649985929090B7Cb225e35a5a7B4087Fb8FCB3D18c8c9a58da68e4eDA8A2e';
1818
$this->assertTrue(CEP57ChecksumUtil::hasChecksum($hexWithCheckSum));
1919

2020
$hexWithoutCheckSum = 'de8649985929090b7cb225e35a5a7b4087fb8fcb3d18c8c9a58da68e4eda8a2e';
@@ -26,7 +26,7 @@ public function testHasCheckSum(): void
2626
*/
2727
public function testEncode(): void
2828
{
29-
$hex = 'de8649985929090b7CB225e35a5A7B4087fb8fcB3D18C8C9a58DA68e4edA8a2E';
29+
$hex = 'dE8649985929090B7Cb225e35a5a7B4087Fb8FCB3D18c8c9a58da68e4eDA8A2e';
3030
$bytes = ByteUtil::hexToByteArray($hex);
3131

3232
$encodedHex = CEP57ChecksumUtil::encode($bytes);
@@ -38,7 +38,7 @@ public function testEncode(): void
3838
*/
3939
public function testDecode(): void
4040
{
41-
$hex = 'de8649985929090b7CB225e35a5A7B4087fb8fcB3D18C8C9a58DA68e4edA8a2E';
41+
$hex = 'dE8649985929090B7Cb225e35a5a7B4087Fb8FCB3D18c8c9a58da68e4eDA8A2e';
4242

4343
$decodedByteArray = CEP57ChecksumUtil::decode($hex);
4444
$this->assertIsArray($decodedByteArray);

0 commit comments

Comments
 (0)