1<?php
2
3/**
4 * PKCS#8 Formatted RSA-PSS Key Handler
5 *
6 * PHP version 5
7 *
8 * Used by PHP's openssl_public_encrypt() and openssl's rsautl (when -pubin is set)
9 *
10 * Processes keys with the following headers:
11 *
12 * -----BEGIN ENCRYPTED PRIVATE KEY-----
13 * -----BEGIN PRIVATE KEY-----
14 * -----BEGIN PUBLIC KEY-----
15 *
16 * Analogous to "openssl genpkey -algorithm rsa-pss".
17 *
18 * @category  Crypt
19 * @package   RSA
20 * @author    Jim Wigginton <terrafrost@php.net>
21 * @copyright 2015 Jim Wigginton
22 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
23 * @link      http://phpseclib.sourceforge.net
24 */
25
26namespace phpseclib3\Crypt\RSA\Formats\Keys;
27
28use phpseclib3\Common\Functions\Strings;
29use phpseclib3\Crypt\Common\Formats\Keys\PKCS8 as Progenitor;
30use phpseclib3\File\ASN1;
31use phpseclib3\File\ASN1\Maps;
32use phpseclib3\Math\BigInteger;
33
34/**
35 * PKCS#8 Formatted RSA-PSS Key Handler
36 *
37 * @package RSA
38 * @author  Jim Wigginton <terrafrost@php.net>
39 * @access  public
40 */
41abstract class PSS extends Progenitor
42{
43    /**
44     * OID Name
45     *
46     * @var string
47     * @access private
48     */
49    const OID_NAME = 'id-RSASSA-PSS';
50
51    /**
52     * OID Value
53     *
54     * @var string
55     * @access private
56     */
57    const OID_VALUE = '1.2.840.113549.1.1.10';
58
59    /**
60     * OIDs loaded
61     *
62     * @var bool
63     * @access private
64     */
65    private static $oidsLoaded = false;
66
67    /**
68     * Child OIDs loaded
69     *
70     * @var bool
71     * @access private
72     */
73    protected static $childOIDsLoaded = false;
74
75    /**
76     * Initialize static variables
77     */
78    private static function initialize_static_variables()
79    {
80        if (!self::$oidsLoaded) {
81            ASN1::loadOIDs([
82                'md2' => '1.2.840.113549.2.2',
83                'md4' => '1.2.840.113549.2.4',
84                'md5' => '1.2.840.113549.2.5',
85                'id-sha1' => '1.3.14.3.2.26',
86                'id-sha256' => '2.16.840.1.101.3.4.2.1',
87                'id-sha384' => '2.16.840.1.101.3.4.2.2',
88                'id-sha512' => '2.16.840.1.101.3.4.2.3',
89                'id-sha224' => '2.16.840.1.101.3.4.2.4',
90                'id-sha512/224' => '2.16.840.1.101.3.4.2.5',
91                'id-sha512/256' => '2.16.840.1.101.3.4.2.6',
92
93                'id-mgf1' => '1.2.840.113549.1.1.8'
94            ]);
95            self::$oidsLoaded = true;
96        }
97    }
98
99    /**
100     * Break a public or private key down into its constituent components
101     *
102     * @access public
103     * @param string $key
104     * @param string $password optional
105     * @return array
106     */
107    public static function load($key, $password = '')
108    {
109        self::initialize_static_variables();
110
111        if (!Strings::is_stringable($key)) {
112            throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
113        }
114
115        $components = ['isPublicKey' => strpos($key, 'PUBLIC') !== false];
116
117        $key = parent::load($key, $password);
118
119        $type = isset($key['privateKey']) ? 'private' : 'public';
120
121        $result = $components + PKCS1::load($key[$type . 'Key']);
122
123        if (isset($key[$type . 'KeyAlgorithm']['parameters'])) {
124            $decoded = ASN1::decodeBER($key[$type . 'KeyAlgorithm']['parameters']);
125            if ($decoded === false) {
126                throw new \UnexpectedValueException('Unable to decode parameters');
127            }
128            $params = ASN1::asn1map($decoded[0], Maps\RSASSA_PSS_params::MAP);
129        } else {
130            $params = [];
131        }
132
133        if (isset($params['maskGenAlgorithm']['parameters'])) {
134            $decoded = ASN1::decodeBER($params['maskGenAlgorithm']['parameters']);
135            if ($decoded === false) {
136                throw new \UnexpectedValueException('Unable to decode parameters');
137            }
138            $params['maskGenAlgorithm']['parameters'] = ASN1::asn1map($decoded[0], Maps\HashAlgorithm::MAP);
139        } else {
140            $params['maskGenAlgorithm'] = [
141                'algorithm' => 'id-mgf1',
142                'parameters' => ['algorithm' => 'id-sha1']
143            ];
144        }
145
146        if (!isset($params['hashAlgorithm']['algorithm'])) {
147            $params['hashAlgorithm']['algorithm'] = 'id-sha1';
148        }
149
150        $result['hash'] = str_replace('id-', '', $params['hashAlgorithm']['algorithm']);
151        $result['MGFHash'] = str_replace('id-', '', $params['maskGenAlgorithm']['parameters']['algorithm']);
152        if (isset($params['saltLength'])) {
153            $result['saltLength'] = (int) $params['saltLength']->toString();
154        }
155
156        if (isset($key['meta'])) {
157            $result['meta'] = $key['meta'];
158        }
159
160        return $result;
161    }
162
163    /**
164     * Convert a private key to the appropriate format.
165     *
166     * @access public
167     * @param \phpseclib3\Math\BigInteger $n
168     * @param \phpseclib3\Math\BigInteger $e
169     * @param \phpseclib3\Math\BigInteger $d
170     * @param array $primes
171     * @param array $exponents
172     * @param array $coefficients
173     * @param string $password optional
174     * @param array $options optional
175     * @return string
176     */
177    public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = [])
178    {
179        self::initialize_static_variables();
180
181        $key = PKCS1::savePrivateKey($n, $e, $d, $primes, $exponents, $coefficients);
182        $key = ASN1::extractBER($key);
183        $params = self::savePSSParams($options);
184        return self::wrapPrivateKey($key, [], $params, $password, null, '', $options);
185    }
186
187    /**
188     * Convert a public key to the appropriate format
189     *
190     * @access public
191     * @param \phpseclib3\Math\BigInteger $n
192     * @param \phpseclib3\Math\BigInteger $e
193     * @param array $options optional
194     * @return string
195     */
196    public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = [])
197    {
198        self::initialize_static_variables();
199
200        $key = PKCS1::savePublicKey($n, $e);
201        $key = ASN1::extractBER($key);
202        $params = self::savePSSParams($options);
203        return self::wrapPublicKey($key, $params);
204    }
205
206    /**
207     * Encodes PSS parameters
208     *
209     * @access public
210     * @param array $options
211     * @return string
212     */
213    public static function savePSSParams(array $options)
214    {
215        /*
216         The trailerField field is an integer.  It provides
217         compatibility with IEEE Std 1363a-2004 [P1363A].  The value
218         MUST be 1, which represents the trailer field with hexadecimal
219         value 0xBC.  Other trailer fields, including the trailer field
220         composed of HashID concatenated with 0xCC that is specified in
221         IEEE Std 1363a, are not supported.  Implementations that
222         perform signature generation MUST omit the trailerField field,
223         indicating that the default trailer field value was used.
224         Implementations that perform signature validation MUST
225         recognize both a present trailerField field with value 1 and an
226         absent trailerField field.
227
228         source: https://tools.ietf.org/html/rfc4055#page-9
229        */
230        $params = [
231            'trailerField' => new BigInteger(1)
232        ];
233        if (isset($options['hash'])) {
234            $params['hashAlgorithm']['algorithm'] = 'id-' . $options['hash'];
235        }
236        if (isset($options['MGFHash'])) {
237            $temp = ['algorithm' => 'id-' . $options['MGFHash']];
238            $temp = ASN1::encodeDER($temp, Maps\HashAlgorithm::MAP);
239            $params['maskGenAlgorithm'] = [
240                'algorithm' => 'id-mgf1',
241                'parameters' => new ASN1\Element($temp)
242            ];
243        }
244        if (isset($options['saltLength'])) {
245            $params['saltLength'] = new BigInteger($options['saltLength']);
246        }
247
248        return new ASN1\Element(ASN1::encodeDER($params, Maps\RSASSA_PSS_params::MAP));
249    }
250}
251