xref: /dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/PublicKey.php (revision 7a48b45e8159fda5cdf0bf07c87cff9744ba1a9c)
1<?php
2
3/**
4 * RSA Public 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\RSA;
13
14use phpseclib3\Common\Functions\Strings;
15use phpseclib3\Crypt\Common;
16use phpseclib3\Crypt\Hash;
17use phpseclib3\Crypt\Random;
18use phpseclib3\Crypt\RSA;
19use phpseclib3\Crypt\RSA\Formats\Keys\PSS;
20use phpseclib3\Exception\BadConfigurationException;
21use phpseclib3\Exception\UnsupportedAlgorithmException;
22use phpseclib3\Exception\UnsupportedFormatException;
23use phpseclib3\File\ASN1;
24use phpseclib3\File\ASN1\Maps\DigestInfo;
25use phpseclib3\Math\BigInteger;
26
27/**
28 * Raw RSA Key Handler
29 *
30 * @author  Jim Wigginton <terrafrost@php.net>
31 */
32final class PublicKey extends RSA implements Common\PublicKey
33{
34    use Common\Traits\Fingerprint;
35
36    /**
37     * Exponentiate
38     *
39     * @param BigInteger $x
40     * @return BigInteger
41     */
42    private function exponentiate(BigInteger $x)
43    {
44        return $x->modPow($this->exponent, $this->modulus);
45    }
46
47    /**
48     * RSAVP1
49     *
50     * See {@link http://tools.ietf.org/html/rfc3447#section-5.2.2 RFC3447#section-5.2.2}.
51     *
52     * @param BigInteger $s
53     * @return bool|BigInteger
54     */
55    private function rsavp1($s)
56    {
57        if ($s->compare(self::$zero) < 0 || $s->compare($this->modulus) > 0) {
58            return false;
59        }
60        return $this->exponentiate($s);
61    }
62
63    /**
64     * RSASSA-PKCS1-V1_5-VERIFY
65     *
66     * See {@link http://tools.ietf.org/html/rfc3447#section-8.2.2 RFC3447#section-8.2.2}.
67     *
68     * @param string $m
69     * @param string $s
70     * @throws \LengthException if the RSA modulus is too short
71     * @return bool
72     */
73    private function rsassa_pkcs1_v1_5_verify($m, $s)
74    {
75        // Length checking
76
77        if (strlen($s) != $this->k) {
78            return false;
79        }
80
81        // RSA verification
82
83        $s = $this->os2ip($s);
84        $m2 = $this->rsavp1($s);
85        if ($m2 === false) {
86            return false;
87        }
88        $em = $this->i2osp($m2, $this->k);
89        if ($em === false) {
90            return false;
91        }
92
93        // EMSA-PKCS1-v1_5 encoding
94
95        $exception = false;
96
97        // If the encoding operation outputs "intended encoded message length too short," output "RSA modulus
98        // too short" and stop.
99        try {
100            $em2 = $this->emsa_pkcs1_v1_5_encode($m, $this->k);
101            $r1 = hash_equals($em, $em2);
102        } catch (\LengthException $e) {
103            $exception = true;
104        }
105
106        try {
107            $em3 = $this->emsa_pkcs1_v1_5_encode_without_null($m, $this->k);
108            $r2 = hash_equals($em, $em3);
109        } catch (\LengthException $e) {
110            $exception = true;
111        } catch (UnsupportedAlgorithmException $e) {
112            $r2 = false;
113        }
114
115        if ($exception) {
116            throw new \LengthException('RSA modulus too short');
117        }
118
119        // Compare
120        return boolval($r1 | $r2);
121    }
122
123    /**
124     * RSASSA-PKCS1-V1_5-VERIFY (relaxed matching)
125     *
126     * Per {@link http://tools.ietf.org/html/rfc3447#page-43 RFC3447#page-43} PKCS1 v1.5
127     * specified the use BER encoding rather than DER encoding that PKCS1 v2.0 specified.
128     * This means that under rare conditions you can have a perfectly valid v1.5 signature
129     * that fails to validate with _rsassa_pkcs1_v1_5_verify(). PKCS1 v2.1 also recommends
130     * that if you're going to validate these types of signatures you "should indicate
131     * whether the underlying BER encoding is a DER encoding and hence whether the signature
132     * is valid with respect to the specification given in [PKCS1 v2.0+]". so if you do
133     * $rsa->getLastPadding() and get RSA::PADDING_RELAXED_PKCS1 back instead of
134     * RSA::PADDING_PKCS1... that means BER encoding was used.
135     *
136     * @param string $m
137     * @param string $s
138     * @return bool
139     */
140    private function rsassa_pkcs1_v1_5_relaxed_verify($m, $s)
141    {
142        // Length checking
143
144        if (strlen($s) != $this->k) {
145            return false;
146        }
147
148        // RSA verification
149
150        $s = $this->os2ip($s);
151        $m2 = $this->rsavp1($s);
152        if ($m2 === false) {
153            return false;
154        }
155        $em = $this->i2osp($m2, $this->k);
156        if ($em === false) {
157            return false;
158        }
159
160        if (Strings::shift($em, 2) != "\0\1") {
161            return false;
162        }
163
164        $em = ltrim($em, "\xFF");
165        if (Strings::shift($em) != "\0") {
166            return false;
167        }
168
169        $decoded = ASN1::decodeBER($em);
170        if (!is_array($decoded) || empty($decoded[0]) || strlen($em) > $decoded[0]['length']) {
171            return false;
172        }
173
174        static $oids;
175        if (!isset($oids)) {
176            $oids = [
177                'md2' => '1.2.840.113549.2.2',
178                'md4' => '1.2.840.113549.2.4', // from PKCS1 v1.5
179                'md5' => '1.2.840.113549.2.5',
180                'id-sha1' => '1.3.14.3.2.26',
181                'id-sha256' => '2.16.840.1.101.3.4.2.1',
182                'id-sha384' => '2.16.840.1.101.3.4.2.2',
183                'id-sha512' => '2.16.840.1.101.3.4.2.3',
184                // from PKCS1 v2.2
185                'id-sha224' => '2.16.840.1.101.3.4.2.4',
186                'id-sha512/224' => '2.16.840.1.101.3.4.2.5',
187                'id-sha512/256' => '2.16.840.1.101.3.4.2.6',
188            ];
189            ASN1::loadOIDs($oids);
190        }
191
192        $decoded = ASN1::asn1map($decoded[0], DigestInfo::MAP);
193        if (!isset($decoded) || $decoded === false) {
194            return false;
195        }
196
197        if (!isset($oids[$decoded['digestAlgorithm']['algorithm']])) {
198            return false;
199        }
200
201        if (isset($decoded['digestAlgorithm']['parameters']) && $decoded['digestAlgorithm']['parameters'] !== ['null' => '']) {
202            return false;
203        }
204
205        $hash = $decoded['digestAlgorithm']['algorithm'];
206        $hash = substr($hash, 0, 3) == 'id-' ?
207            substr($hash, 3) :
208            $hash;
209        $hash = new Hash($hash);
210        $em = $hash->hash($m);
211        $em2 = $decoded['digest'];
212
213        return hash_equals($em, $em2);
214    }
215
216    /**
217     * EMSA-PSS-VERIFY
218     *
219     * See {@link http://tools.ietf.org/html/rfc3447#section-9.1.2 RFC3447#section-9.1.2}.
220     *
221     * @param string $m
222     * @param string $em
223     * @param int $emBits
224     * @return string
225     */
226    private function emsa_pss_verify($m, $em, $emBits)
227    {
228        // if $m is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error
229        // be output.
230
231        $emLen = ($emBits + 7) >> 3; // ie. ceil($emBits / 8);
232        $sLen = $this->sLen !== null ? $this->sLen : $this->hLen;
233
234        $mHash = $this->hash->hash($m);
235        if ($emLen < $this->hLen + $sLen + 2) {
236            return false;
237        }
238
239        if ($em[strlen($em) - 1] != chr(0xBC)) {
240            return false;
241        }
242
243        $maskedDB = substr($em, 0, -$this->hLen - 1);
244        $h = substr($em, -$this->hLen - 1, $this->hLen);
245        $temp = chr(256 - (1 << ($emBits & 7)));
246        if ((~$maskedDB[0] & $temp) != $temp) {
247            return false;
248        }
249        $dbMask = $this->mgf1($h, $emLen - $this->hLen - 1);
250        $db = $maskedDB ^ $dbMask;
251        $db[0] = ~chr(256 - (1 << ($emBits & 7))) & $db[0];
252        $temp = $emLen - $this->hLen - $sLen - 2;
253        if (substr($db, 0, $temp) != str_repeat(chr(0), $temp) || ord($db[$temp]) != 1) {
254            return false;
255        }
256        $salt = substr($db, $temp + 1); // should be $sLen long
257        $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt;
258        $h2 = $this->hash->hash($m2);
259        return hash_equals($h, $h2);
260    }
261
262    /**
263     * RSASSA-PSS-VERIFY
264     *
265     * See {@link http://tools.ietf.org/html/rfc3447#section-8.1.2 RFC3447#section-8.1.2}.
266     *
267     * @param string $m
268     * @param string $s
269     * @return bool|string
270     */
271    private function rsassa_pss_verify($m, $s)
272    {
273        // Length checking
274
275        if (strlen($s) != $this->k) {
276            return false;
277        }
278
279        // RSA verification
280
281        $modBits = strlen($this->modulus->toBits());
282
283        $s2 = $this->os2ip($s);
284        $m2 = $this->rsavp1($s2);
285        $em = $this->i2osp($m2, $this->k);
286        if ($em === false) {
287            return false;
288        }
289
290        // EMSA-PSS verification
291
292        return $this->emsa_pss_verify($m, $em, $modBits - 1);
293    }
294
295    /**
296     * Verifies a signature
297     *
298     * @see self::sign()
299     * @param string $message
300     * @param string $signature
301     * @return bool
302     */
303    public function verify($message, $signature)
304    {
305        $result = $this->handleOpenSSL('openssl_verify', $message, $signature);
306        if ($result !== null) {
307            return $result;
308        }
309
310        switch ($this->signaturePadding) {
311            case self::SIGNATURE_RELAXED_PKCS1:
312                return $this->rsassa_pkcs1_v1_5_relaxed_verify($message, $signature);
313            case self::SIGNATURE_PKCS1:
314                return $this->rsassa_pkcs1_v1_5_verify($message, $signature);
315            //case self::SIGNATURE_PSS:
316            default:
317                return $this->rsassa_pss_verify($message, $signature);
318        }
319    }
320
321    /**
322     * RSAES-PKCS1-V1_5-ENCRYPT
323     *
324     * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.1 RFC3447#section-7.2.1}.
325     *
326     * @param string $m
327     * @param bool $pkcs15_compat optional
328     * @throws \LengthException if strlen($m) > $this->k - 11
329     * @return bool|string
330     */
331    private function rsaes_pkcs1_v1_5_encrypt($m, $pkcs15_compat = false)
332    {
333        $mLen = strlen($m);
334
335        // Length checking
336
337        if ($mLen > $this->k - 11) {
338            throw new \LengthException('Message too long');
339        }
340
341        // EME-PKCS1-v1_5 encoding
342
343        $psLen = $this->k - $mLen - 3;
344        $ps = '';
345        while (strlen($ps) != $psLen) {
346            $temp = Random::string($psLen - strlen($ps));
347            $temp = str_replace("\x00", '', $temp);
348            $ps .= $temp;
349        }
350        $type = 2;
351        $em = chr(0) . chr($type) . $ps . chr(0) . $m;
352
353        // RSA encryption
354        $m = $this->os2ip($em);
355        $c = $this->rsaep($m);
356        $c = $this->i2osp($c, $this->k);
357
358        // Output the ciphertext C
359
360        return $c;
361    }
362
363    /**
364     * RSAES-OAEP-ENCRYPT
365     *
366     * See {@link http://tools.ietf.org/html/rfc3447#section-7.1.1 RFC3447#section-7.1.1} and
367     * {http://en.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding OAES}.
368     *
369     * @param string $m
370     * @throws \LengthException if strlen($m) > $this->k - 2 * $this->hLen - 2
371     * @return string
372     */
373    private function rsaes_oaep_encrypt($m)
374    {
375        $mLen = strlen($m);
376
377        // Length checking
378
379        // if $l is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error
380        // be output.
381
382        if ($mLen > $this->k - 2 * $this->hLen - 2) {
383            throw new \LengthException('Message too long');
384        }
385
386        // EME-OAEP encoding
387
388        $lHash = $this->hash->hash($this->label);
389        $ps = str_repeat(chr(0), $this->k - $mLen - 2 * $this->hLen - 2);
390        $db = $lHash . $ps . chr(1) . $m;
391        $seed = Random::string($this->hLen);
392        $dbMask = $this->mgf1($seed, $this->k - $this->hLen - 1);
393        $maskedDB = $db ^ $dbMask;
394        $seedMask = $this->mgf1($maskedDB, $this->hLen);
395        $maskedSeed = $seed ^ $seedMask;
396        $em = chr(0) . $maskedSeed . $maskedDB;
397
398        // RSA encryption
399
400        $m = $this->os2ip($em);
401        $c = $this->rsaep($m);
402        $c = $this->i2osp($c, $this->k);
403
404        // Output the ciphertext C
405
406        return $c;
407    }
408
409    /**
410     * RSAEP
411     *
412     * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.1}.
413     *
414     * @param BigInteger $m
415     * @return bool|BigInteger
416     */
417    private function rsaep($m)
418    {
419        if ($m->compare(self::$zero) < 0 || $m->compare($this->modulus) > 0) {
420            throw new \OutOfRangeException('Message representative out of range');
421        }
422        return $this->exponentiate($m);
423    }
424
425    /**
426     * Raw Encryption / Decryption
427     *
428     * Doesn't use padding and is not recommended.
429     *
430     * @param string $m
431     * @return bool|string
432     * @throws \LengthException if strlen($m) > $this->k
433     */
434    private function raw_encrypt($m)
435    {
436        if (strlen($m) > $this->k) {
437            throw new \LengthException('Message too long');
438        }
439
440        $temp = $this->os2ip($m);
441        $temp = $this->rsaep($temp);
442        return  $this->i2osp($temp, $this->k);
443    }
444
445    /**
446     * Encryption
447     *
448     * Both self::PADDING_OAEP and self::PADDING_PKCS1 both place limits on how long $plaintext can be.
449     * If $plaintext exceeds those limits it will be broken up so that it does and the resultant ciphertext's will
450     * be concatenated together.
451     *
452     * @see self::decrypt()
453     * @param string $plaintext
454     * @return bool|string
455     * @throws \LengthException if the RSA modulus is too short
456     */
457    public function encrypt($plaintext)
458    {
459        $result = $this->handleOpenSSL('openssl_public_encrypt', $plaintext);
460        if ($result !== null) {
461            return $result;
462        }
463
464        switch ($this->encryptionPadding) {
465            case self::ENCRYPTION_NONE:
466                return $this->raw_encrypt($plaintext);
467            case self::ENCRYPTION_PKCS1:
468                return $this->rsaes_pkcs1_v1_5_encrypt($plaintext);
469            //case self::ENCRYPTION_OAEP:
470            default:
471                return $this->rsaes_oaep_encrypt($plaintext);
472        }
473    }
474
475    /**
476     * Returns the public key
477     *
478     * The public key is only returned under two circumstances - if the private key had the public key embedded within it
479     * or if the public key was set via setPublicKey().  If the currently loaded key is supposed to be the public key this
480     * function won't return it since this library, for the most part, doesn't distinguish between public and private keys.
481     *
482     * @param string $type
483     * @param array $options optional
484     * @return mixed
485     */
486    public function toString($type, array $options = [])
487    {
488        $type = self::validatePlugin('Keys', $type, 'savePublicKey');
489
490        if ($type == PSS::class) {
491            if ($this->signaturePadding == self::SIGNATURE_PSS) {
492                $options += [
493                    'hash' => $this->hash->getHash(),
494                    'MGFHash' => $this->mgfHash->getHash(),
495                    'saltLength' => $this->getSaltLength()
496                ];
497            } else {
498                throw new UnsupportedFormatException('The PSS format can only be used when the signature method has been explicitly set to PSS');
499            }
500        }
501
502        return $type::savePublicKey($this->modulus, $this->publicExponent, $options);
503    }
504
505    /**
506     * Converts a public key to a private key
507     *
508     * @return RSA
509     */
510    public function asPrivateKey()
511    {
512        $new = new PrivateKey();
513        $new->exponent = $this->exponent;
514        $new->modulus = $this->modulus;
515        $new->k = $this->k;
516        $new->format = $this->format;
517        return $new
518            ->withHash($this->hash->getHash())
519            ->withMGFHash($this->mgfHash->getHash())
520            ->withSaltLength($this->sLen)
521            ->withLabel($this->label)
522            ->withPadding($this->signaturePadding | $this->encryptionPadding);
523    }
524}
525