1<?php
2
3/**
4 * EC Private Key
5 *
6 * @category  Crypt
7 * @package   EC
8 * @author    Jim Wigginton <terrafrost@php.net>
9 * @copyright 2015 Jim Wigginton
10 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
11 * @link      http://phpseclib.sourceforge.net
12 */
13
14namespace phpseclib3\Crypt\EC;
15
16use phpseclib3\Common\Functions\Strings;
17use phpseclib3\Crypt\Common;
18use phpseclib3\Crypt\EC;
19use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve;
20use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve;
21use phpseclib3\Crypt\EC\Curves\Curve25519;
22use phpseclib3\Crypt\EC\Curves\Ed25519;
23use phpseclib3\Crypt\EC\Formats\Keys\PKCS1;
24use phpseclib3\Crypt\EC\Formats\Signature\ASN1 as ASN1Signature;
25use phpseclib3\Crypt\Hash;
26use phpseclib3\Exception\UnsupportedOperationException;
27use phpseclib3\Math\BigInteger;
28
29/**
30 * EC Private Key
31 *
32 * @package EC
33 * @author  Jim Wigginton <terrafrost@php.net>
34 * @access  public
35 */
36class PrivateKey extends EC implements Common\PrivateKey
37{
38    use Common\Traits\PasswordProtected;
39
40    /**
41     * Private Key dA
42     *
43     * sign() converts this to a BigInteger so one might wonder why this is a FiniteFieldInteger instead of
44     * a BigInteger. That's because a FiniteFieldInteger, when converted to a byte string, is null padded by
45     * a certain amount whereas a BigInteger isn't.
46     *
47     * @var object
48     */
49    protected $dA;
50
51    /**
52     * Multiplies an encoded point by the private key
53     *
54     * Used by ECDH
55     *
56     * @param string $coordinates
57     * @return string
58     */
59    public function multiply($coordinates)
60    {
61        if ($this->curve instanceof MontgomeryCurve) {
62            if ($this->curve instanceof Curve25519 && self::$engines['libsodium']) {
63                return sodium_crypto_scalarmult($this->dA->toBytes(), $coordinates);
64            }
65
66            $point = [$this->curve->convertInteger(new BigInteger(strrev($coordinates), 256))];
67            $point = $this->curve->multiplyPoint($point, $this->dA);
68            return strrev($point[0]->toBytes(true));
69        }
70        if (!$this->curve instanceof TwistedEdwardsCurve) {
71            $coordinates = "\0$coordinates";
72        }
73        $point = PKCS1::extractPoint($coordinates, $this->curve);
74        $point = $this->curve->multiplyPoint($point, $this->dA);
75        if ($this->curve instanceof TwistedEdwardsCurve) {
76            return $this->curve->encodePoint($point);
77        }
78        if (empty($point)) {
79            throw new \RuntimeException('The infinity point is invalid');
80        }
81        return "\4" . $point[0]->toBytes(true) . $point[1]->toBytes(true);
82    }
83
84    /**
85     * Create a signature
86     *
87     * @see self::verify()
88     * @access public
89     * @param string $message
90     * @return mixed
91     */
92    public function sign($message)
93    {
94        if ($this->curve instanceof MontgomeryCurve) {
95            throw new UnsupportedOperationException('Montgomery Curves cannot be used to create signatures');
96        }
97
98        $dA = $this->dA;
99        $order = $this->curve->getOrder();
100
101        $shortFormat = $this->shortFormat;
102        $format = $this->sigFormat;
103        if ($format === false) {
104            return false;
105        }
106
107        if ($this->curve instanceof TwistedEdwardsCurve) {
108            if ($this->curve instanceof Ed25519 && self::$engines['libsodium'] && !isset($this->context)) {
109                $result = sodium_crypto_sign_detached($message, $this->withPassword()->toString('libsodium'));
110                return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result) : $result;
111            }
112
113            // contexts (Ed25519ctx) are supported but prehashing (Ed25519ph) is not.
114            // quoting https://tools.ietf.org/html/rfc8032#section-8.5 ,
115            // "The Ed25519ph and Ed448ph variants ... SHOULD NOT be used"
116            $A = $this->curve->encodePoint($this->QA);
117            $curve = $this->curve;
118            $hash = new Hash($curve::HASH);
119
120            $secret = substr($hash->hash($this->dA->secret), $curve::SIZE);
121
122            if ($curve instanceof Ed25519) {
123                $dom = !isset($this->context) ? '' :
124                    'SigEd25519 no Ed25519 collisions' . "\0" . chr(strlen($this->context)) . $this->context;
125            } else {
126                $context = isset($this->context) ? $this->context : '';
127                $dom = 'SigEd448' . "\0" . chr(strlen($context)) . $context;
128            }
129            // SHA-512(dom2(F, C) || prefix || PH(M))
130            $r = $hash->hash($dom . $secret . $message);
131            $r = strrev($r);
132            $r = new BigInteger($r, 256);
133            list(, $r) = $r->divide($order);
134            $R = $curve->multiplyPoint($curve->getBasePoint(), $r);
135            $R = $curve->encodePoint($R);
136            $k = $hash->hash($dom . $R . $A . $message);
137            $k = strrev($k);
138            $k = new BigInteger($k, 256);
139            list(, $k) = $k->divide($order);
140            $S = $k->multiply($dA)->add($r);
141            list(, $S) = $S->divide($order);
142            $S = str_pad(strrev($S->toBytes()), $curve::SIZE, "\0");
143            return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $R . $S) : $R . $S;
144        }
145
146        if (self::$engines['OpenSSL'] && in_array($this->hash->getHash(), openssl_get_md_methods())) {
147            $signature = '';
148            // altho PHP's OpenSSL bindings only supported EC key creation in PHP 7.1 they've long
149            // supported signing / verification
150            // we use specified curves to avoid issues with OpenSSL possibly not supporting a given named curve;
151            // doing this may mean some curve-specific optimizations can't be used but idk if OpenSSL even
152            // has curve-specific optimizations
153            $result = openssl_sign($message, $signature, $this->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash());
154
155            if ($result) {
156                if ($shortFormat == 'ASN1') {
157                    return $signature;
158                }
159
160                extract(ASN1Signature::load($signature));
161
162                return $shortFormat == 'SSH2' ? $format::save($r, $s, $this->getCurve()) : $format::save($r, $s);
163            }
164        }
165
166        $e = $this->hash->hash($message);
167        $e = new BigInteger($e, 256);
168
169        $Ln = $this->hash->getLength() - $order->getLength();
170        $z = $Ln > 0 ? $e->bitwise_rightShift($Ln) : $e;
171
172        while (true) {
173            $k = BigInteger::randomRange(self::$one, $order->subtract(self::$one));
174            list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k);
175            $x = $x->toBigInteger();
176            list(, $r) = $x->divide($order);
177            if ($r->equals(self::$zero)) {
178                continue;
179            }
180            $kinv = $k->modInverse($order);
181            $temp = $z->add($dA->multiply($r));
182            $temp = $kinv->multiply($temp);
183            list(, $s) = $temp->divide($order);
184            if (!$s->equals(self::$zero)) {
185                break;
186            }
187        }
188
189        // the following is an RFC6979 compliant implementation of deterministic ECDSA
190        // it's unused because it's mainly intended for use when a good CSPRNG isn't
191        // available. if phpseclib's CSPRNG isn't good then even key generation is
192        // suspect
193        /*
194        // if this were actually being used it'd probably be better if this lived in load() and createKey()
195        $this->q = $this->curve->getOrder();
196        $dA = $this->dA->toBigInteger();
197        $this->x = $dA;
198
199        $h1 = $this->hash->hash($message);
200        $k = $this->computek($h1);
201        list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k);
202        $x = $x->toBigInteger();
203        list(, $r) = $x->divide($this->q);
204        $kinv = $k->modInverse($this->q);
205        $h1 = $this->bits2int($h1);
206        $temp = $h1->add($dA->multiply($r));
207        $temp = $kinv->multiply($temp);
208        list(, $s) = $temp->divide($this->q);
209        */
210
211        return $shortFormat == 'SSH2' ? $format::save($r, $s, $this->getCurve()) : $format::save($r, $s);
212    }
213
214    /**
215     * Returns the private key
216     *
217     * @param string $type
218     * @param array $options optional
219     * @return string
220     */
221    public function toString($type, array $options = [])
222    {
223        $type = self::validatePlugin('Keys', $type, 'savePrivateKey');
224
225        return $type::savePrivateKey($this->dA, $this->curve, $this->QA, $this->password, $options);
226    }
227
228    /**
229     * Returns the public key
230     *
231     * @see self::getPrivateKey()
232     * @access public
233     * @return mixed
234     */
235    public function getPublicKey()
236    {
237        $format = 'PKCS8';
238        if ($this->curve instanceof MontgomeryCurve) {
239            $format = 'MontgomeryPublic';
240        }
241
242        $type = self::validatePlugin('Keys', $format, 'savePublicKey');
243
244        $key = $type::savePublicKey($this->curve, $this->QA);
245        $key = EC::loadFormat($format, $key);
246        if ($this->curve instanceof MontgomeryCurve) {
247            return $key;
248        }
249        $key = $key
250            ->withHash($this->hash->getHash())
251            ->withSignatureFormat($this->shortFormat);
252        if ($this->curve instanceof TwistedEdwardsCurve) {
253            $key = $key->withContext($this->context);
254        }
255        return $key;
256    }
257}
258