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