xref: /dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php (revision a896ec97b4d9a77a7ab6956f96aaa0e7987f57d1)
1<?php
2
3/**
4 * EC Private Key
5 *
6 * @author    Jim Wigginton <terrafrost@php.net>
7 * @copyright 2015 Jim Wigginton
8 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
9 * @link      http://phpseclib.sourceforge.net
10 */
11
12namespace phpseclib3\Crypt\EC;
13
14use phpseclib3\Common\Functions\Strings;
15use phpseclib3\Crypt\Common;
16use phpseclib3\Crypt\EC;
17use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve;
18use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve;
19use phpseclib3\Crypt\EC\Curves\Curve25519;
20use phpseclib3\Crypt\EC\Curves\Ed25519;
21use phpseclib3\Crypt\EC\Formats\Keys\PKCS1;
22use phpseclib3\Crypt\EC\Formats\Signature\ASN1 as ASN1Signature;
23use phpseclib3\Crypt\Hash;
24use phpseclib3\Exception\BadConfigurationException;
25use phpseclib3\Exception\UnsupportedOperationException;
26use phpseclib3\Math\BigInteger;
27
28/**
29 * EC Private Key
30 *
31 * @author  Jim Wigginton <terrafrost@php.net>
32 */
33final class PrivateKey extends EC implements Common\PrivateKey
34{
35    use Common\Traits\PasswordProtected;
36
37    /**
38     * Private Key dA
39     *
40     * sign() converts this to a BigInteger so one might wonder why this is a FiniteFieldInteger instead of
41     * a BigInteger. That's because a FiniteFieldInteger, when converted to a byte string, is null padded by
42     * a certain amount whereas a BigInteger isn't.
43     *
44     * @var object
45     */
46    protected $dA;
47
48    /**
49     * @var string
50     */
51    protected $secret;
52
53    /**
54     * Multiplies an encoded point by the private key
55     *
56     * Used by ECDH
57     *
58     * @param string $coordinates
59     * @return string
60     */
61    public function multiply($coordinates)
62    {
63        if (self::$forcedEngine === 'OpenSSL') {
64            throw new BadConfigurationException('Engine OpenSSL is not supported for the multiplication operation');
65        }
66
67        if (self::$forcedEngine === 'libsodium' && !$this->curve instanceof Curve25519) {
68            throw new BadConfigurationException('Engine libsodium is only supported for Curve25519');
69        }
70
71        if ($this->curve instanceof Curve25519 && self::$forcedEngine !== 'PHP') {
72            if (self::$forcedEngine === 'libsodium' && !function_exists('sodium_crypto_scalarmult')) {
73                throw new BadConfigurationException('Engine libsodium is forced but unsupported for Curve25519');
74            }
75            if (function_exists('sodium_crypto_scalarmult')) {
76                $dA = str_pad($this->dA->toBytes(), 32, "\0", STR_PAD_LEFT);
77                return sodium_crypto_scalarmult($dA, $coordinates);
78            }
79        }
80
81        if ($this->curve instanceof MontgomeryCurve) {
82            $point = [$this->curve->convertInteger(new BigInteger(strrev($coordinates), 256))];
83            $point = $this->curve->multiplyPoint($point, $this->dA);
84            return strrev($point[0]->toBytes(true));
85        }
86
87        if (!$this->curve instanceof TwistedEdwardsCurve) {
88            $coordinates = "\0$coordinates";
89        }
90
91        $point = PKCS1::extractPoint($coordinates, $this->curve);
92        $point = $this->curve->multiplyPoint($point, $this->dA);
93        if ($this->curve instanceof TwistedEdwardsCurve) {
94            return $this->curve->encodePoint($point);
95        }
96        if (empty($point)) {
97            throw new \RuntimeException('The infinity point is invalid');
98        }
99        return "\4" . $point[0]->toBytes(true) . $point[1]->toBytes(true);
100    }
101
102    /**
103     * Create a signature
104     *
105     * @see self::verify()
106     * @param string $message
107     * @return mixed
108     */
109    public function sign($message)
110    {
111        if ($this->curve instanceof MontgomeryCurve) {
112            throw new UnsupportedOperationException('Montgomery Curves cannot be used to create signatures');
113        }
114
115        $dA = $this->dA;
116        $order = $this->curve->getOrder();
117
118        $shortFormat = $this->shortFormat;
119        $format = $this->sigFormat;
120        if ($format === false) {
121            return false;
122        }
123
124        if (self::$forcedEngine === 'libsodium' && !$this->curve instanceof Ed25519) {
125            throw new BadConfigurationException('Engine libsodium is only supported for Ed25519');
126        }
127
128        // at this point either self::$forcedEngine is NOT libsodium or the curve is Ed25519
129
130        if ($this->curve instanceof Ed25519 && self::$forcedEngine !== 'PHP' && self::$forcedEngine !== 'OpenSSL') {
131            if (self::$forcedEngine === 'libsodium') {
132                if (!function_exists('sodium_crypto_sign_detached')) {
133                    throw new BadConfigurationException('Engine libsodium is forced but unsupported for Ed25519 / Ed448');
134                }
135                if (isset($this->context)) {
136                    throw new BadConfigurationException('Engine libsodium is forced but unsupported for Ed25519ctx (context)');
137                }
138            }
139            if (function_exists('sodium_crypto_sign_detached') && !isset($this->context)) {
140                $result = sodium_crypto_sign_detached($message, $this->withPassword()->toString('libsodium'));
141                return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result) : $result;
142            }
143        }
144
145        // at this point self::$forcedEngine CAN'T be libsodium so we won't check for it henceforth
146
147        if ($this->curve instanceof TwistedEdwardsCurve) {
148            if (self::$forcedEngine !== 'PHP') {
149                $keyTypeConstant = $this->curve instanceof Ed25519 ? 'OPENSSL_KEYTYPE_ED25519' : 'OPENSSL_KEYTYPE_ED448';
150                if (self::$forcedEngine === 'OpenSSL') {
151                    if (!defined($keyTypeConstant)) {
152                        throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for Ed25519 / Ed448');
153                    }
154                    // OpenSSL supports Ed25519/Ed448 but not Ed25519ctx (context), so skip if context is set
155                    if (isset($this->context)) {
156                        throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for Ed25519 / Ed448 curves with context\'s');
157                    }
158                }
159                if (defined($keyTypeConstant) && !isset($this->context)) {
160                    $result = '';
161                    // algorithm 0 is used because EdDSA has a built-in hash
162                    openssl_sign($message, $result, $this->withPassword()->toString('PKCS8'), 0);
163                    if ($result) {
164                        $signature = $shortFormat == 'SSH2'
165                            ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result)
166                            : $result;
167                        return $signature;
168                    }
169                    if (self::$forcedEngine === 'OpenSSL') {
170                        throw new BadConfigurationException('Engine OpenSSL is forced but was unable to create signature because of ' . openssl_error_string());
171                    }
172                }
173            }
174
175            // contexts (Ed25519ctx) are supported but prehashing (Ed25519ph) is not.
176            // quoting https://tools.ietf.org/html/rfc8032#section-8.5 ,
177            // "The Ed25519ph and Ed448ph variants ... SHOULD NOT be used"
178            $A = $this->curve->encodePoint($this->QA);
179            $curve = $this->curve;
180            $hash = new Hash($curve::HASH);
181
182            $secret = substr($hash->hash($this->secret), $curve::SIZE);
183
184            if ($curve instanceof Ed25519) {
185                $dom = !isset($this->context) ? '' :
186                    'SigEd25519 no Ed25519 collisions' . "\0" . chr(strlen($this->context)) . $this->context;
187            } else {
188                $context = isset($this->context) ? $this->context : '';
189                $dom = 'SigEd448' . "\0" . chr(strlen($context)) . $context;
190            }
191            // SHA-512(dom2(F, C) || prefix || PH(M))
192            $r = $hash->hash($dom . $secret . $message);
193            $r = strrev($r);
194            $r = new BigInteger($r, 256);
195            list(, $r) = $r->divide($order);
196            $R = $curve->multiplyPoint($curve->getBasePoint(), $r);
197            $R = $curve->encodePoint($R);
198            $k = $hash->hash($dom . $R . $A . $message);
199            $k = strrev($k);
200            $k = new BigInteger($k, 256);
201            list(, $k) = $k->divide($order);
202            $S = $k->multiply($dA)->add($r);
203            list(, $S) = $S->divide($order);
204            $S = str_pad(strrev($S->toBytes()), $curve::SIZE, "\0");
205            return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $R . $S) : $R . $S;
206        }
207
208        if (self::$forcedEngine === 'OpenSSL' && !function_exists('openssl_get_md_methods')) {
209            throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for ECDSA');
210        }
211
212        // at this point $forcedEngine is either PHP or null. either that OR openssl_get_md_methods() exists
213
214        if (self::$forcedEngine !== 'PHP') {
215            if (in_array($this->hash->getHash(), openssl_get_md_methods())) {
216                $signature = '';
217                // altho PHP's OpenSSL bindings only supported EC key creation in PHP 7.1 they've long
218                // supported signing / verification
219                // we use specified curves to avoid issues with OpenSSL possibly not supporting a given named curve;
220                // doing this may mean some curve-specific optimizations can't be used but idk if OpenSSL even
221                // has curve-specific optimizations
222                $result = openssl_sign($message, $signature, $this->withPassword()->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash());
223
224                if ($result) {
225                    if ($shortFormat == 'ASN1') {
226                        return $signature;
227                    }
228
229                    $loaded = ASN1Signature::load($signature);
230                    $r = $loaded['r'];
231                    $s = $loaded['s'];
232
233                    return $this->formatSignature($r, $s);
234                } elseif (self::$forcedEngine === 'OpenSSL') {
235                    throw new BadConfigurationException('Engine OpenSSL is forced but was unable to create signature because of ' . openssl_error_string());
236                }
237            } elseif (self::$forcedEngine === 'OpenSSL') {
238                throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for ECDSA / ' . $this->hash->getHash());
239            }
240        }
241
242        $e = $this->hash->hash($message);
243        $e = new BigInteger($e, 256);
244
245        $Ln = $this->hash->getLength() - $order->getLength();
246        $z = $Ln > 0 ? $e->bitwise_rightShift($Ln) : $e;
247
248        while (true) {
249            $k = BigInteger::randomRange(self::$one, $order->subtract(self::$one));
250            list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k);
251            $x = $x->toBigInteger();
252            list(, $r) = $x->divide($order);
253            if ($r->equals(self::$zero)) {
254                continue;
255            }
256            $kinv = $k->modInverse($order);
257            $temp = $z->add($dA->multiply($r));
258            $temp = $kinv->multiply($temp);
259            list(, $s) = $temp->divide($order);
260            if (!$s->equals(self::$zero)) {
261                break;
262            }
263        }
264
265        // the following is an RFC6979 compliant implementation of deterministic ECDSA
266        // it's unused because it's mainly intended for use when a good CSPRNG isn't
267        // available. if phpseclib's CSPRNG isn't good then even key generation is
268        // suspect
269        /*
270        // if this were actually being used it'd probably be better if this lived in load() and createKey()
271        $this->q = $this->curve->getOrder();
272        $dA = $this->dA->toBigInteger();
273        $this->x = $dA;
274
275        $h1 = $this->hash->hash($message);
276        $k = $this->computek($h1);
277        list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k);
278        $x = $x->toBigInteger();
279        list(, $r) = $x->divide($this->q);
280        $kinv = $k->modInverse($this->q);
281        $h1 = $this->bits2int($h1);
282        $temp = $h1->add($dA->multiply($r));
283        $temp = $kinv->multiply($temp);
284        list(, $s) = $temp->divide($this->q);
285        */
286
287        return $this->formatSignature($r, $s);
288    }
289
290    /**
291     * Returns the private key
292     *
293     * @param string $type
294     * @param array $options optional
295     * @return string
296     */
297    public function toString($type, array $options = [])
298    {
299        $type = self::validatePlugin('Keys', $type, 'savePrivateKey');
300
301        return $type::savePrivateKey($this->dA, $this->curve, $this->QA, $this->secret, $this->password, $options);
302    }
303
304    /**
305     * Returns the public key
306     *
307     * @see self::getPrivateKey()
308     * @return mixed
309     */
310    public function getPublicKey()
311    {
312        $format = 'PKCS8';
313        if ($this->curve instanceof MontgomeryCurve) {
314            $format = 'MontgomeryPublic';
315        }
316
317        $type = self::validatePlugin('Keys', $format, 'savePublicKey');
318
319        $key = $type::savePublicKey($this->curve, $this->QA);
320        $key = EC::loadFormat($format, $key);
321        if ($this->curve instanceof MontgomeryCurve) {
322            return $key;
323        }
324        $key = $key
325            ->withHash($this->hash->getHash())
326            ->withSignatureFormat($this->shortFormat);
327        if ($this->curve instanceof TwistedEdwardsCurve) {
328            $key = $key->withContext($this->context);
329        }
330        return $key;
331    }
332
333    /**
334     * Returns a signature in the appropriate format
335     *
336     * @return string
337     */
338    private function formatSignature(BigInteger $r, BigInteger $s)
339    {
340        $format = $this->sigFormat;
341
342        $temp = new \ReflectionMethod($format, 'save');
343        $paramCount = $temp->getNumberOfRequiredParameters();
344
345        // @codingStandardsIgnoreStart
346        switch ($paramCount) {
347            case 2: return $format::save($r, $s);
348            case 3: return $format::save($r, $s, $this->getCurve());
349            case 4: return $format::save($r, $s, $this->getCurve(), $this->getLength());
350        }
351        // @codingStandardsIgnoreEnd
352
353        // presumably the only way you could get to this is if you were using a custom plugin
354        throw new UnsupportedOperationException("$format::save() has $paramCount parameters - the only valid parameter counts are 2 or 3");
355    }
356}
357