xref: /dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/PublicKey.php (revision a896ec97b4d9a77a7ab6956f96aaa0e7987f57d1)
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        /*
306        https://datatracker.ietf.org/doc/html/rfc4055#page-6 says the following:
307
308           There are two possible encodings for the AlgorithmIdentifier
309           parameters field associated with these object identifiers.  The two
310           alternatives arise from the loss of the OPTIONAL associated with the
311           algorithm identifier parameters when the 1988 syntax for
312           AlgorithmIdentifier was translated into the 1997 syntax.  Later the
313           OPTIONAL was recovered via a defect report, but by then many people
314           thought that algorithm parameters were mandatory.  Because of this
315           history some implementations encode parameters as a NULL element
316           while others omit them entirely.  The correct encoding is to omit the
317           parameters field; however, when RSASSA-PSS and RSAES-OAEP were
318           defined, it was done using the NULL parameters rather than absent
319           parameters.
320
321           All implementations MUST accept both NULL and absent parameters as
322           legal and equivalent encodings.
323
324        OpenSSL does NOT accept both - it REQUIRES NULL be present. phpseclib, however,
325        DOES accept both. at first, it didn't. at first, not knowing why some small number
326        of PKCS1 signatures ommitted NULL, i added the SIGNATURE_RELAXED_PKCS1 mode on
327        2015-08-26. https://phpseclib.com/docs/rsa#rsasignature_relaxed_pkcs1 talks more
328        about that mode. later, on 2021-04-05, there was CVE-2021-30130. consequently,
329        the SIGNATURE_PKCS1 mode was updated to accept either NULL or non-NULL.
330
331        because phpseclib accepts PKCS1 signatures that OpenSSL doesn't, OpenSSL isn't
332        used for PKCS1. if the OpenSSL extension is installed then it'll be used to perform
333        unpadded RSA (ie. modular exponentation), however, the actual PKCS1 construction
334        takes place in PHP code vs OpenSSL.
335
336        see https://security.stackexchange.com/questions/110330/encoding-of-optional-null-in-der
337        for an additional reference
338        */
339        if ($this->signaturePadding === self::SIGNATURE_PKCS1 && isset(self::$forcedEngine) && self::$forcedEngine !== 'PHP') {
340            throw new BadConfigurationException('Engine OpenSSL is forced but unavailable for RSA PKCS1 signature verification');
341        }
342
343        $result = $this->handleOpenSSL('openssl_verify', $message, $signature);
344        if ($result !== null) {
345            return $result;
346        }
347
348        switch ($this->signaturePadding) {
349            case self::SIGNATURE_RELAXED_PKCS1:
350                return $this->rsassa_pkcs1_v1_5_relaxed_verify($message, $signature);
351            case self::SIGNATURE_PKCS1:
352                return $this->rsassa_pkcs1_v1_5_verify($message, $signature);
353            //case self::SIGNATURE_PSS:
354            default:
355                return $this->rsassa_pss_verify($message, $signature);
356        }
357    }
358
359    /**
360     * RSAES-PKCS1-V1_5-ENCRYPT
361     *
362     * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.1 RFC3447#section-7.2.1}.
363     *
364     * @param string $m
365     * @param bool $pkcs15_compat optional
366     * @throws \LengthException if strlen($m) > $this->k - 11
367     * @return bool|string
368     */
369    private function rsaes_pkcs1_v1_5_encrypt($m, $pkcs15_compat = false)
370    {
371        $mLen = strlen($m);
372
373        // Length checking
374
375        if ($mLen > $this->k - 11) {
376            throw new \LengthException('Message too long');
377        }
378
379        // EME-PKCS1-v1_5 encoding
380
381        $psLen = $this->k - $mLen - 3;
382        $ps = '';
383        while (strlen($ps) != $psLen) {
384            $temp = Random::string($psLen - strlen($ps));
385            $temp = str_replace("\x00", '', $temp);
386            $ps .= $temp;
387        }
388        $type = 2;
389        $em = chr(0) . chr($type) . $ps . chr(0) . $m;
390
391        // RSA encryption
392        $m = $this->os2ip($em);
393        $c = $this->rsaep($m);
394        $c = $this->i2osp($c, $this->k);
395
396        // Output the ciphertext C
397
398        return $c;
399    }
400
401    /**
402     * RSAES-OAEP-ENCRYPT
403     *
404     * See {@link http://tools.ietf.org/html/rfc3447#section-7.1.1 RFC3447#section-7.1.1} and
405     * {http://en.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding OAES}.
406     *
407     * @param string $m
408     * @throws \LengthException if strlen($m) > $this->k - 2 * $this->hLen - 2
409     * @return string
410     */
411    private function rsaes_oaep_encrypt($m)
412    {
413        $mLen = strlen($m);
414
415        // Length checking
416
417        // if $l is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error
418        // be output.
419
420        if ($mLen > $this->k - 2 * $this->hLen - 2) {
421            throw new \LengthException('Message too long');
422        }
423
424        // EME-OAEP encoding
425
426        $lHash = $this->hash->hash($this->label);
427        $ps = str_repeat(chr(0), $this->k - $mLen - 2 * $this->hLen - 2);
428        $db = $lHash . $ps . chr(1) . $m;
429        $seed = Random::string($this->hLen);
430        $dbMask = $this->mgf1($seed, $this->k - $this->hLen - 1);
431        $maskedDB = $db ^ $dbMask;
432        $seedMask = $this->mgf1($maskedDB, $this->hLen);
433        $maskedSeed = $seed ^ $seedMask;
434        $em = chr(0) . $maskedSeed . $maskedDB;
435
436        // RSA encryption
437
438        $m = $this->os2ip($em);
439        $c = $this->rsaep($m);
440        $c = $this->i2osp($c, $this->k);
441
442        // Output the ciphertext C
443
444        return $c;
445    }
446
447    /**
448     * RSAEP
449     *
450     * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.1}.
451     *
452     * @param BigInteger $m
453     * @return bool|BigInteger
454     */
455    private function rsaep($m)
456    {
457        if ($m->compare(self::$zero) < 0 || $m->compare($this->modulus) > 0) {
458            throw new \OutOfRangeException('Message representative out of range');
459        }
460        return $this->exponentiate($m);
461    }
462
463    /**
464     * Raw Encryption / Decryption
465     *
466     * Doesn't use padding and is not recommended.
467     *
468     * @param string $m
469     * @return bool|string
470     * @throws \LengthException if strlen($m) > $this->k
471     */
472    private function raw_encrypt($m)
473    {
474        if (strlen($m) > $this->k) {
475            throw new \LengthException('Message too long');
476        }
477
478        $temp = $this->os2ip($m);
479        $temp = $this->rsaep($temp);
480        return  $this->i2osp($temp, $this->k);
481    }
482
483    /**
484     * Encryption
485     *
486     * Both self::PADDING_OAEP and self::PADDING_PKCS1 both place limits on how long $plaintext can be.
487     * If $plaintext exceeds those limits it will be broken up so that it does and the resultant ciphertext's will
488     * be concatenated together.
489     *
490     * @see self::decrypt()
491     * @param string $plaintext
492     * @return bool|string
493     * @throws \LengthException if the RSA modulus is too short
494     */
495    public function encrypt($plaintext)
496    {
497        $result = $this->handleOpenSSL('openssl_public_encrypt', $plaintext);
498        if ($result !== null) {
499            return $result;
500        }
501
502        switch ($this->encryptionPadding) {
503            case self::ENCRYPTION_NONE:
504                return $this->raw_encrypt($plaintext);
505            case self::ENCRYPTION_PKCS1:
506                return $this->rsaes_pkcs1_v1_5_encrypt($plaintext);
507            //case self::ENCRYPTION_OAEP:
508            default:
509                return $this->rsaes_oaep_encrypt($plaintext);
510        }
511    }
512
513    /**
514     * Returns the public key
515     *
516     * The public key is only returned under two circumstances - if the private key had the public key embedded within it
517     * or if the public key was set via setPublicKey().  If the currently loaded key is supposed to be the public key this
518     * function won't return it since this library, for the most part, doesn't distinguish between public and private keys.
519     *
520     * @param string $type
521     * @param array $options optional
522     * @return mixed
523     */
524    public function toString($type, array $options = [])
525    {
526        $type = self::validatePlugin('Keys', $type, 'savePublicKey');
527
528        if ($type == PSS::class) {
529            if ($this->signaturePadding == self::SIGNATURE_PSS) {
530                $options += [
531                    'hash' => $this->hash->getHash(),
532                    'MGFHash' => $this->mgfHash->getHash(),
533                    'saltLength' => $this->getSaltLength()
534                ];
535            } else {
536                throw new UnsupportedFormatException('The PSS format can only be used when the signature method has been explicitly set to PSS');
537            }
538        }
539
540        return $type::savePublicKey($this->modulus, $this->publicExponent, $options);
541    }
542
543    /**
544     * Converts a public key to a private key
545     *
546     * @return RSA
547     */
548    public function asPrivateKey()
549    {
550        $new = new PrivateKey();
551        $new->exponent = $this->exponent;
552        $new->modulus = $this->modulus;
553        $new->k = $this->k;
554        $new->format = $this->format;
555        return $new
556            ->withHash($this->hash->getHash())
557            ->withMGFHash($this->mgfHash->getHash())
558            ->withSaltLength($this->sLen)
559            ->withLabel($this->label)
560            ->withPadding($this->signaturePadding | $this->encryptionPadding);
561    }
562}
563