Completed
Pull Request — master (#591)
by thomas
15:59
created

EcAdapter::signCompact()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Adapter;
6
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PrivateKey;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PublicKey;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\CompactSignature;
11
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\Signature;
12
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
13
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
14
use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\CompactSignatureInterface;
15
use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SignatureInterface;
16
use BitWasp\Bitcoin\Crypto\Random\RbgInterface;
17
use BitWasp\Bitcoin\Crypto\Random\Rfc6979;
18
use BitWasp\Bitcoin\Math\Math;
19
use BitWasp\Buffertools\BufferInterface;
20
use Mdanter\Ecc\Crypto\Signature\Signer;
21
use Mdanter\Ecc\Primitives\GeneratorPoint;
22
use Mdanter\Ecc\Primitives\PointInterface;
23
24
class EcAdapter implements EcAdapterInterface
25
{
26
    /**
27
     * @var Math
28
     */
29
    private $math;
30
31
    /**
32
     * @var GeneratorPoint
33
     */
34
    private $generator;
35
36
    /**
37
     * @param Math $math
38
     * @param GeneratorPoint $generator
39
     */
40 4
    public function __construct(Math $math, GeneratorPoint $generator)
41
    {
42 4
        $this->math = $math;
43 4
        $this->generator = $generator;
44 4
    }
45
46
    /**
47
     * @return Math
48
     */
49 1357
    public function getMath(): Math
50
    {
51 1357
        return $this->math;
52
    }
53
54
    /**
55
     * @return GeneratorPoint
56
     */
57 194
    public function getGenerator()
58
    {
59 194
        return $this->generator;
60
    }
61
62
    /**
63
     * @param \GMP $scalar
64
     * @param bool|false $compressed
65
     * @return PrivateKeyInterface
66
     */
67 40
    public function getPrivateKey(\GMP $scalar, bool $compressed = false): PrivateKeyInterface
68
    {
69 40
        return new PrivateKey($this, $scalar, $compressed);
70
    }
71
72
    /**
73
     * @param PointInterface $point
74
     * @param bool|false $compressed
75
     * @return PublicKey
76
     */
77 55
    public function getPublicKey(PointInterface $point, bool $compressed = false)
78
    {
79 55
        return new PublicKey($this, $point, $compressed);
80
    }
81
82
    /**
83
     * @param \GMP $r
84
     * @param \GMP $s
85
     * @return Signature
86
     */
87
    public function getSignature(\GMP $r, \GMP $s): SignatureInterface
88
    {
89
        return new Signature($this, $r, $s);
90
    }
91
92
    /**
93
     * @param BufferInterface $messageHash
94
     * @param PublicKey $publicKey
95
     * @param Signature $signature
96
     * @return bool
97
     */
98 155
    private function doVerify(BufferInterface $messageHash, PublicKey $publicKey, Signature $signature)
99
    {
100 155
        $hash = gmp_init($messageHash->getHex(), 16);
101 155
        $signer = new Signer($this->math);
102 155
        return $signer->verify($publicKey, $signature, $hash);
103
    }
104
105
    /**
106
     * @param BufferInterface $messageHash
107
     * @param PublicKeyInterface $publicKey
108
     * @param SignatureInterface $signature
109
     * @return bool
110
     */
111 155
    public function verify(BufferInterface $messageHash, PublicKeyInterface $publicKey, SignatureInterface $signature): bool
112
    {
113
        /** @var PublicKey $publicKey */
114
        /** @var Signature $signature */
115 155
        return $this->doVerify($messageHash, $publicKey, $signature);
0 ignored issues
show
Compatibility introduced by
$publicKey of type object<BitWasp\Bitcoin\C...Key\PublicKeyInterface> is not a sub-type of object<BitWasp\Bitcoin\C...l\PhpEcc\Key\PublicKey>. It seems like you assume a concrete implementation of the interface BitWasp\Bitcoin\Crypto\E...\Key\PublicKeyInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
116
    }
117
118
    /**
119
     * @param BufferInterface $messageHash
120
     * @param PrivateKey $privateKey
121
     * @param RbgInterface|null $rbg
122
     * @return Signature
123
     */
124 33
    private function doSign(BufferInterface $messageHash, PrivateKey $privateKey, RbgInterface $rbg = null): Signature
125
    {
126 33
        $rbg = $rbg ?: new Rfc6979($this, $privateKey, $messageHash);
127 33
        $randomK = gmp_init($rbg->bytes(32)->getHex(), 16);
128 33
        $hash = gmp_init($messageHash->getHex(), 16);
129
130 33
        $signer = new Signer($this->math);
131 33
        $signature = $signer->sign($privateKey, $hash, $randomK);
132 33
        $s = $signature->getS();
133
134
        // if s is less than half the curve order, invert s
135 33
        if (!$this->validateSignatureElement($s, true)) {
136 17
            $s = $this->math->sub($this->generator->getOrder(), $s);
137
        }
138
139 33
        return new Signature($this, $signature->getR(), $s);
0 ignored issues
show
Bug introduced by
It seems like $s defined by $this->math->sub($this->...erator->getOrder(), $s) on line 136 can also be of type resource; however, BitWasp\Bitcoin\Crypto\E...ignature::__construct() does only seem to accept object<GMP>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
140
    }
141
142
    /**
143
     * @param BufferInterface $messageHash
144
     * @param PrivateKeyInterface $privateKey
145
     * @param RbgInterface $rbg
146
     * @return SignatureInterface
147
     * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure
148
     */
149 33
    public function sign(BufferInterface $messageHash, PrivateKeyInterface $privateKey, RbgInterface $rbg = null): SignatureInterface
150
    {
151
        /** @var PrivateKey $privateKey */
152 33
        return $this->doSign($messageHash, $privateKey, $rbg);
153
    }
154
155
    /**
156
     * @param BufferInterface $messageHash
157
     * @param CompactSignatureInterface $signature
158
     * @return PublicKeyInterface
159
     * @throws \Exception
160
     */
161 7
    public function recover(BufferInterface $messageHash, CompactSignatureInterface $signature): PublicKeyInterface
162
    {
163 7
        $math = $this->getMath();
164 7
        $G = $this->getGenerator();
165
166 7
        $zero = gmp_init(0);
167 7
        $one = gmp_init(1);
168
169 7
        $r = $signature->getR();
170 7
        $s = $signature->getS();
171 7
        $recGMP = gmp_init($signature->getRecoveryId(), 10);
172 7
        $isYEven = $math->cmp($math->bitwiseAnd($recGMP, $one), $zero) !== 0;
173 7
        $isSecondKey = $math->cmp($math->bitwiseAnd($recGMP, gmp_init(2)), $zero) !== 0;
174 7
        $curve = $G->getCurve();
175
176
        // Precalculate (p + 1) / 4 where p is the field order
177 7
        $p_over_four = $math->div($math->add($curve->getPrime(), $one), gmp_init(4));
178
179
        // 1.1 Compute x
180 7
        if (!$isSecondKey) {
181 7
            $x = $r;
182
        } else {
183 1
            $x = $math->add($r, $G->getOrder());
184
        }
185
186
        // 1.3 Convert x to point
187 7
        $alpha = $math->mod($math->add($math->add($math->pow($x, 3), $math->mul($curve->getA(), $x)), $curve->getB()), $curve->getPrime());
0 ignored issues
show
Bug introduced by
It seems like $x defined by $math->add($r, $G->getOrder()) on line 183 can also be of type resource; however, Mdanter\Ecc\Math\GmpMath::pow() does only seem to accept object<GMP>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $x defined by $math->add($r, $G->getOrder()) on line 183 can also be of type resource; however, Mdanter\Ecc\Math\GmpMath::mul() does only seem to accept object<GMP>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
188 7
        $beta = $math->powmod($alpha, $p_over_four, $curve->getPrime());
189
190
        // If beta is even, but y isn't or vice versa, then convert it,
191
        // otherwise we're done and y=beta.
192 7
        if ($math->isEven($beta) === $isYEven) {
193 4
            $y = $math->sub($curve->getPrime(), $beta);
194
        } else {
195 6
            $y = $beta;
196
        }
197
198
        // 1.4 Check that nR is at infinity (implicitly done in constructor)
199 7
        $R = $G->getCurve()->getPoint($x, $y);
0 ignored issues
show
Bug introduced by
It seems like $x defined by $math->add($r, $G->getOrder()) on line 183 can also be of type resource; however, Mdanter\Ecc\Primitives\C...FpInterface::getPoint() does only seem to accept object<GMP>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
200
201 7
        $point_negate = function (PointInterface $p) use ($math, $G) {
202 7
            return $G->getCurve()->getPoint($p->getX(), $math->mul($p->getY(), gmp_init('-1', 10)));
203 7
        };
204
205
        // 1.6.1 Compute a candidate public key Q = r^-1 (sR - eG)
206 7
        $rInv = $math->inverseMod($r, $G->getOrder());
207 7
        $eGNeg = $point_negate($G->mul($messageHash->getGmp()));
208 7
        $Q = $R->mul($s)->add($eGNeg)->mul($rInv);
209
210
        // 1.6.2 Test Q as a public key
211 7
        $Qk = new PublicKey($this, $Q, $signature->isCompressed());
212 7
        if ($this->verify($messageHash, $Qk, $signature->convert())) {
213 6
            return $Qk;
214
        }
215
216 1
        throw new \Exception('Unable to recover public key');
217
    }
218
219
    /**
220
     * Attempt to calculate the public key recovery param by trial and error
221
     *
222
     * @param \GMP $r
223
     * @param \GMP $s
224
     * @param BufferInterface $messageHash
225
     * @param PublicKey $publicKey
226
     * @return int
227
     * @throws \Exception
228
     */
229 6
    public function calcPubKeyRecoveryParam(\GMP $r, \GMP $s, BufferInterface $messageHash, PublicKey $publicKey): int
230
    {
231 6
        $Q = $publicKey->getPoint();
232 6
        for ($i = 0; $i < 4; $i++) {
233
            try {
234 6
                $recover = $this->recover($messageHash, new CompactSignature($this, $r, $s, $i, $publicKey->isCompressed()));
235 5
                if ($Q->equals($recover->getPoint())) {
236 5
                    return $i;
237
                }
238 1
            } catch (\Exception $e) {
239 1
                continue;
240
            }
241
        }
242
243 1
        throw new \Exception('Failed to find valid recovery factor');
244
    }
245
246
    /**
247
     * @param BufferInterface $messageHash
248
     * @param PrivateKey $privateKey
249
     * @param RbgInterface|null $rbg
250
     * @return CompactSignature
251
     * @throws \Exception
252
     */
253 5
    private function doSignCompact(BufferInterface $messageHash, PrivateKey $privateKey, RbgInterface $rbg = null): CompactSignature
254
    {
255 5
        $sign = $this->sign($messageHash, $privateKey, $rbg);
256
257
        // calculate the recovery param
258
        // there should be a way to get this when signing too, but idk how ...
259 5
        return new CompactSignature(
260 5
            $this,
261 5
            $sign->getR(),
262 5
            $sign->getS(),
263 5
            $this->calcPubKeyRecoveryParam($sign->getR(), $sign->getS(), $messageHash, $privateKey->getPublicKey()),
264 5
            $privateKey->isCompressed()
265
        );
266
    }
267
268
    /**
269
     * @param PrivateKeyInterface $privateKey
270
     * @param BufferInterface $messageHash
271
     * @param RbgInterface $rbg
272
     * @return CompactSignatureInterface
273
     */
274 5
    public function signCompact(BufferInterface $messageHash, PrivateKeyInterface $privateKey, RbgInterface $rbg = null): CompactSignatureInterface
275
    {
276
        /** @var PrivateKey $privateKey */
277 5
        return $this->doSignCompact($messageHash, $privateKey, $rbg);
278
    }
279
280
    /**
281
     * @param BufferInterface $privateKey
282
     * @return bool
283
     */
284 42
    public function validatePrivateKey(BufferInterface $privateKey): bool
285
    {
286 42
        $math = $this->math;
287 42
        $scalar = $privateKey->getGmp();
288 42
        return $math->cmp($scalar, gmp_init(0)) > 0 && $math->cmp($scalar, $this->getGenerator()->getOrder()) < 0;
289
    }
290
291
    /**
292
     * @param \GMP $element
293
     * @param bool $half
294
     * @return bool
295
     */
296 35
    public function validateSignatureElement(\GMP $element, bool $half = false): bool
297
    {
298 35
        $math = $this->getMath();
299 35
        $against = $this->getGenerator()->getOrder();
300 35
        if ($half) {
301 35
            $against = $math->rightShift($against, 1);
302
        }
303
304 35
        return $math->cmp($element, $against) < 0 && $math->cmp($element, gmp_init(0)) !== 0;
305
    }
306
307
    /**
308
     * @param BufferInterface $publicKey
309
     * @return PublicKeyInterface
310
     * @throws \Exception
311
     */
312 187
    public function publicKeyFromBuffer(BufferInterface $publicKey): PublicKeyInterface
313
    {
314 187
        $prefix = $publicKey->slice(0, 1)->getBinary();
315 187
        $size = $publicKey->getSize();
316 187
        $compressed = false;
317 187
        if ($prefix === PublicKey::KEY_UNCOMPRESSED || $prefix === "\x06" || $prefix === "\x07") {
318 74
            if ($size !== PublicKey::LENGTH_UNCOMPRESSED) {
319 74
                throw new \Exception('Invalid length for uncompressed key');
320
            }
321 120
        } else if ($prefix === PublicKey::KEY_COMPRESSED_EVEN || $prefix === PublicKey::KEY_COMPRESSED_ODD) {
322 120
            if ($size !== PublicKey::LENGTH_COMPRESSED) {
323
                throw new \Exception('Invalid length for compressed key');
324
            }
325 120
            $compressed = true;
326
        } else {
327
            throw new \Exception('Unknown public key prefix');
328
        }
329
        
330 187
        $x = $publicKey->slice(1, 32)->getGmp();
331 187
        $curve = $this->generator->getCurve();
332 187
        $y = $compressed
333 120
            ? $curve->recoverYfromX($prefix === PublicKey::KEY_COMPRESSED_ODD, $x)
334 187
            : $publicKey->slice(33, 32)->getGmp();
335
336 187
        return new PublicKey(
337 187
            $this,
338 187
            $curve->getPoint($x, $y),
339 187
            $compressed,
340 187
            $prefix
341
        );
342
    }
343
}
344