Subversion Repositories oidplus

Rev

Rev 1308 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
827 daniel-mar 1
<?php
2
 
3
/**
4
 * Pure-PHP X.509 Parser
5
 *
6
 * PHP version 5
7
 *
8
 * Encode and decode X.509 certificates.
9
 *
10
 * The extensions are from {@link http://tools.ietf.org/html/rfc5280 RFC5280} and
11
 * {@link http://web.archive.org/web/19961027104704/http://www3.netscape.com/eng/security/cert-exts.html Netscape Certificate Extensions}.
12
 *
13
 * Note that loading an X.509 certificate and resaving it may invalidate the signature.  The reason being that the signature is based on a
14
 * portion of the certificate that contains optional parameters with default values.  ie. if the parameter isn't there the default value is
15
 * used.  Problem is, if the parameter is there and it just so happens to have the default value there are two ways that that parameter can
16
 * be encoded.  It can be encoded explicitly or left out all together.  This would effect the signature value and thus may invalidate the
17
 * the certificate all together unless the certificate is re-signed.
18
 *
19
 * @author    Jim Wigginton <terrafrost@php.net>
20
 * @copyright 2012 Jim Wigginton
21
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
22
 * @link      http://phpseclib.sourceforge.net
23
 */
24
 
25
namespace phpseclib3\File;
26
 
1042 daniel-mar 27
use phpseclib3\Common\Functions\Strings;
827 daniel-mar 28
use phpseclib3\Crypt\Common\PrivateKey;
29
use phpseclib3\Crypt\Common\PublicKey;
30
use phpseclib3\Crypt\DSA;
31
use phpseclib3\Crypt\EC;
32
use phpseclib3\Crypt\Hash;
33
use phpseclib3\Crypt\PublicKeyLoader;
34
use phpseclib3\Crypt\Random;
35
use phpseclib3\Crypt\RSA;
36
use phpseclib3\Crypt\RSA\Formats\Keys\PSS;
37
use phpseclib3\Exception\UnsupportedAlgorithmException;
38
use phpseclib3\File\ASN1\Element;
39
use phpseclib3\File\ASN1\Maps;
40
use phpseclib3\Math\BigInteger;
41
 
42
/**
43
 * Pure-PHP X.509 Parser
44
 *
45
 * @author  Jim Wigginton <terrafrost@php.net>
46
 */
47
class X509
48
{
49
    /**
50
     * Flag to only accept signatures signed by certificate authorities
51
     *
52
     * Not really used anymore but retained all the same to suppress E_NOTICEs from old installs
53
     *
54
     */
55
    const VALIDATE_SIGNATURE_BY_CA = 1;
56
 
57
    /**
58
     * Return internal array representation
59
     *
60
     * @see \phpseclib3\File\X509::getDN()
61
     */
62
    const DN_ARRAY = 0;
63
    /**
64
     * Return string
65
     *
66
     * @see \phpseclib3\File\X509::getDN()
67
     */
68
    const DN_STRING = 1;
69
    /**
70
     * Return ASN.1 name string
71
     *
72
     * @see \phpseclib3\File\X509::getDN()
73
     */
74
    const DN_ASN1 = 2;
75
    /**
76
     * Return OpenSSL compatible array
77
     *
78
     * @see \phpseclib3\File\X509::getDN()
79
     */
80
    const DN_OPENSSL = 3;
81
    /**
82
     * Return canonical ASN.1 RDNs string
83
     *
84
     * @see \phpseclib3\File\X509::getDN()
85
     */
86
    const DN_CANON = 4;
87
    /**
88
     * Return name hash for file indexing
89
     *
90
     * @see \phpseclib3\File\X509::getDN()
91
     */
92
    const DN_HASH = 5;
93
 
94
    /**
95
     * Save as PEM
96
     *
97
     * ie. a base64-encoded PEM with a header and a footer
98
     *
99
     * @see \phpseclib3\File\X509::saveX509()
100
     * @see \phpseclib3\File\X509::saveCSR()
101
     * @see \phpseclib3\File\X509::saveCRL()
102
     */
103
    const FORMAT_PEM = 0;
104
    /**
105
     * Save as DER
106
     *
107
     * @see \phpseclib3\File\X509::saveX509()
108
     * @see \phpseclib3\File\X509::saveCSR()
109
     * @see \phpseclib3\File\X509::saveCRL()
110
     */
111
    const FORMAT_DER = 1;
112
    /**
113
     * Save as a SPKAC
114
     *
115
     * @see \phpseclib3\File\X509::saveX509()
116
     * @see \phpseclib3\File\X509::saveCSR()
117
     * @see \phpseclib3\File\X509::saveCRL()
118
     *
119
     * Only works on CSRs. Not currently supported.
120
     */
121
    const FORMAT_SPKAC = 2;
122
    /**
123
     * Auto-detect the format
124
     *
125
     * Used only by the load*() functions
126
     *
127
     * @see \phpseclib3\File\X509::saveX509()
128
     * @see \phpseclib3\File\X509::saveCSR()
129
     * @see \phpseclib3\File\X509::saveCRL()
130
     */
131
    const FORMAT_AUTO_DETECT = 3;
132
 
133
    /**
134
     * Attribute value disposition.
135
     * If disposition is >= 0, this is the index of the target value.
136
     */
137
    const ATTR_ALL = -1; // All attribute values (array).
138
    const ATTR_APPEND = -2; // Add a value.
139
    const ATTR_REPLACE = -3; // Clear first, then add a value.
140
 
141
    /**
142
     * Distinguished Name
143
     *
144
     * @var array
145
     */
146
    private $dn;
147
 
148
    /**
149
     * Public key
150
     *
151
     * @var string|PublicKey
152
     */
153
    private $publicKey;
154
 
155
    /**
156
     * Private key
157
     *
158
     * @var string|PrivateKey
159
     */
160
    private $privateKey;
161
 
162
    /**
163
     * The certificate authorities
164
     *
165
     * @var array
166
     */
1308 daniel-mar 167
    private $CAs = [];
827 daniel-mar 168
 
169
    /**
170
     * The currently loaded certificate
171
     *
172
     * @var array
173
     */
174
    private $currentCert;
175
 
176
    /**
177
     * The signature subject
178
     *
179
     * There's no guarantee \phpseclib3\File\X509 is going to re-encode an X.509 cert in the same way it was originally
180
     * encoded so we take save the portion of the original cert that the signature would have made for.
181
     *
182
     * @var string
183
     */
184
    private $signatureSubject;
185
 
186
    /**
187
     * Certificate Start Date
188
     *
189
     * @var string
190
     */
191
    private $startDate;
192
 
193
    /**
194
     * Certificate End Date
195
     *
196
     * @var string|Element
197
     */
198
    private $endDate;
199
 
200
    /**
201
     * Serial Number
202
     *
203
     * @var string
204
     */
205
    private $serialNumber;
206
 
207
    /**
208
     * Key Identifier
209
     *
210
     * See {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.1 RFC5280#section-4.2.1.1} and
211
     * {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.2 RFC5280#section-4.2.1.2}.
212
     *
213
     * @var string
214
     */
215
    private $currentKeyIdentifier;
216
 
217
    /**
218
     * CA Flag
219
     *
220
     * @var bool
221
     */
222
    private $caFlag = false;
223
 
224
    /**
225
     * SPKAC Challenge
226
     *
227
     * @var string
228
     */
229
    private $challenge;
230
 
231
    /**
232
     * @var array
233
     */
234
    private $extensionValues = [];
235
 
236
    /**
237
     * OIDs loaded
238
     *
239
     * @var bool
240
     */
241
    private static $oidsLoaded = false;
242
 
243
    /**
244
     * Recursion Limit
245
     *
246
     * @var int
247
     */
248
    private static $recur_limit = 5;
249
 
250
    /**
251
     * URL fetch flag
252
     *
253
     * @var bool
254
     */
255
    private static $disable_url_fetch = false;
256
 
257
    /**
258
     * @var array
259
     */
260
    private static $extensions = [];
261
 
262
    /**
263
     * @var ?array
264
     */
265
    private $ipAddresses = null;
266
 
267
    /**
268
     * @var ?array
269
     */
270
    private $domains = null;
271
 
272
    /**
273
     * Default Constructor.
274
     *
275
     * @return \phpseclib3\File\X509
276
     */
277
    public function __construct()
278
    {
279
        // Explicitly Tagged Module, 1988 Syntax
280
        // http://tools.ietf.org/html/rfc5280#appendix-A.1
281
 
282
        if (!self::$oidsLoaded) {
283
            // OIDs from RFC5280 and those RFCs mentioned in RFC5280#section-4.1.1.2
284
            ASN1::loadOIDs([
285
                //'id-pkix' => '1.3.6.1.5.5.7',
286
                //'id-pe' => '1.3.6.1.5.5.7.1',
287
                //'id-qt' => '1.3.6.1.5.5.7.2',
288
                //'id-kp' => '1.3.6.1.5.5.7.3',
289
                //'id-ad' => '1.3.6.1.5.5.7.48',
290
                'id-qt-cps' => '1.3.6.1.5.5.7.2.1',
291
                'id-qt-unotice' => '1.3.6.1.5.5.7.2.2',
292
                'id-ad-ocsp' => '1.3.6.1.5.5.7.48.1',
293
                'id-ad-caIssuers' => '1.3.6.1.5.5.7.48.2',
294
                'id-ad-timeStamping' => '1.3.6.1.5.5.7.48.3',
295
                'id-ad-caRepository' => '1.3.6.1.5.5.7.48.5',
296
                //'id-at' => '2.5.4',
297
                'id-at-name' => '2.5.4.41',
298
                'id-at-surname' => '2.5.4.4',
299
                'id-at-givenName' => '2.5.4.42',
300
                'id-at-initials' => '2.5.4.43',
301
                'id-at-generationQualifier' => '2.5.4.44',
302
                'id-at-commonName' => '2.5.4.3',
303
                'id-at-localityName' => '2.5.4.7',
304
                'id-at-stateOrProvinceName' => '2.5.4.8',
305
                'id-at-organizationName' => '2.5.4.10',
306
                'id-at-organizationalUnitName' => '2.5.4.11',
307
                'id-at-title' => '2.5.4.12',
308
                'id-at-description' => '2.5.4.13',
309
                'id-at-dnQualifier' => '2.5.4.46',
310
                'id-at-countryName' => '2.5.4.6',
311
                'id-at-serialNumber' => '2.5.4.5',
312
                'id-at-pseudonym' => '2.5.4.65',
313
                'id-at-postalCode' => '2.5.4.17',
314
                'id-at-streetAddress' => '2.5.4.9',
315
                'id-at-uniqueIdentifier' => '2.5.4.45',
316
                'id-at-role' => '2.5.4.72',
317
                'id-at-postalAddress' => '2.5.4.16',
1308 daniel-mar 318
                'jurisdictionOfIncorporationCountryName' => '1.3.6.1.4.1.311.60.2.1.3',
319
                'jurisdictionOfIncorporationStateOrProvinceName' => '1.3.6.1.4.1.311.60.2.1.2',
320
                'jurisdictionLocalityName' => '1.3.6.1.4.1.311.60.2.1.1',
321
                'id-at-businessCategory' => '2.5.4.15',
827 daniel-mar 322
 
323
                //'id-domainComponent' => '0.9.2342.19200300.100.1.25',
324
                //'pkcs-9' => '1.2.840.113549.1.9',
325
                'pkcs-9-at-emailAddress' => '1.2.840.113549.1.9.1',
326
                //'id-ce' => '2.5.29',
327
                'id-ce-authorityKeyIdentifier' => '2.5.29.35',
328
                'id-ce-subjectKeyIdentifier' => '2.5.29.14',
329
                'id-ce-keyUsage' => '2.5.29.15',
330
                'id-ce-privateKeyUsagePeriod' => '2.5.29.16',
331
                'id-ce-certificatePolicies' => '2.5.29.32',
332
                //'anyPolicy' => '2.5.29.32.0',
333
 
334
                'id-ce-policyMappings' => '2.5.29.33',
335
 
336
                'id-ce-subjectAltName' => '2.5.29.17',
337
                'id-ce-issuerAltName' => '2.5.29.18',
338
                'id-ce-subjectDirectoryAttributes' => '2.5.29.9',
339
                'id-ce-basicConstraints' => '2.5.29.19',
340
                'id-ce-nameConstraints' => '2.5.29.30',
341
                'id-ce-policyConstraints' => '2.5.29.36',
342
                'id-ce-cRLDistributionPoints' => '2.5.29.31',
343
                'id-ce-extKeyUsage' => '2.5.29.37',
344
                //'anyExtendedKeyUsage' => '2.5.29.37.0',
345
                'id-kp-serverAuth' => '1.3.6.1.5.5.7.3.1',
346
                'id-kp-clientAuth' => '1.3.6.1.5.5.7.3.2',
347
                'id-kp-codeSigning' => '1.3.6.1.5.5.7.3.3',
348
                'id-kp-emailProtection' => '1.3.6.1.5.5.7.3.4',
349
                'id-kp-timeStamping' => '1.3.6.1.5.5.7.3.8',
350
                'id-kp-OCSPSigning' => '1.3.6.1.5.5.7.3.9',
351
                'id-ce-inhibitAnyPolicy' => '2.5.29.54',
352
                'id-ce-freshestCRL' => '2.5.29.46',
353
                'id-pe-authorityInfoAccess' => '1.3.6.1.5.5.7.1.1',
354
                'id-pe-subjectInfoAccess' => '1.3.6.1.5.5.7.1.11',
355
                'id-ce-cRLNumber' => '2.5.29.20',
356
                'id-ce-issuingDistributionPoint' => '2.5.29.28',
357
                'id-ce-deltaCRLIndicator' => '2.5.29.27',
358
                'id-ce-cRLReasons' => '2.5.29.21',
359
                'id-ce-certificateIssuer' => '2.5.29.29',
360
                'id-ce-holdInstructionCode' => '2.5.29.23',
361
                //'holdInstruction' => '1.2.840.10040.2',
362
                'id-holdinstruction-none' => '1.2.840.10040.2.1',
363
                'id-holdinstruction-callissuer' => '1.2.840.10040.2.2',
364
                'id-holdinstruction-reject' => '1.2.840.10040.2.3',
365
                'id-ce-invalidityDate' => '2.5.29.24',
366
 
367
                'rsaEncryption' => '1.2.840.113549.1.1.1',
368
                'md2WithRSAEncryption' => '1.2.840.113549.1.1.2',
369
                'md5WithRSAEncryption' => '1.2.840.113549.1.1.4',
370
                'sha1WithRSAEncryption' => '1.2.840.113549.1.1.5',
371
                'sha224WithRSAEncryption' => '1.2.840.113549.1.1.14',
372
                'sha256WithRSAEncryption' => '1.2.840.113549.1.1.11',
373
                'sha384WithRSAEncryption' => '1.2.840.113549.1.1.12',
374
                'sha512WithRSAEncryption' => '1.2.840.113549.1.1.13',
375
 
376
                'id-ecPublicKey' => '1.2.840.10045.2.1',
377
                'ecdsa-with-SHA1' => '1.2.840.10045.4.1',
378
                // from https://tools.ietf.org/html/rfc5758#section-3.2
379
                'ecdsa-with-SHA224' => '1.2.840.10045.4.3.1',
380
                'ecdsa-with-SHA256' => '1.2.840.10045.4.3.2',
381
                'ecdsa-with-SHA384' => '1.2.840.10045.4.3.3',
382
                'ecdsa-with-SHA512' => '1.2.840.10045.4.3.4',
383
 
384
                'id-dsa' => '1.2.840.10040.4.1',
385
                'id-dsa-with-sha1' => '1.2.840.10040.4.3',
386
                // from https://tools.ietf.org/html/rfc5758#section-3.1
387
                'id-dsa-with-sha224' => '2.16.840.1.101.3.4.3.1',
388
                'id-dsa-with-sha256' => '2.16.840.1.101.3.4.3.2',
389
 
390
                // from https://tools.ietf.org/html/rfc8410:
391
                'id-Ed25519' => '1.3.101.112',
392
                'id-Ed448' => '1.3.101.113',
393
 
394
                'id-RSASSA-PSS' => '1.2.840.113549.1.1.10',
395
 
396
                //'id-sha224' => '2.16.840.1.101.3.4.2.4',
397
                //'id-sha256' => '2.16.840.1.101.3.4.2.1',
398
                //'id-sha384' => '2.16.840.1.101.3.4.2.2',
399
                //'id-sha512' => '2.16.840.1.101.3.4.2.3',
400
                //'id-GostR3411-94-with-GostR3410-94' => '1.2.643.2.2.4',
401
                //'id-GostR3411-94-with-GostR3410-2001' => '1.2.643.2.2.3',
402
                //'id-GostR3410-2001' => '1.2.643.2.2.20',
403
                //'id-GostR3410-94' => '1.2.643.2.2.19',
404
                // Netscape Object Identifiers from "Netscape Certificate Extensions"
405
                'netscape' => '2.16.840.1.113730',
406
                'netscape-cert-extension' => '2.16.840.1.113730.1',
407
                'netscape-cert-type' => '2.16.840.1.113730.1.1',
408
                'netscape-comment' => '2.16.840.1.113730.1.13',
409
                'netscape-ca-policy-url' => '2.16.840.1.113730.1.8',
410
                // the following are X.509 extensions not supported by phpseclib
411
                'id-pe-logotype' => '1.3.6.1.5.5.7.1.12',
412
                'entrustVersInfo' => '1.2.840.113533.7.65.0',
413
                'verisignPrivate' => '2.16.840.1.113733.1.6.9',
414
                // for Certificate Signing Requests
415
                // see http://tools.ietf.org/html/rfc2985
416
                'pkcs-9-at-unstructuredName' => '1.2.840.113549.1.9.2', // PKCS #9 unstructured name
417
                'pkcs-9-at-challengePassword' => '1.2.840.113549.1.9.7', // Challenge password for certificate revocations
418
                'pkcs-9-at-extensionRequest' => '1.2.840.113549.1.9.14' // Certificate extension request
419
            ]);
420
        }
421
    }
422
 
423
    /**
424
     * Load X.509 certificate
425
     *
426
     * Returns an associative array describing the X.509 cert or a false if the cert failed to load
427
     *
1042 daniel-mar 428
     * @param array|string $cert
827 daniel-mar 429
     * @param int $mode
430
     * @return mixed
431
     */
432
    public function loadX509($cert, $mode = self::FORMAT_AUTO_DETECT)
433
    {
434
        if (is_array($cert) && isset($cert['tbsCertificate'])) {
435
            unset($this->currentCert);
436
            unset($this->currentKeyIdentifier);
437
            $this->dn = $cert['tbsCertificate']['subject'];
438
            if (!isset($this->dn)) {
439
                return false;
440
            }
441
            $this->currentCert = $cert;
442
 
443
            $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier');
444
            $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null;
445
 
446
            unset($this->signatureSubject);
447
 
448
            return $cert;
449
        }
450
 
451
        if ($mode != self::FORMAT_DER) {
452
            $newcert = ASN1::extractBER($cert);
453
            if ($mode == self::FORMAT_PEM && $cert == $newcert) {
454
                return false;
455
            }
456
            $cert = $newcert;
457
        }
458
 
459
        if ($cert === false) {
460
            $this->currentCert = false;
461
            return false;
462
        }
463
 
464
        $decoded = ASN1::decodeBER($cert);
465
 
1042 daniel-mar 466
        if ($decoded) {
827 daniel-mar 467
            $x509 = ASN1::asn1map($decoded[0], Maps\Certificate::MAP);
468
        }
469
        if (!isset($x509) || $x509 === false) {
470
            $this->currentCert = false;
471
            return false;
472
        }
473
 
474
        $this->signatureSubject = substr($cert, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']);
475
 
476
        if ($this->isSubArrayValid($x509, 'tbsCertificate/extensions')) {
477
            $this->mapInExtensions($x509, 'tbsCertificate/extensions');
478
        }
479
        $this->mapInDNs($x509, 'tbsCertificate/issuer/rdnSequence');
480
        $this->mapInDNs($x509, 'tbsCertificate/subject/rdnSequence');
481
 
482
        $key = $x509['tbsCertificate']['subjectPublicKeyInfo'];
483
        $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP);
484
        $x509['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] =
485
            "-----BEGIN PUBLIC KEY-----\r\n" .
486
            chunk_split(base64_encode($key), 64) .
487
            "-----END PUBLIC KEY-----";
488
 
489
        $this->currentCert = $x509;
490
        $this->dn = $x509['tbsCertificate']['subject'];
491
 
492
        $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier');
493
        $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null;
494
 
495
        return $x509;
496
    }
497
 
498
    /**
499
     * Save X.509 certificate
500
     *
501
     * @param array $cert
502
     * @param int $format optional
503
     * @return string
504
     */
1042 daniel-mar 505
    public function saveX509(array $cert, $format = self::FORMAT_PEM)
827 daniel-mar 506
    {
507
        if (!is_array($cert) || !isset($cert['tbsCertificate'])) {
508
            return false;
509
        }
510
 
511
        switch (true) {
512
            // "case !$a: case !$b: break; default: whatever();" is the same thing as "if ($a && $b) whatever()"
513
            case !($algorithm = $this->subArray($cert, 'tbsCertificate/subjectPublicKeyInfo/algorithm/algorithm')):
514
            case is_object($cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']):
515
                break;
516
            default:
517
                $cert['tbsCertificate']['subjectPublicKeyInfo'] = new Element(
518
                    base64_decode(preg_replace('#-.+-|[\r\n]#', '', $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']))
519
                );
520
        }
521
 
522
        if ($algorithm == 'rsaEncryption') {
523
            $cert['signatureAlgorithm']['parameters'] = null;
524
            $cert['tbsCertificate']['signature']['parameters'] = null;
525
        }
526
 
527
        $filters = [];
528
        $type_utf8_string = ['type' => ASN1::TYPE_UTF8_STRING];
529
        $filters['tbsCertificate']['signature']['parameters'] = $type_utf8_string;
530
        $filters['tbsCertificate']['signature']['issuer']['rdnSequence']['value'] = $type_utf8_string;
531
        $filters['tbsCertificate']['issuer']['rdnSequence']['value'] = $type_utf8_string;
532
        $filters['tbsCertificate']['subject']['rdnSequence']['value'] = $type_utf8_string;
533
        $filters['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['parameters'] = $type_utf8_string;
534
        $filters['signatureAlgorithm']['parameters'] = $type_utf8_string;
535
        $filters['authorityCertIssuer']['directoryName']['rdnSequence']['value'] = $type_utf8_string;
536
        //$filters['policyQualifiers']['qualifier'] = $type_utf8_string;
537
        $filters['distributionPoint']['fullName']['directoryName']['rdnSequence']['value'] = $type_utf8_string;
538
        $filters['directoryName']['rdnSequence']['value'] = $type_utf8_string;
539
 
540
        foreach (self::$extensions as $extension) {
541
            $filters['tbsCertificate']['extensions'][] = $extension;
542
        }
543
 
544
        /* in the case of policyQualifiers/qualifier, the type has to be \phpseclib3\File\ASN1::TYPE_IA5_STRING.
545
           \phpseclib3\File\ASN1::TYPE_PRINTABLE_STRING will cause OpenSSL's X.509 parser to spit out random
546
           characters.
547
         */
548
        $filters['policyQualifiers']['qualifier']
549
            = ['type' => ASN1::TYPE_IA5_STRING];
550
 
551
        ASN1::setFilters($filters);
552
 
553
        $this->mapOutExtensions($cert, 'tbsCertificate/extensions');
554
        $this->mapOutDNs($cert, 'tbsCertificate/issuer/rdnSequence');
555
        $this->mapOutDNs($cert, 'tbsCertificate/subject/rdnSequence');
556
 
557
        $cert = ASN1::encodeDER($cert, Maps\Certificate::MAP);
558
 
559
        switch ($format) {
560
            case self::FORMAT_DER:
561
                return $cert;
562
            // case self::FORMAT_PEM:
563
            default:
1042 daniel-mar 564
                return "-----BEGIN CERTIFICATE-----\r\n" . chunk_split(Strings::base64_encode($cert), 64) . '-----END CERTIFICATE-----';
827 daniel-mar 565
        }
566
    }
567
 
568
    /**
569
     * Map extension values from octet string to extension-specific internal
570
     *   format.
571
     *
572
     * @param array $root (by reference)
573
     * @param string $path
574
     */
1042 daniel-mar 575
    private function mapInExtensions(array &$root, $path)
827 daniel-mar 576
    {
577
        $extensions = &$this->subArrayUnchecked($root, $path);
578
 
579
        if ($extensions) {
580
            for ($i = 0; $i < count($extensions); $i++) {
581
                $id = $extensions[$i]['extnId'];
582
                $value = &$extensions[$i]['extnValue'];
583
                /* [extnValue] contains the DER encoding of an ASN.1 value
584
                   corresponding to the extension type identified by extnID */
585
                $map = $this->getMapping($id);
586
                if (!is_bool($map)) {
587
                    $decoder = $id == 'id-ce-nameConstraints' ?
588
                        [static::class, 'decodeNameConstraintIP'] :
589
                        [static::class, 'decodeIP'];
590
                    $decoded = ASN1::decodeBER($value);
1042 daniel-mar 591
                    if (!$decoded) {
592
                        continue;
593
                    }
827 daniel-mar 594
                    $mapped = ASN1::asn1map($decoded[0], $map, ['iPAddress' => $decoder]);
595
                    $value = $mapped === false ? $decoded[0] : $mapped;
596
 
597
                    if ($id == 'id-ce-certificatePolicies') {
598
                        for ($j = 0; $j < count($value); $j++) {
599
                            if (!isset($value[$j]['policyQualifiers'])) {
600
                                continue;
601
                            }
602
                            for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) {
603
                                $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId'];
604
                                $map = $this->getMapping($subid);
605
                                $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier'];
606
                                if ($map !== false) {
607
                                    $decoded = ASN1::decodeBER($subvalue);
1042 daniel-mar 608
                                    if (!$decoded) {
609
                                        continue;
610
                                    }
827 daniel-mar 611
                                    $mapped = ASN1::asn1map($decoded[0], $map);
612
                                    $subvalue = $mapped === false ? $decoded[0] : $mapped;
613
                                }
614
                            }
615
                        }
616
                    }
617
                }
618
            }
619
        }
620
    }
621
 
622
    /**
623
     * Map extension values from extension-specific internal format to
624
     *   octet string.
625
     *
626
     * @param array $root (by reference)
627
     * @param string $path
628
     */
1042 daniel-mar 629
    private function mapOutExtensions(array &$root, $path)
827 daniel-mar 630
    {
631
        $extensions = &$this->subArray($root, $path, !empty($this->extensionValues));
632
 
633
        foreach ($this->extensionValues as $id => $data) {
634
            extract($data);
635
            $newext = [
636
                'extnId' => $id,
637
                'extnValue' => $value,
638
                'critical' => $critical
639
            ];
640
            if ($replace) {
641
                foreach ($extensions as $key => $value) {
642
                    if ($value['extnId'] == $id) {
643
                        $extensions[$key] = $newext;
644
                        continue 2;
645
                    }
646
                }
647
            }
648
            $extensions[] = $newext;
649
        }
650
 
651
        if (is_array($extensions)) {
652
            $size = count($extensions);
653
            for ($i = 0; $i < $size; $i++) {
654
                if ($extensions[$i] instanceof Element) {
655
                    continue;
656
                }
657
 
658
                $id = $extensions[$i]['extnId'];
659
                $value = &$extensions[$i]['extnValue'];
660
 
661
                switch ($id) {
662
                    case 'id-ce-certificatePolicies':
663
                        for ($j = 0; $j < count($value); $j++) {
664
                            if (!isset($value[$j]['policyQualifiers'])) {
665
                                continue;
666
                            }
667
                            for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) {
668
                                $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId'];
669
                                $map = $this->getMapping($subid);
670
                                $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier'];
671
                                if ($map !== false) {
672
                                    // by default \phpseclib3\File\ASN1 will try to render qualifier as a \phpseclib3\File\ASN1::TYPE_IA5_STRING since it's
673
                                    // actual type is \phpseclib3\File\ASN1::TYPE_ANY
674
                                    $subvalue = new Element(ASN1::encodeDER($subvalue, $map));
675
                                }
676
                            }
677
                        }
678
                        break;
679
                    case 'id-ce-authorityKeyIdentifier': // use 00 as the serial number instead of an empty string
680
                        if (isset($value['authorityCertSerialNumber'])) {
681
                            if ($value['authorityCertSerialNumber']->toBytes() == '') {
682
                                $temp = chr((ASN1::CLASS_CONTEXT_SPECIFIC << 6) | 2) . "\1\0";
683
                                $value['authorityCertSerialNumber'] = new Element($temp);
684
                            }
685
                        }
686
                }
687
 
688
                /* [extnValue] contains the DER encoding of an ASN.1 value
689
                   corresponding to the extension type identified by extnID */
690
                $map = $this->getMapping($id);
691
                if (is_bool($map)) {
692
                    if (!$map) {
693
                        //user_error($id . ' is not a currently supported extension');
694
                        unset($extensions[$i]);
695
                    }
696
                } else {
697
                    $value = ASN1::encodeDER($value, $map, ['iPAddress' => [static::class, 'encodeIP']]);
698
                }
699
            }
700
        }
701
    }
702
 
703
    /**
704
     * Map attribute values from ANY type to attribute-specific internal
705
     *   format.
706
     *
707
     * @param array $root (by reference)
708
     * @param string $path
709
     */
710
    private function mapInAttributes(&$root, $path)
711
    {
712
        $attributes = &$this->subArray($root, $path);
713
 
714
        if (is_array($attributes)) {
715
            for ($i = 0; $i < count($attributes); $i++) {
716
                $id = $attributes[$i]['type'];
717
                /* $value contains the DER encoding of an ASN.1 value
718
                   corresponding to the attribute type identified by type */
719
                $map = $this->getMapping($id);
720
                if (is_array($attributes[$i]['value'])) {
721
                    $values = &$attributes[$i]['value'];
722
                    for ($j = 0; $j < count($values); $j++) {
723
                        $value = ASN1::encodeDER($values[$j], Maps\AttributeValue::MAP);
724
                        $decoded = ASN1::decodeBER($value);
725
                        if (!is_bool($map)) {
1042 daniel-mar 726
                            if (!$decoded) {
727
                                continue;
728
                            }
827 daniel-mar 729
                            $mapped = ASN1::asn1map($decoded[0], $map);
730
                            if ($mapped !== false) {
731
                                $values[$j] = $mapped;
732
                            }
733
                            if ($id == 'pkcs-9-at-extensionRequest' && $this->isSubArrayValid($values, $j)) {
734
                                $this->mapInExtensions($values, $j);
735
                            }
736
                        } elseif ($map) {
737
                            $values[$j] = $value;
738
                        }
739
                    }
740
                }
741
            }
742
        }
743
    }
744
 
745
    /**
746
     * Map attribute values from attribute-specific internal format to
747
     *   ANY type.
748
     *
749
     * @param array $root (by reference)
750
     * @param string $path
751
     */
752
    private function mapOutAttributes(&$root, $path)
753
    {
754
        $attributes = &$this->subArray($root, $path);
755
 
756
        if (is_array($attributes)) {
757
            $size = count($attributes);
758
            for ($i = 0; $i < $size; $i++) {
759
                /* [value] contains the DER encoding of an ASN.1 value
760
                   corresponding to the attribute type identified by type */
761
                $id = $attributes[$i]['type'];
762
                $map = $this->getMapping($id);
763
                if ($map === false) {
764
                    //user_error($id . ' is not a currently supported attribute', E_USER_NOTICE);
765
                    unset($attributes[$i]);
766
                } elseif (is_array($attributes[$i]['value'])) {
767
                    $values = &$attributes[$i]['value'];
768
                    for ($j = 0; $j < count($values); $j++) {
769
                        switch ($id) {
770
                            case 'pkcs-9-at-extensionRequest':
771
                                $this->mapOutExtensions($values, $j);
772
                                break;
773
                        }
774
 
775
                        if (!is_bool($map)) {
776
                            $temp = ASN1::encodeDER($values[$j], $map);
777
                            $decoded = ASN1::decodeBER($temp);
1042 daniel-mar 778
                            if (!$decoded) {
779
                                continue;
780
                            }
827 daniel-mar 781
                            $values[$j] = ASN1::asn1map($decoded[0], Maps\AttributeValue::MAP);
782
                        }
783
                    }
784
                }
785
            }
786
        }
787
    }
788
 
789
    /**
790
     * Map DN values from ANY type to DN-specific internal
791
     *   format.
792
     *
793
     * @param array $root (by reference)
794
     * @param string $path
795
     */
1042 daniel-mar 796
    private function mapInDNs(array &$root, $path)
827 daniel-mar 797
    {
798
        $dns = &$this->subArray($root, $path);
799
 
800
        if (is_array($dns)) {
801
            for ($i = 0; $i < count($dns); $i++) {
802
                for ($j = 0; $j < count($dns[$i]); $j++) {
803
                    $type = $dns[$i][$j]['type'];
804
                    $value = &$dns[$i][$j]['value'];
805
                    if (is_object($value) && $value instanceof Element) {
806
                        $map = $this->getMapping($type);
807
                        if (!is_bool($map)) {
808
                            $decoded = ASN1::decodeBER($value);
1042 daniel-mar 809
                            if (!$decoded) {
810
                                continue;
811
                            }
827 daniel-mar 812
                            $value = ASN1::asn1map($decoded[0], $map);
813
                        }
814
                    }
815
                }
816
            }
817
        }
818
    }
819
 
820
    /**
821
     * Map DN values from DN-specific internal format to
822
     *   ANY type.
823
     *
824
     * @param array $root (by reference)
825
     * @param string $path
826
     */
1042 daniel-mar 827
    private function mapOutDNs(array &$root, $path)
827 daniel-mar 828
    {
829
        $dns = &$this->subArray($root, $path);
830
 
831
        if (is_array($dns)) {
832
            $size = count($dns);
833
            for ($i = 0; $i < $size; $i++) {
834
                for ($j = 0; $j < count($dns[$i]); $j++) {
835
                    $type = $dns[$i][$j]['type'];
836
                    $value = &$dns[$i][$j]['value'];
837
                    if (is_object($value) && $value instanceof Element) {
838
                        continue;
839
                    }
840
 
841
                    $map = $this->getMapping($type);
842
                    if (!is_bool($map)) {
843
                        $value = new Element(ASN1::encodeDER($value, $map));
844
                    }
845
                }
846
            }
847
        }
848
    }
849
 
850
    /**
851
     * Associate an extension ID to an extension mapping
852
     *
853
     * @param string $extnId
854
     * @return mixed
855
     */
856
    private function getMapping($extnId)
857
    {
858
        if (!is_string($extnId)) { // eg. if it's a \phpseclib3\File\ASN1\Element object
859
            return true;
860
        }
861
 
862
        if (isset(self::$extensions[$extnId])) {
863
            return self::$extensions[$extnId];
864
        }
865
 
866
        switch ($extnId) {
867
            case 'id-ce-keyUsage':
868
                return Maps\KeyUsage::MAP;
869
            case 'id-ce-basicConstraints':
870
                return Maps\BasicConstraints::MAP;
871
            case 'id-ce-subjectKeyIdentifier':
872
                return Maps\KeyIdentifier::MAP;
873
            case 'id-ce-cRLDistributionPoints':
874
                return Maps\CRLDistributionPoints::MAP;
875
            case 'id-ce-authorityKeyIdentifier':
876
                return Maps\AuthorityKeyIdentifier::MAP;
877
            case 'id-ce-certificatePolicies':
878
                return Maps\CertificatePolicies::MAP;
879
            case 'id-ce-extKeyUsage':
880
                return Maps\ExtKeyUsageSyntax::MAP;
881
            case 'id-pe-authorityInfoAccess':
882
                return Maps\AuthorityInfoAccessSyntax::MAP;
883
            case 'id-ce-subjectAltName':
884
                return Maps\SubjectAltName::MAP;
885
            case 'id-ce-subjectDirectoryAttributes':
886
                return Maps\SubjectDirectoryAttributes::MAP;
887
            case 'id-ce-privateKeyUsagePeriod':
888
                return Maps\PrivateKeyUsagePeriod::MAP;
889
            case 'id-ce-issuerAltName':
890
                return Maps\IssuerAltName::MAP;
891
            case 'id-ce-policyMappings':
892
                return Maps\PolicyMappings::MAP;
893
            case 'id-ce-nameConstraints':
894
                return Maps\NameConstraints::MAP;
895
 
896
            case 'netscape-cert-type':
897
                return Maps\netscape_cert_type::MAP;
898
            case 'netscape-comment':
899
                return Maps\netscape_comment::MAP;
900
            case 'netscape-ca-policy-url':
901
                return Maps\netscape_ca_policy_url::MAP;
902
 
903
            // since id-qt-cps isn't a constructed type it will have already been decoded as a string by the time it gets
904
            // back around to asn1map() and we don't want it decoded again.
905
            //case 'id-qt-cps':
906
            //    return Maps\CPSuri::MAP;
907
            case 'id-qt-unotice':
908
                return Maps\UserNotice::MAP;
909
 
910
            // the following OIDs are unsupported but we don't want them to give notices when calling saveX509().
911
            case 'id-pe-logotype': // http://www.ietf.org/rfc/rfc3709.txt
912
            case 'entrustVersInfo':
913
            // http://support.microsoft.com/kb/287547
914
            case '1.3.6.1.4.1.311.20.2': // szOID_ENROLL_CERTTYPE_EXTENSION
915
            case '1.3.6.1.4.1.311.21.1': // szOID_CERTSRV_CA_VERSION
916
            // "SET Secure Electronic Transaction Specification"
917
            // http://www.maithean.com/docs/set_bk3.pdf
918
            case '2.23.42.7.0': // id-set-hashedRootKey
919
            // "Certificate Transparency"
920
            // https://tools.ietf.org/html/rfc6962
921
            case '1.3.6.1.4.1.11129.2.4.2':
922
            // "Qualified Certificate statements"
923
            // https://tools.ietf.org/html/rfc3739#section-3.2.6
924
            case '1.3.6.1.5.5.7.1.3':
925
                return true;
926
 
927
            // CSR attributes
928
            case 'pkcs-9-at-unstructuredName':
929
                return Maps\PKCS9String::MAP;
930
            case 'pkcs-9-at-challengePassword':
931
                return Maps\DirectoryString::MAP;
932
            case 'pkcs-9-at-extensionRequest':
933
                return Maps\Extensions::MAP;
934
 
935
            // CRL extensions.
936
            case 'id-ce-cRLNumber':
937
                return Maps\CRLNumber::MAP;
938
            case 'id-ce-deltaCRLIndicator':
939
                return Maps\CRLNumber::MAP;
940
            case 'id-ce-issuingDistributionPoint':
941
                return Maps\IssuingDistributionPoint::MAP;
942
            case 'id-ce-freshestCRL':
943
                return Maps\CRLDistributionPoints::MAP;
944
            case 'id-ce-cRLReasons':
945
                return Maps\CRLReason::MAP;
946
            case 'id-ce-invalidityDate':
947
                return Maps\InvalidityDate::MAP;
948
            case 'id-ce-certificateIssuer':
949
                return Maps\CertificateIssuer::MAP;
950
            case 'id-ce-holdInstructionCode':
951
                return Maps\HoldInstructionCode::MAP;
952
            case 'id-at-postalAddress':
953
                return Maps\PostalAddress::MAP;
954
        }
955
 
956
        return false;
957
    }
958
 
959
    /**
960
     * Load an X.509 certificate as a certificate authority
961
     *
962
     * @param string $cert
963
     * @return bool
964
     */
965
    public function loadCA($cert)
966
    {
967
        $olddn = $this->dn;
968
        $oldcert = $this->currentCert;
969
        $oldsigsubj = $this->signatureSubject;
970
        $oldkeyid = $this->currentKeyIdentifier;
971
 
972
        $cert = $this->loadX509($cert);
973
        if (!$cert) {
974
            $this->dn = $olddn;
975
            $this->currentCert = $oldcert;
976
            $this->signatureSubject = $oldsigsubj;
977
            $this->currentKeyIdentifier = $oldkeyid;
978
 
979
            return false;
980
        }
981
 
982
        /* From RFC5280 "PKIX Certificate and CRL Profile":
983
 
984
           If the keyUsage extension is present, then the subject public key
985
           MUST NOT be used to verify signatures on certificates or CRLs unless
986
           the corresponding keyCertSign or cRLSign bit is set. */
987
        //$keyUsage = $this->getExtension('id-ce-keyUsage');
988
        //if ($keyUsage && !in_array('keyCertSign', $keyUsage)) {
989
        //    return false;
990
        //}
991
 
992
        /* From RFC5280 "PKIX Certificate and CRL Profile":
993
 
994
           The cA boolean indicates whether the certified public key may be used
995
           to verify certificate signatures.  If the cA boolean is not asserted,
996
           then the keyCertSign bit in the key usage extension MUST NOT be
997
           asserted.  If the basic constraints extension is not present in a
998
           version 3 certificate, or the extension is present but the cA boolean
999
           is not asserted, then the certified public key MUST NOT be used to
1000
           verify certificate signatures. */
1001
        //$basicConstraints = $this->getExtension('id-ce-basicConstraints');
1002
        //if (!$basicConstraints || !$basicConstraints['cA']) {
1003
        //    return false;
1004
        //}
1005
 
1006
        $this->CAs[] = $cert;
1007
 
1008
        $this->dn = $olddn;
1009
        $this->currentCert = $oldcert;
1010
        $this->signatureSubject = $oldsigsubj;
1011
 
1012
        return true;
1013
    }
1014
 
1015
    /**
1016
     * Validate an X.509 certificate against a URL
1017
     *
1018
     * From RFC2818 "HTTP over TLS":
1019
     *
1020
     * Matching is performed using the matching rules specified by
1021
     * [RFC2459].  If more than one identity of a given type is present in
1022
     * the certificate (e.g., more than one dNSName name, a match in any one
1023
     * of the set is considered acceptable.) Names may contain the wildcard
1024
     * character * which is considered to match any single domain name
1025
     * component or component fragment. E.g., *.a.com matches foo.a.com but
1026
     * not bar.foo.a.com. f*.com matches foo.com but not bar.com.
1027
     *
1028
     * @param string $url
1029
     * @return bool
1030
     */
1031
    public function validateURL($url)
1032
    {
1033
        if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) {
1034
            return false;
1035
        }
1036
 
1037
        $components = parse_url($url);
1038
        if (!isset($components['host'])) {
1039
            return false;
1040
        }
1041
 
1042
        if ($names = $this->getExtension('id-ce-subjectAltName')) {
1043
            foreach ($names as $name) {
1044
                foreach ($name as $key => $value) {
1411 daniel-mar 1045
                    $value = preg_quote($value);
1046
                    $value = str_replace('\*', '[^.]*', $value);
827 daniel-mar 1047
                    switch ($key) {
1048
                        case 'dNSName':
1049
                            /* From RFC2818 "HTTP over TLS":
1050
 
1051
                               If a subjectAltName extension of type dNSName is present, that MUST
1052
                               be used as the identity. Otherwise, the (most specific) Common Name
1053
                               field in the Subject field of the certificate MUST be used. Although
1054
                               the use of the Common Name is existing practice, it is deprecated and
1055
                               Certification Authorities are encouraged to use the dNSName instead. */
1056
                            if (preg_match('#^' . $value . '$#', $components['host'])) {
1057
                                return true;
1058
                            }
1059
                            break;
1060
                        case 'iPAddress':
1061
                            /* From RFC2818 "HTTP over TLS":
1062
 
1063
                               In some cases, the URI is specified as an IP address rather than a
1064
                               hostname. In this case, the iPAddress subjectAltName must be present
1065
                               in the certificate and must exactly match the IP in the URI. */
1066
                            if (preg_match('#(?:\d{1-3}\.){4}#', $components['host'] . '.') && preg_match('#^' . $value . '$#', $components['host'])) {
1067
                                return true;
1068
                            }
1069
                    }
1070
                }
1071
            }
1072
            return false;
1073
        }
1074
 
1075
        if ($value = $this->getDNProp('id-at-commonName')) {
1076
            $value = str_replace(['.', '*'], ['\.', '[^.]*'], $value[0]);
1077
            return preg_match('#^' . $value . '$#', $components['host']) === 1;
1078
        }
1079
 
1080
        return false;
1081
    }
1082
 
1083
    /**
1084
     * Validate a date
1085
     *
1086
     * If $date isn't defined it is assumed to be the current date.
1087
     *
1088
     * @param \DateTimeInterface|string $date optional
1089
     * @return bool
1090
     */
1091
    public function validateDate($date = null)
1092
    {
1093
        if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) {
1094
            return false;
1095
        }
1096
 
1097
        if (!isset($date)) {
1098
            $date = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
1099
        }
1100
 
1101
        $notBefore = $this->currentCert['tbsCertificate']['validity']['notBefore'];
1102
        $notBefore = isset($notBefore['generalTime']) ? $notBefore['generalTime'] : $notBefore['utcTime'];
1103
 
1104
        $notAfter = $this->currentCert['tbsCertificate']['validity']['notAfter'];
1105
        $notAfter = isset($notAfter['generalTime']) ? $notAfter['generalTime'] : $notAfter['utcTime'];
1106
 
1107
        if (is_string($date)) {
1108
            $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get()));
1109
        }
1110
 
1111
        $notBefore = new \DateTimeImmutable($notBefore, new \DateTimeZone(@date_default_timezone_get()));
1112
        $notAfter = new \DateTimeImmutable($notAfter, new \DateTimeZone(@date_default_timezone_get()));
1113
 
1114
        return $date >= $notBefore && $date <= $notAfter;
1115
    }
1116
 
1117
    /**
1118
     * Fetches a URL
1119
     *
1120
     * @param string $url
1121
     * @return bool|string
1122
     */
1123
    private static function fetchURL($url)
1124
    {
1125
        if (self::$disable_url_fetch) {
1126
            return false;
1127
        }
1128
 
1129
        $parts = parse_url($url);
1130
        $data = '';
1131
        switch ($parts['scheme']) {
1132
            case 'http':
1133
                $fsock = @fsockopen($parts['host'], isset($parts['port']) ? $parts['port'] : 80);
1134
                if (!$fsock) {
1135
                    return false;
1136
                }
1042 daniel-mar 1137
                $path = $parts['path'];
1138
                if (isset($parts['query'])) {
1139
                    $path .= '?' . $parts['query'];
1140
                }
1141
                fputs($fsock, "GET $path HTTP/1.0\r\n");
827 daniel-mar 1142
                fputs($fsock, "Host: $parts[host]\r\n\r\n");
1143
                $line = fgets($fsock, 1024);
1144
                if (strlen($line) < 3) {
1145
                    return false;
1146
                }
1147
                preg_match('#HTTP/1.\d (\d{3})#', $line, $temp);
1148
                if ($temp[1] != '200') {
1149
                    return false;
1150
                }
1151
 
1152
                // skip the rest of the headers in the http response
1153
                while (!feof($fsock) && fgets($fsock, 1024) != "\r\n") {
1154
                }
1155
 
1156
                while (!feof($fsock)) {
1157
                    $temp = fread($fsock, 1024);
1158
                    if ($temp === false) {
1159
                        return false;
1160
                    }
1161
                    $data .= $temp;
1162
                }
1163
 
1164
                break;
1165
            //case 'ftp':
1166
            //case 'ldap':
1167
            //default:
1168
        }
1169
 
1170
        return $data;
1171
    }
1172
 
1173
    /**
1174
     * Validates an intermediate cert as identified via authority info access extension
1175
     *
1176
     * See https://tools.ietf.org/html/rfc4325 for more info
1177
     *
1178
     * @param bool $caonly
1179
     * @param int $count
1180
     * @return bool
1181
     */
1182
    private function testForIntermediate($caonly, $count)
1183
    {
1184
        $opts = $this->getExtension('id-pe-authorityInfoAccess');
1185
        if (!is_array($opts)) {
1186
            return false;
1187
        }
1188
        foreach ($opts as $opt) {
1189
            if ($opt['accessMethod'] == 'id-ad-caIssuers') {
1190
                // accessLocation is a GeneralName. GeneralName fields support stuff like email addresses, IP addresses, LDAP,
1191
                // etc, but we're only supporting URI's. URI's and LDAP are the only thing https://tools.ietf.org/html/rfc4325
1192
                // discusses
1193
                if (isset($opt['accessLocation']['uniformResourceIdentifier'])) {
1194
                    $url = $opt['accessLocation']['uniformResourceIdentifier'];
1195
                    break;
1196
                }
1197
            }
1198
        }
1199
 
1200
        if (!isset($url)) {
1201
            return false;
1202
        }
1203
 
1204
        $cert = static::fetchURL($url);
1205
        if (!is_string($cert)) {
1206
            return false;
1207
        }
1208
 
1209
        $parent = new static();
1210
        $parent->CAs = $this->CAs;
1211
        /*
1212
         "Conforming applications that support HTTP or FTP for accessing
1213
          certificates MUST be able to accept .cer files and SHOULD be able
1214
          to accept .p7c files." -- https://tools.ietf.org/html/rfc4325
1215
 
1216
         A .p7c file is 'a "certs-only" CMS message as specified in RFC 2797"
1217
 
1218
         These are currently unsupported
1219
        */
1220
        if (!is_array($parent->loadX509($cert))) {
1221
            return false;
1222
        }
1223
 
1224
        if (!$parent->validateSignatureCountable($caonly, ++$count)) {
1225
            return false;
1226
        }
1227
 
1228
        $this->CAs[] = $parent->currentCert;
1229
        //$this->loadCA($cert);
1230
 
1231
        return true;
1232
    }
1233
 
1234
    /**
1235
     * Validate a signature
1236
     *
1237
     * Works on X.509 certs, CSR's and CRL's.
1238
     * Returns true if the signature is verified, false if it is not correct or null on error
1239
     *
1240
     * By default returns false for self-signed certs. Call validateSignature(false) to make this support
1241
     * self-signed.
1242
     *
1243
     * The behavior of this function is inspired by {@link http://php.net/openssl-verify openssl_verify}.
1244
     *
1245
     * @param bool $caonly optional
1246
     * @return mixed
1247
     */
1248
    public function validateSignature($caonly = true)
1249
    {
1250
        return $this->validateSignatureCountable($caonly, 0);
1251
    }
1252
 
1253
    /**
1254
     * Validate a signature
1255
     *
1256
     * Performs said validation whilst keeping track of how many times validation method is called
1257
     *
1258
     * @param bool $caonly
1259
     * @param int $count
1260
     * @return mixed
1261
     */
1262
    private function validateSignatureCountable($caonly, $count)
1263
    {
1264
        if (!is_array($this->currentCert) || !isset($this->signatureSubject)) {
1265
            return null;
1266
        }
1267
 
1268
        if ($count == self::$recur_limit) {
1269
            return false;
1270
        }
1271
 
1272
        /* TODO:
1273
           "emailAddress attribute values are not case-sensitive (e.g., "subscriber@example.com" is the same as "SUBSCRIBER@EXAMPLE.COM")."
1274
            -- http://tools.ietf.org/html/rfc5280#section-4.1.2.6
1275
 
1276
           implement pathLenConstraint in the id-ce-basicConstraints extension */
1277
 
1278
        switch (true) {
1279
            case isset($this->currentCert['tbsCertificate']):
1280
                // self-signed cert
1281
                switch (true) {
1282
                    case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $this->currentCert['tbsCertificate']['subject']:
1283
                    case defined('FILE_X509_IGNORE_TYPE') && $this->getIssuerDN(self::DN_STRING) === $this->getDN(self::DN_STRING):
1284
                        $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier');
1285
                        $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier');
1286
                        switch (true) {
1287
                            case !is_array($authorityKey):
1288
                            case !$subjectKeyID:
1289
                            case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
1290
                                $signingCert = $this->currentCert; // working cert
1291
                        }
1292
                }
1293
 
1294
                if (!empty($this->CAs)) {
1295
                    for ($i = 0; $i < count($this->CAs); $i++) {
1296
                        // even if the cert is a self-signed one we still want to see if it's a CA;
1297
                        // if not, we'll conditionally return an error
1298
                        $ca = $this->CAs[$i];
1299
                        switch (true) {
1300
                            case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']:
1301
                            case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertificate']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']):
1302
                                $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier');
1303
                                $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca);
1304
                                switch (true) {
1305
                                    case !is_array($authorityKey):
1306
                                    case !$subjectKeyID:
1307
                                    case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
1308
                                        if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) {
1309
                                            break 2; // serial mismatch - check other ca
1310
                                        }
1311
                                        $signingCert = $ca; // working cert
1312
                                        break 3;
1313
                                }
1314
                        }
1315
                    }
1316
                    if (count($this->CAs) == $i && $caonly) {
1317
                        return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly);
1318
                    }
1319
                } elseif (!isset($signingCert) || $caonly) {
1320
                    return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly);
1321
                }
1322
                return $this->validateSignatureHelper(
1323
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'],
1324
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'],
1325
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1326
                    substr($this->currentCert['signature'], 1),
1327
                    $this->signatureSubject
1328
                );
1329
            case isset($this->currentCert['certificationRequestInfo']):
1330
                return $this->validateSignatureHelper(
1331
                    $this->currentCert['certificationRequestInfo']['subjectPKInfo']['algorithm']['algorithm'],
1332
                    $this->currentCert['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'],
1333
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1334
                    substr($this->currentCert['signature'], 1),
1335
                    $this->signatureSubject
1336
                );
1337
            case isset($this->currentCert['publicKeyAndChallenge']):
1338
                return $this->validateSignatureHelper(
1339
                    $this->currentCert['publicKeyAndChallenge']['spki']['algorithm']['algorithm'],
1340
                    $this->currentCert['publicKeyAndChallenge']['spki']['subjectPublicKey'],
1341
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1342
                    substr($this->currentCert['signature'], 1),
1343
                    $this->signatureSubject
1344
                );
1345
            case isset($this->currentCert['tbsCertList']):
1346
                if (!empty($this->CAs)) {
1347
                    for ($i = 0; $i < count($this->CAs); $i++) {
1348
                        $ca = $this->CAs[$i];
1349
                        switch (true) {
1350
                            case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertList']['issuer'] === $ca['tbsCertificate']['subject']:
1351
                            case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertList']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']):
1352
                                $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier');
1353
                                $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca);
1354
                                switch (true) {
1355
                                    case !is_array($authorityKey):
1356
                                    case !$subjectKeyID:
1357
                                    case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
1358
                                        if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) {
1359
                                            break 2; // serial mismatch - check other ca
1360
                                        }
1361
                                        $signingCert = $ca; // working cert
1362
                                        break 3;
1363
                                }
1364
                        }
1365
                    }
1366
                }
1367
                if (!isset($signingCert)) {
1368
                    return false;
1369
                }
1370
                return $this->validateSignatureHelper(
1371
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'],
1372
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'],
1373
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1374
                    substr($this->currentCert['signature'], 1),
1375
                    $this->signatureSubject
1376
                );
1377
            default:
1378
                return false;
1379
        }
1380
    }
1381
 
1382
    /**
1383
     * Validates a signature
1384
     *
1385
     * Returns true if the signature is verified and false if it is not correct.
1386
     * If the algorithms are unsupposed an exception is thrown.
1387
     *
1388
     * @param string $publicKeyAlgorithm
1389
     * @param string $publicKey
1390
     * @param string $signatureAlgorithm
1391
     * @param string $signature
1392
     * @param string $signatureSubject
1393
     * @throws \phpseclib3\Exception\UnsupportedAlgorithmException if the algorithm is unsupported
1394
     * @return bool
1395
     */
1396
    private function validateSignatureHelper($publicKeyAlgorithm, $publicKey, $signatureAlgorithm, $signature, $signatureSubject)
1397
    {
1398
        switch ($publicKeyAlgorithm) {
1399
            case 'id-RSASSA-PSS':
1400
                $key = RSA::loadFormat('PSS', $publicKey);
1401
                break;
1402
            case 'rsaEncryption':
1403
                $key = RSA::loadFormat('PKCS8', $publicKey);
1404
                switch ($signatureAlgorithm) {
1042 daniel-mar 1405
                    case 'id-RSASSA-PSS':
1406
                        break;
827 daniel-mar 1407
                    case 'md2WithRSAEncryption':
1408
                    case 'md5WithRSAEncryption':
1409
                    case 'sha1WithRSAEncryption':
1410
                    case 'sha224WithRSAEncryption':
1411
                    case 'sha256WithRSAEncryption':
1412
                    case 'sha384WithRSAEncryption':
1413
                    case 'sha512WithRSAEncryption':
1414
                        $key = $key
1415
                            ->withHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm))
1416
                            ->withPadding(RSA::SIGNATURE_PKCS1);
1417
                        break;
1418
                    default:
1419
                        throw new UnsupportedAlgorithmException('Signature algorithm unsupported');
1420
                }
1421
                break;
1422
            case 'id-Ed25519':
1423
            case 'id-Ed448':
1424
                $key = EC::loadFormat('PKCS8', $publicKey);
1425
                break;
1426
            case 'id-ecPublicKey':
1427
                $key = EC::loadFormat('PKCS8', $publicKey);
1428
                switch ($signatureAlgorithm) {
1429
                    case 'ecdsa-with-SHA1':
1430
                    case 'ecdsa-with-SHA224':
1431
                    case 'ecdsa-with-SHA256':
1432
                    case 'ecdsa-with-SHA384':
1433
                    case 'ecdsa-with-SHA512':
1434
                        $key = $key
1435
                            ->withHash(preg_replace('#^ecdsa-with-#', '', strtolower($signatureAlgorithm)));
1436
                        break;
1437
                    default:
1438
                        throw new UnsupportedAlgorithmException('Signature algorithm unsupported');
1439
                }
1440
                break;
1441
            case 'id-dsa':
1442
                $key = DSA::loadFormat('PKCS8', $publicKey);
1443
                switch ($signatureAlgorithm) {
1444
                    case 'id-dsa-with-sha1':
1445
                    case 'id-dsa-with-sha224':
1446
                    case 'id-dsa-with-sha256':
1447
                        $key = $key
1448
                            ->withHash(preg_replace('#^id-dsa-with-#', '', strtolower($signatureAlgorithm)));
1449
                        break;
1450
                    default:
1451
                        throw new UnsupportedAlgorithmException('Signature algorithm unsupported');
1452
                }
1453
                break;
1454
            default:
1455
                throw new UnsupportedAlgorithmException('Public key algorithm unsupported');
1456
        }
1457
 
1458
        return $key->verify($signatureSubject, $signature);
1459
    }
1460
 
1461
    /**
1462
     * Sets the recursion limit
1463
     *
1464
     * When validating a signature it may be necessary to download intermediate certs from URI's.
1465
     * An intermediate cert that linked to itself would result in an infinite loop so to prevent
1466
     * that we set a recursion limit. A negative number means that there is no recursion limit.
1467
     *
1468
     * @param int $count
1469
     */
1470
    public static function setRecurLimit($count)
1471
    {
1472
        self::$recur_limit = $count;
1473
    }
1474
 
1475
    /**
1476
     * Prevents URIs from being automatically retrieved
1477
     *
1478
     */
1479
    public static function disableURLFetch()
1480
    {
1481
        self::$disable_url_fetch = true;
1482
    }
1483
 
1484
    /**
1485
     * Allows URIs to be automatically retrieved
1486
     *
1487
     */
1488
    public static function enableURLFetch()
1489
    {
1490
        self::$disable_url_fetch = false;
1491
    }
1492
 
1493
    /**
1494
     * Decodes an IP address
1495
     *
1496
     * Takes in a base64 encoded "blob" and returns a human readable IP address
1497
     *
1498
     * @param string $ip
1499
     * @return string
1500
     */
1501
    public static function decodeIP($ip)
1502
    {
1503
        return inet_ntop($ip);
1504
    }
1505
 
1506
    /**
1507
     * Decodes an IP address in a name constraints extension
1508
     *
1509
     * Takes in a base64 encoded "blob" and returns a human readable IP address / mask
1510
     *
1511
     * @param string $ip
1512
     * @return array
1513
     */
1514
    public static function decodeNameConstraintIP($ip)
1515
    {
1516
        $size = strlen($ip) >> 1;
1517
        $mask = substr($ip, $size);
1518
        $ip = substr($ip, 0, $size);
1519
        return [inet_ntop($ip), inet_ntop($mask)];
1520
    }
1521
 
1522
    /**
1523
     * Encodes an IP address
1524
     *
1525
     * Takes a human readable IP address into a base64-encoded "blob"
1526
     *
1527
     * @param string|array $ip
1528
     * @return string
1529
     */
1530
    public static function encodeIP($ip)
1531
    {
1532
        return is_string($ip) ?
1533
            inet_pton($ip) :
1534
            inet_pton($ip[0]) . inet_pton($ip[1]);
1535
    }
1536
 
1537
    /**
1538
     * "Normalizes" a Distinguished Name property
1539
     *
1540
     * @param string $propName
1541
     * @return mixed
1542
     */
1543
    private function translateDNProp($propName)
1544
    {
1545
        switch (strtolower($propName)) {
1308 daniel-mar 1546
            case 'jurisdictionofincorporationcountryname':
1547
            case 'jurisdictioncountryname':
1548
            case 'jurisdictionc':
1549
                return 'jurisdictionOfIncorporationCountryName';
1550
            case 'jurisdictionofincorporationstateorprovincename':
1551
            case 'jurisdictionstateorprovincename':
1552
            case 'jurisdictionst':
1553
                return 'jurisdictionOfIncorporationStateOrProvinceName';
1554
            case 'jurisdictionlocalityname':
1555
            case 'jurisdictionl':
1556
                return 'jurisdictionLocalityName';
1557
            case 'id-at-businesscategory':
1558
            case 'businesscategory':
1559
                return 'id-at-businessCategory';
827 daniel-mar 1560
            case 'id-at-countryname':
1561
            case 'countryname':
1562
            case 'c':
1563
                return 'id-at-countryName';
1564
            case 'id-at-organizationname':
1565
            case 'organizationname':
1566
            case 'o':
1567
                return 'id-at-organizationName';
1568
            case 'id-at-dnqualifier':
1569
            case 'dnqualifier':
1570
                return 'id-at-dnQualifier';
1571
            case 'id-at-commonname':
1572
            case 'commonname':
1573
            case 'cn':
1574
                return 'id-at-commonName';
1575
            case 'id-at-stateorprovincename':
1576
            case 'stateorprovincename':
1577
            case 'state':
1578
            case 'province':
1579
            case 'provincename':
1580
            case 'st':
1581
                return 'id-at-stateOrProvinceName';
1582
            case 'id-at-localityname':
1583
            case 'localityname':
1584
            case 'l':
1585
                return 'id-at-localityName';
1586
            case 'id-emailaddress':
1587
            case 'emailaddress':
1588
                return 'pkcs-9-at-emailAddress';
1589
            case 'id-at-serialnumber':
1590
            case 'serialnumber':
1591
                return 'id-at-serialNumber';
1592
            case 'id-at-postalcode':
1593
            case 'postalcode':
1594
                return 'id-at-postalCode';
1595
            case 'id-at-streetaddress':
1596
            case 'streetaddress':
1597
                return 'id-at-streetAddress';
1598
            case 'id-at-name':
1599
            case 'name':
1600
                return 'id-at-name';
1601
            case 'id-at-givenname':
1602
            case 'givenname':
1603
                return 'id-at-givenName';
1604
            case 'id-at-surname':
1605
            case 'surname':
1606
            case 'sn':
1607
                return 'id-at-surname';
1608
            case 'id-at-initials':
1609
            case 'initials':
1610
                return 'id-at-initials';
1611
            case 'id-at-generationqualifier':
1612
            case 'generationqualifier':
1613
                return 'id-at-generationQualifier';
1614
            case 'id-at-organizationalunitname':
1615
            case 'organizationalunitname':
1616
            case 'ou':
1617
                return 'id-at-organizationalUnitName';
1618
            case 'id-at-pseudonym':
1619
            case 'pseudonym':
1620
                return 'id-at-pseudonym';
1621
            case 'id-at-title':
1622
            case 'title':
1623
                return 'id-at-title';
1624
            case 'id-at-description':
1625
            case 'description':
1626
                return 'id-at-description';
1627
            case 'id-at-role':
1628
            case 'role':
1629
                return 'id-at-role';
1630
            case 'id-at-uniqueidentifier':
1631
            case 'uniqueidentifier':
1632
            case 'x500uniqueidentifier':
1633
                return 'id-at-uniqueIdentifier';
1634
            case 'postaladdress':
1635
            case 'id-at-postaladdress':
1636
                return 'id-at-postalAddress';
1637
            default:
1638
                return false;
1639
        }
1640
    }
1641
 
1642
    /**
1643
     * Set a Distinguished Name property
1644
     *
1645
     * @param string $propName
1646
     * @param mixed $propValue
1647
     * @param string $type optional
1648
     * @return bool
1649
     */
1650
    public function setDNProp($propName, $propValue, $type = 'utf8String')
1651
    {
1652
        if (empty($this->dn)) {
1653
            $this->dn = ['rdnSequence' => []];
1654
        }
1655
 
1656
        if (($propName = $this->translateDNProp($propName)) === false) {
1657
            return false;
1658
        }
1659
 
1660
        foreach ((array) $propValue as $v) {
1661
            if (!is_array($v) && isset($type)) {
1662
                $v = [$type => $v];
1663
            }
1664
            $this->dn['rdnSequence'][] = [
1665
                [
1666
                    'type' => $propName,
1667
                    'value' => $v
1668
                ]
1669
            ];
1670
        }
1671
 
1672
        return true;
1673
    }
1674
 
1675
    /**
1676
     * Remove Distinguished Name properties
1677
     *
1678
     * @param string $propName
1679
     */
1680
    public function removeDNProp($propName)
1681
    {
1682
        if (empty($this->dn)) {
1683
            return;
1684
        }
1685
 
1686
        if (($propName = $this->translateDNProp($propName)) === false) {
1687
            return;
1688
        }
1689
 
1690
        $dn = &$this->dn['rdnSequence'];
1691
        $size = count($dn);
1692
        for ($i = 0; $i < $size; $i++) {
1693
            if ($dn[$i][0]['type'] == $propName) {
1694
                unset($dn[$i]);
1695
            }
1696
        }
1697
 
1698
        $dn = array_values($dn);
1699
        // fix for https://bugs.php.net/75433 affecting PHP 7.2
1700
        if (!isset($dn[0])) {
1701
            $dn = array_splice($dn, 0, 0);
1702
        }
1703
    }
1704
 
1705
    /**
1706
     * Get Distinguished Name properties
1707
     *
1708
     * @param string $propName
1709
     * @param array $dn optional
1710
     * @param bool $withType optional
1711
     * @return mixed
1712
     */
1042 daniel-mar 1713
    public function getDNProp($propName, array $dn = null, $withType = false)
827 daniel-mar 1714
    {
1715
        if (!isset($dn)) {
1716
            $dn = $this->dn;
1717
        }
1718
 
1719
        if (empty($dn)) {
1720
            return false;
1721
        }
1722
 
1723
        if (($propName = $this->translateDNProp($propName)) === false) {
1724
            return false;
1725
        }
1726
 
1727
        $filters = [];
1728
        $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1729
        ASN1::setFilters($filters);
1730
        $this->mapOutDNs($dn, 'rdnSequence');
1731
        $dn = $dn['rdnSequence'];
1732
        $result = [];
1733
        for ($i = 0; $i < count($dn); $i++) {
1734
            if ($dn[$i][0]['type'] == $propName) {
1735
                $v = $dn[$i][0]['value'];
1736
                if (!$withType) {
1737
                    if (is_array($v)) {
1738
                        foreach ($v as $type => $s) {
1739
                            $type = array_search($type, ASN1::ANY_MAP);
1740
                            if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) {
1741
                                $s = ASN1::convert($s, $type);
1742
                                if ($s !== false) {
1743
                                    $v = $s;
1744
                                    break;
1745
                                }
1746
                            }
1747
                        }
1748
                        if (is_array($v)) {
1749
                            $v = array_pop($v); // Always strip data type.
1750
                        }
1751
                    } elseif (is_object($v) && $v instanceof Element) {
1752
                        $map = $this->getMapping($propName);
1753
                        if (!is_bool($map)) {
1754
                            $decoded = ASN1::decodeBER($v);
1042 daniel-mar 1755
                            if (!$decoded) {
1756
                                return false;
1757
                            }
827 daniel-mar 1758
                            $v = ASN1::asn1map($decoded[0], $map);
1759
                        }
1760
                    }
1761
                }
1762
                $result[] = $v;
1763
            }
1764
        }
1765
 
1766
        return $result;
1767
    }
1768
 
1769
    /**
1770
     * Set a Distinguished Name
1771
     *
1772
     * @param mixed $dn
1773
     * @param bool $merge optional
1774
     * @param string $type optional
1775
     * @return bool
1776
     */
1777
    public function setDN($dn, $merge = false, $type = 'utf8String')
1778
    {
1779
        if (!$merge) {
1780
            $this->dn = null;
1781
        }
1782
 
1783
        if (is_array($dn)) {
1784
            if (isset($dn['rdnSequence'])) {
1785
                $this->dn = $dn; // No merge here.
1786
                return true;
1787
            }
1788
 
1789
            // handles stuff generated by openssl_x509_parse()
1790
            foreach ($dn as $prop => $value) {
1791
                if (!$this->setDNProp($prop, $value, $type)) {
1792
                    return false;
1793
                }
1794
            }
1795
            return true;
1796
        }
1797
 
1798
        // handles everything else
1799
        $results = preg_split('#((?:^|, *|/)(?:C=|O=|OU=|CN=|L=|ST=|SN=|postalCode=|streetAddress=|emailAddress=|serialNumber=|organizationalUnitName=|title=|description=|role=|x500UniqueIdentifier=|postalAddress=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE);
1800
        for ($i = 1; $i < count($results); $i += 2) {
1801
            $prop = trim($results[$i], ', =/');
1802
            $value = $results[$i + 1];
1803
            if (!$this->setDNProp($prop, $value, $type)) {
1804
                return false;
1805
            }
1806
        }
1807
 
1808
        return true;
1809
    }
1810
 
1811
    /**
1812
     * Get the Distinguished Name for a certificates subject
1813
     *
1814
     * @param mixed $format optional
1815
     * @param array $dn optional
1816
     * @return array|bool|string
1817
     */
1042 daniel-mar 1818
    public function getDN($format = self::DN_ARRAY, array $dn = null)
827 daniel-mar 1819
    {
1820
        if (!isset($dn)) {
1821
            $dn = isset($this->currentCert['tbsCertList']) ? $this->currentCert['tbsCertList']['issuer'] : $this->dn;
1822
        }
1823
 
1824
        switch ((int) $format) {
1825
            case self::DN_ARRAY:
1826
                return $dn;
1827
            case self::DN_ASN1:
1828
                $filters = [];
1829
                $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1830
                ASN1::setFilters($filters);
1831
                $this->mapOutDNs($dn, 'rdnSequence');
1832
                return ASN1::encodeDER($dn, Maps\Name::MAP);
1833
            case self::DN_CANON:
1834
                //  No SEQUENCE around RDNs and all string values normalized as
1835
                // trimmed lowercase UTF-8 with all spacing as one blank.
1836
                // constructed RDNs will not be canonicalized
1837
                $filters = [];
1838
                $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1839
                ASN1::setFilters($filters);
1840
                $result = '';
1841
                $this->mapOutDNs($dn, 'rdnSequence');
1842
                foreach ($dn['rdnSequence'] as $rdn) {
1843
                    foreach ($rdn as $i => $attr) {
1844
                        $attr = &$rdn[$i];
1845
                        if (is_array($attr['value'])) {
1846
                            foreach ($attr['value'] as $type => $v) {
1847
                                $type = array_search($type, ASN1::ANY_MAP, true);
1848
                                if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) {
1849
                                    $v = ASN1::convert($v, $type);
1850
                                    if ($v !== false) {
1851
                                        $v = preg_replace('/\s+/', ' ', $v);
1852
                                        $attr['value'] = strtolower(trim($v));
1853
                                        break;
1854
                                    }
1855
                                }
1856
                            }
1857
                        }
1858
                    }
1859
                    $result .= ASN1::encodeDER($rdn, Maps\RelativeDistinguishedName::MAP);
1860
                }
1861
                return $result;
1862
            case self::DN_HASH:
1863
                $dn = $this->getDN(self::DN_CANON, $dn);
1864
                $hash = new Hash('sha1');
1865
                $hash = $hash->hash($dn);
1866
                extract(unpack('Vhash', $hash));
1042 daniel-mar 1867
                return strtolower(Strings::bin2hex(pack('N', $hash)));
827 daniel-mar 1868
        }
1869
 
1870
        // Default is to return a string.
1871
        $start = true;
1872
        $output = '';
1873
 
1874
        $result = [];
1875
        $filters = [];
1876
        $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1877
        ASN1::setFilters($filters);
1878
        $this->mapOutDNs($dn, 'rdnSequence');
1879
 
1880
        foreach ($dn['rdnSequence'] as $field) {
1881
            $prop = $field[0]['type'];
1882
            $value = $field[0]['value'];
1883
 
1884
            $delim = ', ';
1885
            switch ($prop) {
1886
                case 'id-at-countryName':
1887
                    $desc = 'C';
1888
                    break;
1889
                case 'id-at-stateOrProvinceName':
1890
                    $desc = 'ST';
1891
                    break;
1892
                case 'id-at-organizationName':
1893
                    $desc = 'O';
1894
                    break;
1895
                case 'id-at-organizationalUnitName':
1896
                    $desc = 'OU';
1897
                    break;
1898
                case 'id-at-commonName':
1899
                    $desc = 'CN';
1900
                    break;
1901
                case 'id-at-localityName':
1902
                    $desc = 'L';
1903
                    break;
1904
                case 'id-at-surname':
1905
                    $desc = 'SN';
1906
                    break;
1907
                case 'id-at-uniqueIdentifier':
1908
                    $delim = '/';
1909
                    $desc = 'x500UniqueIdentifier';
1910
                    break;
1911
                case 'id-at-postalAddress':
1912
                    $delim = '/';
1913
                    $desc = 'postalAddress';
1914
                    break;
1915
                default:
1916
                    $delim = '/';
1917
                    $desc = preg_replace('#.+-([^-]+)$#', '$1', $prop);
1918
            }
1919
 
1920
            if (!$start) {
1921
                $output .= $delim;
1922
            }
1923
            if (is_array($value)) {
1924
                foreach ($value as $type => $v) {
1925
                    $type = array_search($type, ASN1::ANY_MAP, true);
1926
                    if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) {
1927
                        $v = ASN1::convert($v, $type);
1928
                        if ($v !== false) {
1929
                            $value = $v;
1930
                            break;
1931
                        }
1932
                    }
1933
                }
1934
                if (is_array($value)) {
1935
                    $value = array_pop($value); // Always strip data type.
1936
                }
1937
            } elseif (is_object($value) && $value instanceof Element) {
1938
                $callback = function ($x) {
1939
                    return '\x' . bin2hex($x[0]);
1940
                };
1941
                $value = strtoupper(preg_replace_callback('#[^\x20-\x7E]#', $callback, $value->element));
1942
            }
1943
            $output .= $desc . '=' . $value;
1944
            $result[$desc] = isset($result[$desc]) ?
1945
                array_merge((array) $result[$desc], [$value]) :
1946
                $value;
1947
            $start = false;
1948
        }
1949
 
1950
        return $format == self::DN_OPENSSL ? $result : $output;
1951
    }
1952
 
1953
    /**
1954
     * Get the Distinguished Name for a certificate/crl issuer
1955
     *
1956
     * @param int $format optional
1957
     * @return mixed
1958
     */
1959
    public function getIssuerDN($format = self::DN_ARRAY)
1960
    {
1961
        switch (true) {
1962
            case !isset($this->currentCert) || !is_array($this->currentCert):
1963
                break;
1964
            case isset($this->currentCert['tbsCertificate']):
1965
                return $this->getDN($format, $this->currentCert['tbsCertificate']['issuer']);
1966
            case isset($this->currentCert['tbsCertList']):
1967
                return $this->getDN($format, $this->currentCert['tbsCertList']['issuer']);
1968
        }
1969
 
1970
        return false;
1971
    }
1972
 
1973
    /**
1974
     * Get the Distinguished Name for a certificate/csr subject
1975
     * Alias of getDN()
1976
     *
1977
     * @param int $format optional
1978
     * @return mixed
1979
     */
1980
    public function getSubjectDN($format = self::DN_ARRAY)
1981
    {
1982
        switch (true) {
1983
            case !empty($this->dn):
1984
                return $this->getDN($format);
1985
            case !isset($this->currentCert) || !is_array($this->currentCert):
1986
                break;
1987
            case isset($this->currentCert['tbsCertificate']):
1988
                return $this->getDN($format, $this->currentCert['tbsCertificate']['subject']);
1989
            case isset($this->currentCert['certificationRequestInfo']):
1990
                return $this->getDN($format, $this->currentCert['certificationRequestInfo']['subject']);
1991
        }
1992
 
1993
        return false;
1994
    }
1995
 
1996
    /**
1997
     * Get an individual Distinguished Name property for a certificate/crl issuer
1998
     *
1999
     * @param string $propName
2000
     * @param bool $withType optional
2001
     * @return mixed
2002
     */
2003
    public function getIssuerDNProp($propName, $withType = false)
2004
    {
2005
        switch (true) {
2006
            case !isset($this->currentCert) || !is_array($this->currentCert):
2007
                break;
2008
            case isset($this->currentCert['tbsCertificate']):
2009
                return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['issuer'], $withType);
2010
            case isset($this->currentCert['tbsCertList']):
2011
                return $this->getDNProp($propName, $this->currentCert['tbsCertList']['issuer'], $withType);
2012
        }
2013
 
2014
        return false;
2015
    }
2016
 
2017
    /**
2018
     * Get an individual Distinguished Name property for a certificate/csr subject
2019
     *
2020
     * @param string $propName
2021
     * @param bool $withType optional
2022
     * @return mixed
2023
     */
2024
    public function getSubjectDNProp($propName, $withType = false)
2025
    {
2026
        switch (true) {
2027
            case !empty($this->dn):
2028
                return $this->getDNProp($propName, null, $withType);
2029
            case !isset($this->currentCert) || !is_array($this->currentCert):
2030
                break;
2031
            case isset($this->currentCert['tbsCertificate']):
2032
                return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['subject'], $withType);
2033
            case isset($this->currentCert['certificationRequestInfo']):
2034
                return $this->getDNProp($propName, $this->currentCert['certificationRequestInfo']['subject'], $withType);
2035
        }
2036
 
2037
        return false;
2038
    }
2039
 
2040
    /**
2041
     * Get the certificate chain for the current cert
2042
     *
2043
     * @return mixed
2044
     */
2045
    public function getChain()
2046
    {
2047
        $chain = [$this->currentCert];
2048
 
2049
        if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) {
2050
            return false;
2051
        }
2052
        while (true) {
2053
            $currentCert = $chain[count($chain) - 1];
2054
            for ($i = 0; $i < count($this->CAs); $i++) {
2055
                $ca = $this->CAs[$i];
2056
                if ($currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']) {
2057
                    $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier', $currentCert);
2058
                    $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca);
2059
                    switch (true) {
2060
                        case !is_array($authorityKey):
2061
                        case is_array($authorityKey) && isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
2062
                            if ($currentCert === $ca) {
2063
                                break 3;
2064
                            }
2065
                            $chain[] = $ca;
2066
                            break 2;
2067
                    }
2068
                }
2069
            }
2070
            if ($i == count($this->CAs)) {
2071
                break;
2072
            }
2073
        }
2074
        foreach ($chain as $key => $value) {
2075
            $chain[$key] = new X509();
2076
            $chain[$key]->loadX509($value);
2077
        }
2078
        return $chain;
2079
    }
2080
 
2081
    /**
2082
     * Returns the current cert
2083
     *
2084
     * @return array|bool
2085
     */
2086
    public function &getCurrentCert()
2087
    {
2088
        return $this->currentCert;
2089
    }
2090
 
2091
    /**
2092
     * Set public key
2093
     *
2094
     * Key needs to be a \phpseclib3\Crypt\RSA object
2095
     *
2096
     * @param PublicKey $key
2097
     * @return void
2098
     */
2099
    public function setPublicKey(PublicKey $key)
2100
    {
2101
        $this->publicKey = $key;
2102
    }
2103
 
2104
    /**
2105
     * Set private key
2106
     *
2107
     * Key needs to be a \phpseclib3\Crypt\RSA object
2108
     *
2109
     * @param PrivateKey $key
2110
     */
2111
    public function setPrivateKey(PrivateKey $key)
2112
    {
2113
        $this->privateKey = $key;
2114
    }
2115
 
2116
    /**
2117
     * Set challenge
2118
     *
2119
     * Used for SPKAC CSR's
2120
     *
2121
     * @param string $challenge
2122
     */
2123
    public function setChallenge($challenge)
2124
    {
2125
        $this->challenge = $challenge;
2126
    }
2127
 
2128
    /**
2129
     * Gets the public key
2130
     *
2131
     * Returns a \phpseclib3\Crypt\RSA object or a false.
2132
     *
2133
     * @return mixed
2134
     */
2135
    public function getPublicKey()
2136
    {
2137
        if (isset($this->publicKey)) {
2138
            return $this->publicKey;
2139
        }
2140
 
2141
        if (isset($this->currentCert) && is_array($this->currentCert)) {
2142
            $paths = [
2143
                'tbsCertificate/subjectPublicKeyInfo',
2144
                'certificationRequestInfo/subjectPKInfo',
2145
                'publicKeyAndChallenge/spki'
2146
            ];
2147
            foreach ($paths as $path) {
2148
                $keyinfo = $this->subArray($this->currentCert, $path);
2149
                if (!empty($keyinfo)) {
2150
                    break;
2151
                }
2152
            }
2153
        }
2154
        if (empty($keyinfo)) {
2155
            return false;
2156
        }
2157
 
2158
        $key = $keyinfo['subjectPublicKey'];
2159
 
2160
        switch ($keyinfo['algorithm']['algorithm']) {
2161
            case 'id-RSASSA-PSS':
2162
                return RSA::loadFormat('PSS', $key);
2163
            case 'rsaEncryption':
2164
                return RSA::loadFormat('PKCS8', $key)->withPadding(RSA::SIGNATURE_PKCS1);
2165
            case 'id-ecPublicKey':
2166
            case 'id-Ed25519':
2167
            case 'id-Ed448':
2168
                return EC::loadFormat('PKCS8', $key);
2169
            case 'id-dsa':
2170
                return DSA::loadFormat('PKCS8', $key);
2171
        }
2172
 
2173
        return false;
2174
    }
2175
 
2176
    /**
2177
     * Load a Certificate Signing Request
2178
     *
2179
     * @param string $csr
2180
     * @param int $mode
2181
     * @return mixed
2182
     */
2183
    public function loadCSR($csr, $mode = self::FORMAT_AUTO_DETECT)
2184
    {
2185
        if (is_array($csr) && isset($csr['certificationRequestInfo'])) {
2186
            unset($this->currentCert);
2187
            unset($this->currentKeyIdentifier);
2188
            unset($this->signatureSubject);
2189
            $this->dn = $csr['certificationRequestInfo']['subject'];
2190
            if (!isset($this->dn)) {
2191
                return false;
2192
            }
2193
 
2194
            $this->currentCert = $csr;
2195
            return $csr;
2196
        }
2197
 
2198
        // see http://tools.ietf.org/html/rfc2986
2199
 
2200
        if ($mode != self::FORMAT_DER) {
2201
            $newcsr = ASN1::extractBER($csr);
2202
            if ($mode == self::FORMAT_PEM && $csr == $newcsr) {
2203
                return false;
2204
            }
2205
            $csr = $newcsr;
2206
        }
2207
        $orig = $csr;
2208
 
2209
        if ($csr === false) {
2210
            $this->currentCert = false;
2211
            return false;
2212
        }
2213
 
2214
        $decoded = ASN1::decodeBER($csr);
2215
 
1042 daniel-mar 2216
        if (!$decoded) {
827 daniel-mar 2217
            $this->currentCert = false;
2218
            return false;
2219
        }
2220
 
2221
        $csr = ASN1::asn1map($decoded[0], Maps\CertificationRequest::MAP);
2222
        if (!isset($csr) || $csr === false) {
2223
            $this->currentCert = false;
2224
            return false;
2225
        }
2226
 
2227
        $this->mapInAttributes($csr, 'certificationRequestInfo/attributes');
2228
        $this->mapInDNs($csr, 'certificationRequestInfo/subject/rdnSequence');
2229
 
2230
        $this->dn = $csr['certificationRequestInfo']['subject'];
2231
 
2232
        $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']);
2233
 
2234
        $key = $csr['certificationRequestInfo']['subjectPKInfo'];
2235
        $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP);
2236
        $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'] =
2237
            "-----BEGIN PUBLIC KEY-----\r\n" .
2238
            chunk_split(base64_encode($key), 64) .
2239
            "-----END PUBLIC KEY-----";
2240
 
2241
        $this->currentKeyIdentifier = null;
2242
        $this->currentCert = $csr;
2243
 
2244
        $this->publicKey = null;
2245
        $this->publicKey = $this->getPublicKey();
2246
 
2247
        return $csr;
2248
    }
2249
 
2250
    /**
2251
     * Save CSR request
2252
     *
2253
     * @param array $csr
2254
     * @param int $format optional
2255
     * @return string
2256
     */
1042 daniel-mar 2257
    public function saveCSR(array $csr, $format = self::FORMAT_PEM)
827 daniel-mar 2258
    {
2259
        if (!is_array($csr) || !isset($csr['certificationRequestInfo'])) {
2260
            return false;
2261
        }
2262
 
2263
        switch (true) {
2264
            case !($algorithm = $this->subArray($csr, 'certificationRequestInfo/subjectPKInfo/algorithm/algorithm')):
2265
            case is_object($csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']):
2266
                break;
2267
            default:
2268
                $csr['certificationRequestInfo']['subjectPKInfo'] = new Element(
2269
                    base64_decode(preg_replace('#-.+-|[\r\n]#', '', $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']))
2270
                );
2271
        }
2272
 
2273
        $filters = [];
2274
        $filters['certificationRequestInfo']['subject']['rdnSequence']['value']
2275
            = ['type' => ASN1::TYPE_UTF8_STRING];
2276
 
2277
        ASN1::setFilters($filters);
2278
 
2279
        $this->mapOutDNs($csr, 'certificationRequestInfo/subject/rdnSequence');
2280
        $this->mapOutAttributes($csr, 'certificationRequestInfo/attributes');
2281
        $csr = ASN1::encodeDER($csr, Maps\CertificationRequest::MAP);
2282
 
2283
        switch ($format) {
2284
            case self::FORMAT_DER:
2285
                return $csr;
2286
            // case self::FORMAT_PEM:
2287
            default:
1042 daniel-mar 2288
                return "-----BEGIN CERTIFICATE REQUEST-----\r\n" . chunk_split(Strings::base64_encode($csr), 64) . '-----END CERTIFICATE REQUEST-----';
827 daniel-mar 2289
        }
2290
    }
2291
 
2292
    /**
2293
     * Load a SPKAC CSR
2294
     *
2295
     * SPKAC's are produced by the HTML5 keygen element:
2296
     *
2297
     * https://developer.mozilla.org/en-US/docs/HTML/Element/keygen
2298
     *
2299
     * @param string $spkac
2300
     * @return mixed
2301
     */
2302
    public function loadSPKAC($spkac)
2303
    {
2304
        if (is_array($spkac) && isset($spkac['publicKeyAndChallenge'])) {
2305
            unset($this->currentCert);
2306
            unset($this->currentKeyIdentifier);
2307
            unset($this->signatureSubject);
2308
            $this->currentCert = $spkac;
2309
            return $spkac;
2310
        }
2311
 
2312
        // see http://www.w3.org/html/wg/drafts/html/master/forms.html#signedpublickeyandchallenge
2313
 
2314
        // OpenSSL produces SPKAC's that are preceded by the string SPKAC=
2315
        $temp = preg_replace('#(?:SPKAC=)|[ \r\n\\\]#', '', $spkac);
1042 daniel-mar 2316
        $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? Strings::base64_decode($temp) : false;
827 daniel-mar 2317
        if ($temp != false) {
2318
            $spkac = $temp;
2319
        }
2320
        $orig = $spkac;
2321
 
2322
        if ($spkac === false) {
2323
            $this->currentCert = false;
2324
            return false;
2325
        }
2326
 
2327
        $decoded = ASN1::decodeBER($spkac);
2328
 
1042 daniel-mar 2329
        if (!$decoded) {
827 daniel-mar 2330
            $this->currentCert = false;
2331
            return false;
2332
        }
2333
 
2334
        $spkac = ASN1::asn1map($decoded[0], Maps\SignedPublicKeyAndChallenge::MAP);
2335
 
2336
        if (!isset($spkac) || !is_array($spkac)) {
2337
            $this->currentCert = false;
2338
            return false;
2339
        }
2340
 
2341
        $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']);
2342
 
2343
        $key = $spkac['publicKeyAndChallenge']['spki'];
2344
        $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP);
2345
        $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey'] =
2346
            "-----BEGIN PUBLIC KEY-----\r\n" .
2347
            chunk_split(base64_encode($key), 64) .
2348
            "-----END PUBLIC KEY-----";
2349
 
2350
        $this->currentKeyIdentifier = null;
2351
        $this->currentCert = $spkac;
2352
 
2353
        $this->publicKey = null;
2354
        $this->publicKey = $this->getPublicKey();
2355
 
2356
        return $spkac;
2357
    }
2358
 
2359
    /**
2360
     * Save a SPKAC CSR request
2361
     *
2362
     * @param array $spkac
2363
     * @param int $format optional
2364
     * @return string
2365
     */
1042 daniel-mar 2366
    public function saveSPKAC(array $spkac, $format = self::FORMAT_PEM)
827 daniel-mar 2367
    {
2368
        if (!is_array($spkac) || !isset($spkac['publicKeyAndChallenge'])) {
2369
            return false;
2370
        }
2371
 
2372
        $algorithm = $this->subArray($spkac, 'publicKeyAndChallenge/spki/algorithm/algorithm');
2373
        switch (true) {
2374
            case !$algorithm:
2375
            case is_object($spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']):
2376
                break;
2377
            default:
2378
                $spkac['publicKeyAndChallenge']['spki'] = new Element(
2379
                    base64_decode(preg_replace('#-.+-|[\r\n]#', '', $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']))
2380
                );
2381
        }
2382
 
2383
        $spkac = ASN1::encodeDER($spkac, Maps\SignedPublicKeyAndChallenge::MAP);
2384
 
2385
        switch ($format) {
2386
            case self::FORMAT_DER:
2387
                return $spkac;
2388
            // case self::FORMAT_PEM:
2389
            default:
2390
                // OpenSSL's implementation of SPKAC requires the SPKAC be preceded by SPKAC= and since there are pretty much
2391
                // no other SPKAC decoders phpseclib will use that same format
1042 daniel-mar 2392
                return 'SPKAC=' . Strings::base64_encode($spkac);
827 daniel-mar 2393
        }
2394
    }
2395
 
2396
    /**
2397
     * Load a Certificate Revocation List
2398
     *
2399
     * @param string $crl
2400
     * @param int $mode
2401
     * @return mixed
2402
     */
2403
    public function loadCRL($crl, $mode = self::FORMAT_AUTO_DETECT)
2404
    {
2405
        if (is_array($crl) && isset($crl['tbsCertList'])) {
2406
            $this->currentCert = $crl;
2407
            unset($this->signatureSubject);
2408
            return $crl;
2409
        }
2410
 
2411
        if ($mode != self::FORMAT_DER) {
2412
            $newcrl = ASN1::extractBER($crl);
2413
            if ($mode == self::FORMAT_PEM && $crl == $newcrl) {
2414
                return false;
2415
            }
2416
            $crl = $newcrl;
2417
        }
2418
        $orig = $crl;
2419
 
2420
        if ($crl === false) {
2421
            $this->currentCert = false;
2422
            return false;
2423
        }
2424
 
2425
        $decoded = ASN1::decodeBER($crl);
2426
 
1042 daniel-mar 2427
        if (!$decoded) {
827 daniel-mar 2428
            $this->currentCert = false;
2429
            return false;
2430
        }
2431
 
2432
        $crl = ASN1::asn1map($decoded[0], Maps\CertificateList::MAP);
2433
        if (!isset($crl) || $crl === false) {
2434
            $this->currentCert = false;
2435
            return false;
2436
        }
2437
 
2438
        $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']);
2439
 
2440
        $this->mapInDNs($crl, 'tbsCertList/issuer/rdnSequence');
2441
        if ($this->isSubArrayValid($crl, 'tbsCertList/crlExtensions')) {
2442
            $this->mapInExtensions($crl, 'tbsCertList/crlExtensions');
2443
        }
2444
        if ($this->isSubArrayValid($crl, 'tbsCertList/revokedCertificates')) {
2445
            $rclist_ref = &$this->subArrayUnchecked($crl, 'tbsCertList/revokedCertificates');
2446
            if ($rclist_ref) {
2447
                $rclist = $crl['tbsCertList']['revokedCertificates'];
2448
                foreach ($rclist as $i => $extension) {
2449
                    if ($this->isSubArrayValid($rclist, "$i/crlEntryExtensions")) {
2450
                        $this->mapInExtensions($rclist_ref, "$i/crlEntryExtensions");
2451
                    }
2452
                }
2453
            }
2454
        }
2455
 
2456
        $this->currentKeyIdentifier = null;
2457
        $this->currentCert = $crl;
2458
 
2459
        return $crl;
2460
    }
2461
 
2462
    /**
2463
     * Save Certificate Revocation List.
2464
     *
2465
     * @param array $crl
2466
     * @param int $format optional
2467
     * @return string
2468
     */
1042 daniel-mar 2469
    public function saveCRL(array $crl, $format = self::FORMAT_PEM)
827 daniel-mar 2470
    {
2471
        if (!is_array($crl) || !isset($crl['tbsCertList'])) {
2472
            return false;
2473
        }
2474
 
2475
        $filters = [];
2476
        $filters['tbsCertList']['issuer']['rdnSequence']['value']
2477
            = ['type' => ASN1::TYPE_UTF8_STRING];
2478
        $filters['tbsCertList']['signature']['parameters']
2479
            = ['type' => ASN1::TYPE_UTF8_STRING];
2480
        $filters['signatureAlgorithm']['parameters']
2481
            = ['type' => ASN1::TYPE_UTF8_STRING];
2482
 
2483
        if (empty($crl['tbsCertList']['signature']['parameters'])) {
2484
            $filters['tbsCertList']['signature']['parameters']
2485
                = ['type' => ASN1::TYPE_NULL];
2486
        }
2487
 
2488
        if (empty($crl['signatureAlgorithm']['parameters'])) {
2489
            $filters['signatureAlgorithm']['parameters']
2490
                = ['type' => ASN1::TYPE_NULL];
2491
        }
2492
 
2493
        ASN1::setFilters($filters);
2494
 
2495
        $this->mapOutDNs($crl, 'tbsCertList/issuer/rdnSequence');
2496
        $this->mapOutExtensions($crl, 'tbsCertList/crlExtensions');
2497
        $rclist = &$this->subArray($crl, 'tbsCertList/revokedCertificates');
2498
        if (is_array($rclist)) {
2499
            foreach ($rclist as $i => $extension) {
2500
                $this->mapOutExtensions($rclist, "$i/crlEntryExtensions");
2501
            }
2502
        }
2503
 
2504
        $crl = ASN1::encodeDER($crl, Maps\CertificateList::MAP);
2505
 
2506
        switch ($format) {
2507
            case self::FORMAT_DER:
2508
                return $crl;
2509
            // case self::FORMAT_PEM:
2510
            default:
1042 daniel-mar 2511
                return "-----BEGIN X509 CRL-----\r\n" . chunk_split(Strings::base64_encode($crl), 64) . '-----END X509 CRL-----';
827 daniel-mar 2512
        }
2513
    }
2514
 
2515
    /**
2516
     * Helper function to build a time field according to RFC 3280 section
2517
     *  - 4.1.2.5 Validity
2518
     *  - 5.1.2.4 This Update
2519
     *  - 5.1.2.5 Next Update
2520
     *  - 5.1.2.6 Revoked Certificates
2521
     * by choosing utcTime iff year of date given is before 2050 and generalTime else.
2522
     *
2523
     * @param string $date in format date('D, d M Y H:i:s O')
2524
     * @return array|Element
2525
     */
2526
    private function timeField($date)
2527
    {
2528
        if ($date instanceof Element) {
2529
            return $date;
2530
        }
2531
        $dateObj = new \DateTimeImmutable($date, new \DateTimeZone('GMT'));
2532
        $year = $dateObj->format('Y'); // the same way ASN1.php parses this
2533
        if ($year < 2050) {
2534
            return ['utcTime' => $date];
2535
        } else {
2536
            return ['generalTime' => $date];
2537
        }
2538
    }
2539
 
2540
    /**
2541
     * Sign an X.509 certificate
2542
     *
2543
     * $issuer's private key needs to be loaded.
2544
     * $subject can be either an existing X.509 cert (if you want to resign it),
2545
     * a CSR or something with the DN and public key explicitly set.
2546
     *
2547
     * @return mixed
2548
     */
1042 daniel-mar 2549
    public function sign(X509 $issuer, X509 $subject)
827 daniel-mar 2550
    {
2551
        if (!is_object($issuer->privateKey) || empty($issuer->dn)) {
2552
            return false;
2553
        }
2554
 
2555
        if (isset($subject->publicKey) && !($subjectPublicKey = $subject->formatSubjectPublicKey())) {
2556
            return false;
2557
        }
2558
 
2559
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2560
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2561
        $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey);
2562
 
2563
        if (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertificate'])) {
2564
            $this->currentCert = $subject->currentCert;
2565
            $this->currentCert['tbsCertificate']['signature'] = $signatureAlgorithm;
2566
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
2567
 
2568
            if (!empty($this->startDate)) {
2569
                $this->currentCert['tbsCertificate']['validity']['notBefore'] = $this->timeField($this->startDate);
2570
            }
2571
            if (!empty($this->endDate)) {
2572
                $this->currentCert['tbsCertificate']['validity']['notAfter'] = $this->timeField($this->endDate);
2573
            }
2574
            if (!empty($this->serialNumber)) {
2575
                $this->currentCert['tbsCertificate']['serialNumber'] = $this->serialNumber;
2576
            }
2577
            if (!empty($subject->dn)) {
2578
                $this->currentCert['tbsCertificate']['subject'] = $subject->dn;
2579
            }
2580
            if (!empty($subject->publicKey)) {
2581
                $this->currentCert['tbsCertificate']['subjectPublicKeyInfo'] = $subjectPublicKey;
2582
            }
2583
            $this->removeExtension('id-ce-authorityKeyIdentifier');
2584
            if (isset($subject->domains)) {
2585
                $this->removeExtension('id-ce-subjectAltName');
2586
            }
2587
        } elseif (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertList'])) {
2588
            return false;
2589
        } else {
2590
            if (!isset($subject->publicKey)) {
2591
                return false;
2592
            }
2593
 
2594
            $startDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
2595
            $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O');
2596
 
2597
            $endDate = new \DateTimeImmutable('+1 year', new \DateTimeZone(@date_default_timezone_get()));
2598
            $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O');
2599
 
2600
            /* "The serial number MUST be a positive integer"
2601
               "Conforming CAs MUST NOT use serialNumber values longer than 20 octets."
2602
                -- https://tools.ietf.org/html/rfc5280#section-4.1.2.2
2603
 
2604
               for the integer to be positive the leading bit needs to be 0 hence the
2605
               application of a bitmap
2606
            */
2607
            $serialNumber = !empty($this->serialNumber) ?
2608
                $this->serialNumber :
2609
                new BigInteger(Random::string(20) & ("\x7F" . str_repeat("\xFF", 19)), 256);
2610
 
2611
            $this->currentCert = [
2612
                'tbsCertificate' =>
2613
                    [
2614
                        'version' => 'v3',
2615
                        'serialNumber' => $serialNumber, // $this->setSerialNumber()
2616
                        'signature' => $signatureAlgorithm,
2617
                        'issuer' => false, // this is going to be overwritten later
2618
                        'validity' => [
2619
                            'notBefore' => $this->timeField($startDate), // $this->setStartDate()
2620
                            'notAfter' => $this->timeField($endDate)   // $this->setEndDate()
2621
                        ],
2622
                        'subject' => $subject->dn,
2623
                        'subjectPublicKeyInfo' => $subjectPublicKey
2624
                    ],
2625
                    'signatureAlgorithm' => $signatureAlgorithm,
2626
                    'signature'          => false // this is going to be overwritten later
2627
            ];
2628
 
2629
            // Copy extensions from CSR.
2630
            $csrexts = $subject->getAttribute('pkcs-9-at-extensionRequest', 0);
2631
 
2632
            if (!empty($csrexts)) {
2633
                $this->currentCert['tbsCertificate']['extensions'] = $csrexts;
2634
            }
2635
        }
2636
 
2637
        $this->currentCert['tbsCertificate']['issuer'] = $issuer->dn;
2638
 
2639
        if (isset($issuer->currentKeyIdentifier)) {
2640
            $this->setExtension('id-ce-authorityKeyIdentifier', [
2641
                    //'authorityCertIssuer' => array(
2642
                    //    array(
2643
                    //        'directoryName' => $issuer->dn
2644
                    //    )
2645
                    //),
2646
                    'keyIdentifier' => $issuer->currentKeyIdentifier
2647
                ]);
2648
            //$extensions = &$this->currentCert['tbsCertificate']['extensions'];
2649
            //if (isset($issuer->serialNumber)) {
2650
            //    $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber;
2651
            //}
2652
            //unset($extensions);
2653
        }
2654
 
2655
        if (isset($subject->currentKeyIdentifier)) {
2656
            $this->setExtension('id-ce-subjectKeyIdentifier', $subject->currentKeyIdentifier);
2657
        }
2658
 
2659
        $altName = [];
2660
 
2661
        if (isset($subject->domains) && count($subject->domains)) {
2662
            $altName = array_map(['\phpseclib3\File\X509', 'dnsName'], $subject->domains);
2663
        }
2664
 
2665
        if (isset($subject->ipAddresses) && count($subject->ipAddresses)) {
2666
            // should an IP address appear as the CN if no domain name is specified? idk
2667
            //$ips = count($subject->domains) ? $subject->ipAddresses : array_slice($subject->ipAddresses, 1);
2668
            $ipAddresses = [];
2669
            foreach ($subject->ipAddresses as $ipAddress) {
2670
                $encoded = $subject->ipAddress($ipAddress);
2671
                if ($encoded !== false) {
2672
                    $ipAddresses[] = $encoded;
2673
                }
2674
            }
2675
            if (count($ipAddresses)) {
2676
                $altName = array_merge($altName, $ipAddresses);
2677
            }
2678
        }
2679
 
2680
        if (!empty($altName)) {
2681
            $this->setExtension('id-ce-subjectAltName', $altName);
2682
        }
2683
 
2684
        if ($this->caFlag) {
2685
            $keyUsage = $this->getExtension('id-ce-keyUsage');
2686
            if (!$keyUsage) {
2687
                $keyUsage = [];
2688
            }
2689
 
2690
            $this->setExtension(
2691
                'id-ce-keyUsage',
2692
                array_values(array_unique(array_merge($keyUsage, ['cRLSign', 'keyCertSign'])))
2693
            );
2694
 
2695
            $basicConstraints = $this->getExtension('id-ce-basicConstraints');
2696
            if (!$basicConstraints) {
2697
                $basicConstraints = [];
2698
            }
2699
 
2700
            $this->setExtension(
2701
                'id-ce-basicConstraints',
2702
                array_merge(['cA' => true], $basicConstraints),
2703
                true
2704
            );
2705
 
2706
            if (!isset($subject->currentKeyIdentifier)) {
2707
                $this->setExtension('id-ce-subjectKeyIdentifier', $this->computeKeyIdentifier($this->currentCert), false, false);
2708
            }
2709
        }
2710
 
2711
        // resync $this->signatureSubject
2712
        // save $tbsCertificate in case there are any \phpseclib3\File\ASN1\Element objects in it
2713
        $tbsCertificate = $this->currentCert['tbsCertificate'];
2714
        $this->loadX509($this->saveX509($this->currentCert));
2715
 
2716
        $result = $this->currentCert;
2717
        $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject);
2718
        $result['tbsCertificate'] = $tbsCertificate;
2719
 
2720
        $this->currentCert = $currentCert;
2721
        $this->signatureSubject = $signatureSubject;
2722
 
2723
        return $result;
2724
    }
2725
 
2726
    /**
2727
     * Sign a CSR
2728
     *
2729
     * @return mixed
2730
     */
2731
    public function signCSR()
2732
    {
2733
        if (!is_object($this->privateKey) || empty($this->dn)) {
2734
            return false;
2735
        }
2736
 
2737
        $origPublicKey = $this->publicKey;
2738
        $this->publicKey = $this->privateKey->getPublicKey();
2739
        $publicKey = $this->formatSubjectPublicKey();
2740
        $this->publicKey = $origPublicKey;
2741
 
2742
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2743
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2744
        $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey);
2745
 
2746
        if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['certificationRequestInfo'])) {
1042 daniel-mar 2747
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
827 daniel-mar 2748
            if (!empty($this->dn)) {
2749
                $this->currentCert['certificationRequestInfo']['subject'] = $this->dn;
2750
            }
2751
            $this->currentCert['certificationRequestInfo']['subjectPKInfo'] = $publicKey;
2752
        } else {
2753
            $this->currentCert = [
2754
                'certificationRequestInfo' =>
2755
                    [
2756
                        'version' => 'v1',
2757
                        'subject' => $this->dn,
2758
                        'subjectPKInfo' => $publicKey
2759
                    ],
1042 daniel-mar 2760
                    'signatureAlgorithm' => $signatureAlgorithm,
827 daniel-mar 2761
                    'signature'          => false // this is going to be overwritten later
2762
            ];
2763
        }
2764
 
2765
        // resync $this->signatureSubject
2766
        // save $certificationRequestInfo in case there are any \phpseclib3\File\ASN1\Element objects in it
2767
        $certificationRequestInfo = $this->currentCert['certificationRequestInfo'];
2768
        $this->loadCSR($this->saveCSR($this->currentCert));
2769
 
2770
        $result = $this->currentCert;
2771
        $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject);
2772
        $result['certificationRequestInfo'] = $certificationRequestInfo;
2773
 
2774
        $this->currentCert = $currentCert;
2775
        $this->signatureSubject = $signatureSubject;
2776
 
2777
        return $result;
2778
    }
2779
 
2780
    /**
2781
     * Sign a SPKAC
2782
     *
2783
     * @return mixed
2784
     */
2785
    public function signSPKAC()
2786
    {
2787
        if (!is_object($this->privateKey)) {
2788
            return false;
2789
        }
2790
 
2791
        $origPublicKey = $this->publicKey;
2792
        $this->publicKey = $this->privateKey->getPublicKey();
2793
        $publicKey = $this->formatSubjectPublicKey();
2794
        $this->publicKey = $origPublicKey;
2795
 
2796
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2797
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2798
        $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey);
2799
 
2800
        // re-signing a SPKAC seems silly but since everything else supports re-signing why not?
2801
        if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['publicKeyAndChallenge'])) {
1042 daniel-mar 2802
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
827 daniel-mar 2803
            $this->currentCert['publicKeyAndChallenge']['spki'] = $publicKey;
2804
            if (!empty($this->challenge)) {
2805
                // the bitwise AND ensures that the output is a valid IA5String
2806
                $this->currentCert['publicKeyAndChallenge']['challenge'] = $this->challenge & str_repeat("\x7F", strlen($this->challenge));
2807
            }
2808
        } else {
2809
            $this->currentCert = [
2810
                'publicKeyAndChallenge' =>
2811
                    [
2812
                        'spki' => $publicKey,
2813
                        // quoting <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen>,
2814
                        // "A challenge string that is submitted along with the public key. Defaults to an empty string if not specified."
2815
                        // both Firefox and OpenSSL ("openssl spkac -key private.key") behave this way
2816
                        // we could alternatively do this instead if we ignored the specs:
2817
                        // Random::string(8) & str_repeat("\x7F", 8)
2818
                        'challenge' => !empty($this->challenge) ? $this->challenge : ''
2819
                    ],
1042 daniel-mar 2820
                    'signatureAlgorithm' => $signatureAlgorithm,
827 daniel-mar 2821
                    'signature'          => false // this is going to be overwritten later
2822
            ];
2823
        }
2824
 
2825
        // resync $this->signatureSubject
2826
        // save $publicKeyAndChallenge in case there are any \phpseclib3\File\ASN1\Element objects in it
2827
        $publicKeyAndChallenge = $this->currentCert['publicKeyAndChallenge'];
2828
        $this->loadSPKAC($this->saveSPKAC($this->currentCert));
2829
 
2830
        $result = $this->currentCert;
2831
        $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject);
2832
        $result['publicKeyAndChallenge'] = $publicKeyAndChallenge;
2833
 
2834
        $this->currentCert = $currentCert;
2835
        $this->signatureSubject = $signatureSubject;
2836
 
2837
        return $result;
2838
    }
2839
 
2840
    /**
2841
     * Sign a CRL
2842
     *
2843
     * $issuer's private key needs to be loaded.
2844
     *
2845
     * @return mixed
2846
     */
1042 daniel-mar 2847
    public function signCRL(X509 $issuer, X509 $crl)
827 daniel-mar 2848
    {
2849
        if (!is_object($issuer->privateKey) || empty($issuer->dn)) {
2850
            return false;
2851
        }
2852
 
2853
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2854
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2855
        $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey);
2856
 
2857
        $thisUpdate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
2858
        $thisUpdate = !empty($this->startDate) ? $this->startDate : $thisUpdate->format('D, d M Y H:i:s O');
2859
 
2860
        if (isset($crl->currentCert) && is_array($crl->currentCert) && isset($crl->currentCert['tbsCertList'])) {
2861
            $this->currentCert = $crl->currentCert;
1042 daniel-mar 2862
            $this->currentCert['tbsCertList']['signature'] = $signatureAlgorithm;
2863
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
827 daniel-mar 2864
        } else {
2865
            $this->currentCert = [
2866
                'tbsCertList' =>
2867
                    [
2868
                        'version' => 'v2',
1042 daniel-mar 2869
                        'signature' => $signatureAlgorithm,
827 daniel-mar 2870
                        'issuer' => false, // this is going to be overwritten later
2871
                        'thisUpdate' => $this->timeField($thisUpdate) // $this->setStartDate()
2872
                    ],
1042 daniel-mar 2873
                    'signatureAlgorithm' => $signatureAlgorithm,
827 daniel-mar 2874
                    'signature'          => false // this is going to be overwritten later
2875
            ];
2876
        }
2877
 
2878
        $tbsCertList = &$this->currentCert['tbsCertList'];
2879
        $tbsCertList['issuer'] = $issuer->dn;
2880
        $tbsCertList['thisUpdate'] = $this->timeField($thisUpdate);
2881
 
2882
        if (!empty($this->endDate)) {
2883
            $tbsCertList['nextUpdate'] = $this->timeField($this->endDate); // $this->setEndDate()
2884
        } else {
2885
            unset($tbsCertList['nextUpdate']);
2886
        }
2887
 
2888
        if (!empty($this->serialNumber)) {
2889
            $crlNumber = $this->serialNumber;
2890
        } else {
2891
            $crlNumber = $this->getExtension('id-ce-cRLNumber');
2892
            // "The CRL number is a non-critical CRL extension that conveys a
2893
            //  monotonically increasing sequence number for a given CRL scope and
2894
            //  CRL issuer.  This extension allows users to easily determine when a
2895
            //  particular CRL supersedes another CRL."
2896
            // -- https://tools.ietf.org/html/rfc5280#section-5.2.3
2897
            $crlNumber = $crlNumber !== false ? $crlNumber->add(new BigInteger(1)) : null;
2898
        }
2899
 
2900
        $this->removeExtension('id-ce-authorityKeyIdentifier');
2901
        $this->removeExtension('id-ce-issuerAltName');
2902
 
2903
        // Be sure version >= v2 if some extension found.
2904
        $version = isset($tbsCertList['version']) ? $tbsCertList['version'] : 0;
2905
        if (!$version) {
2906
            if (!empty($tbsCertList['crlExtensions'])) {
2907
                $version = 1; // v2.
2908
            } elseif (!empty($tbsCertList['revokedCertificates'])) {
2909
                foreach ($tbsCertList['revokedCertificates'] as $cert) {
2910
                    if (!empty($cert['crlEntryExtensions'])) {
2911
                        $version = 1; // v2.
2912
                    }
2913
                }
2914
            }
2915
 
2916
            if ($version) {
2917
                $tbsCertList['version'] = $version;
2918
            }
2919
        }
2920
 
2921
        // Store additional extensions.
2922
        if (!empty($tbsCertList['version'])) { // At least v2.
2923
            if (!empty($crlNumber)) {
2924
                $this->setExtension('id-ce-cRLNumber', $crlNumber);
2925
            }
2926
 
2927
            if (isset($issuer->currentKeyIdentifier)) {
2928
                $this->setExtension('id-ce-authorityKeyIdentifier', [
2929
                        //'authorityCertIssuer' => array(
2930
                        //    ]
2931
                        //        'directoryName' => $issuer->dn
2932
                        //    ]
2933
                        //),
2934
                        'keyIdentifier' => $issuer->currentKeyIdentifier
2935
                    ]);
2936
                //$extensions = &$tbsCertList['crlExtensions'];
2937
                //if (isset($issuer->serialNumber)) {
2938
                //    $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber;
2939
                //}
2940
                //unset($extensions);
2941
            }
2942
 
2943
            $issuerAltName = $this->getExtension('id-ce-subjectAltName', $issuer->currentCert);
2944
 
2945
            if ($issuerAltName !== false) {
2946
                $this->setExtension('id-ce-issuerAltName', $issuerAltName);
2947
            }
2948
        }
2949
 
2950
        if (empty($tbsCertList['revokedCertificates'])) {
2951
            unset($tbsCertList['revokedCertificates']);
2952
        }
2953
 
2954
        unset($tbsCertList);
2955
 
2956
        // resync $this->signatureSubject
2957
        // save $tbsCertList in case there are any \phpseclib3\File\ASN1\Element objects in it
2958
        $tbsCertList = $this->currentCert['tbsCertList'];
2959
        $this->loadCRL($this->saveCRL($this->currentCert));
2960
 
2961
        $result = $this->currentCert;
2962
        $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject);
2963
        $result['tbsCertList'] = $tbsCertList;
2964
 
2965
        $this->currentCert = $currentCert;
2966
        $this->signatureSubject = $signatureSubject;
2967
 
2968
        return $result;
2969
    }
2970
 
2971
    /**
2972
     * Identify signature algorithm from key settings
2973
     *
2974
     * @param PrivateKey $key
2975
     * @throws \phpseclib3\Exception\UnsupportedAlgorithmException if the algorithm is unsupported
1042 daniel-mar 2976
     * @return array
827 daniel-mar 2977
     */
2978
    private static function identifySignatureAlgorithm(PrivateKey $key)
2979
    {
2980
        if ($key instanceof RSA) {
2981
            if ($key->getPadding() & RSA::SIGNATURE_PSS) {
1042 daniel-mar 2982
                $r = PSS::load($key->withPassword()->toString('PSS'));
2983
                return [
2984
                    'algorithm' => 'id-RSASSA-PSS',
2985
                    'parameters' => PSS::savePSSParams($r)
2986
                ];
827 daniel-mar 2987
            }
2988
            switch ($key->getHash()) {
2989
                case 'md2':
2990
                case 'md5':
2991
                case 'sha1':
2992
                case 'sha224':
2993
                case 'sha256':
2994
                case 'sha384':
2995
                case 'sha512':
1042 daniel-mar 2996
                    return ['algorithm' => $key->getHash() . 'WithRSAEncryption'];
827 daniel-mar 2997
            }
2998
            throw new UnsupportedAlgorithmException('The only supported hash algorithms for RSA are: md2, md5, sha1, sha224, sha256, sha384, sha512');
2999
        }
3000
 
3001
        if ($key instanceof DSA) {
3002
            switch ($key->getHash()) {
3003
                case 'sha1':
3004
                case 'sha224':
3005
                case 'sha256':
1042 daniel-mar 3006
                    return ['algorithm' => 'id-dsa-with-' . $key->getHash()];
827 daniel-mar 3007
            }
3008
            throw new UnsupportedAlgorithmException('The only supported hash algorithms for DSA are: sha1, sha224, sha256');
3009
        }
3010
 
3011
        if ($key instanceof EC) {
3012
            switch ($key->getCurve()) {
3013
                case 'Ed25519':
3014
                case 'Ed448':
1042 daniel-mar 3015
                    return ['algorithm' => 'id-' . $key->getCurve()];
827 daniel-mar 3016
            }
3017
            switch ($key->getHash()) {
3018
                case 'sha1':
3019
                case 'sha224':
3020
                case 'sha256':
3021
                case 'sha384':
3022
                case 'sha512':
1042 daniel-mar 3023
                    return ['algorithm' => 'ecdsa-with-' . strtoupper($key->getHash())];
827 daniel-mar 3024
            }
3025
            throw new UnsupportedAlgorithmException('The only supported hash algorithms for EC are: sha1, sha224, sha256, sha384, sha512');
3026
        }
3027
 
3028
        throw new UnsupportedAlgorithmException('The only supported public key classes are: RSA, DSA, EC');
3029
    }
3030
 
3031
    /**
3032
     * Set certificate start date
3033
     *
3034
     * @param \DateTimeInterface|string $date
3035
     */
3036
    public function setStartDate($date)
3037
    {
3038
        if (!is_object($date) || !($date instanceof \DateTimeInterface)) {
3039
            $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get()));
3040
        }
3041
 
3042
        $this->startDate = $date->format('D, d M Y H:i:s O');
3043
    }
3044
 
3045
    /**
3046
     * Set certificate end date
3047
     *
3048
     * @param \DateTimeInterface|string $date
3049
     */
3050
    public function setEndDate($date)
3051
    {
3052
        /*
3053
          To indicate that a certificate has no well-defined expiration date,
3054
          the notAfter SHOULD be assigned the GeneralizedTime value of
3055
          99991231235959Z.
3056
 
3057
          -- http://tools.ietf.org/html/rfc5280#section-4.1.2.5
3058
        */
3059
        if (is_string($date) && strtolower($date) === 'lifetime') {
3060
            $temp = '99991231235959Z';
3061
            $temp = chr(ASN1::TYPE_GENERALIZED_TIME) . ASN1::encodeLength(strlen($temp)) . $temp;
3062
            $this->endDate = new Element($temp);
3063
        } else {
3064
            if (!is_object($date) || !($date instanceof \DateTimeInterface)) {
3065
                $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get()));
3066
            }
3067
 
3068
            $this->endDate = $date->format('D, d M Y H:i:s O');
3069
        }
3070
    }
3071
 
3072
    /**
3073
     * Set Serial Number
3074
     *
3075
     * @param string $serial
3076
     * @param int $base optional
3077
     */
3078
    public function setSerialNumber($serial, $base = -256)
3079
    {
3080
        $this->serialNumber = new BigInteger($serial, $base);
3081
    }
3082
 
3083
    /**
3084
     * Turns the certificate into a certificate authority
3085
     *
3086
     */
3087
    public function makeCA()
3088
    {
3089
        $this->caFlag = true;
3090
    }
3091
 
3092
    /**
3093
     * Check for validity of subarray
3094
     *
3095
     * This is intended for use in conjunction with _subArrayUnchecked(),
3096
     * implementing the checks included in _subArray() but without copying
3097
     * a potentially large array by passing its reference by-value to is_array().
3098
     *
3099
     * @param array $root
3100
     * @param string $path
3101
     * @return boolean
3102
     */
1042 daniel-mar 3103
    private function isSubArrayValid(array $root, $path)
827 daniel-mar 3104
    {
3105
        if (!is_array($root)) {
3106
            return false;
3107
        }
3108
 
3109
        foreach (explode('/', $path) as $i) {
3110
            if (!is_array($root)) {
3111
                return false;
3112
            }
3113
 
3114
            if (!isset($root[$i])) {
3115
                return true;
3116
            }
3117
 
3118
            $root = $root[$i];
3119
        }
3120
 
3121
        return true;
3122
    }
3123
 
3124
    /**
3125
     * Get a reference to a subarray
3126
     *
3127
     * This variant of _subArray() does no is_array() checking,
3128
     * so $root should be checked with _isSubArrayValid() first.
3129
     *
3130
     * This is here for performance reasons:
3131
     * Passing a reference (i.e. $root) by-value (i.e. to is_array())
3132
     * creates a copy. If $root is an especially large array, this is expensive.
3133
     *
3134
     * @param array $root
3135
     * @param string $path  absolute path with / as component separator
3136
     * @param bool $create optional
3137
     * @return array|false
3138
     */
1042 daniel-mar 3139
    private function &subArrayUnchecked(array &$root, $path, $create = false)
827 daniel-mar 3140
    {
3141
        $false = false;
3142
 
3143
        foreach (explode('/', $path) as $i) {
3144
            if (!isset($root[$i])) {
3145
                if (!$create) {
3146
                    return $false;
3147
                }
3148
 
3149
                $root[$i] = [];
3150
            }
3151
 
3152
            $root = &$root[$i];
3153
        }
3154
 
3155
        return $root;
3156
    }
3157
 
3158
    /**
3159
     * Get a reference to a subarray
3160
     *
3161
     * @param array $root
3162
     * @param string $path  absolute path with / as component separator
3163
     * @param bool $create optional
3164
     * @return array|false
3165
     */
1042 daniel-mar 3166
    private function &subArray(array &$root = null, $path, $create = false)
827 daniel-mar 3167
    {
3168
        $false = false;
3169
 
3170
        if (!is_array($root)) {
3171
            return $false;
3172
        }
3173
 
3174
        foreach (explode('/', $path) as $i) {
3175
            if (!is_array($root)) {
3176
                return $false;
3177
            }
3178
 
3179
            if (!isset($root[$i])) {
3180
                if (!$create) {
3181
                    return $false;
3182
                }
3183
 
3184
                $root[$i] = [];
3185
            }
3186
 
3187
            $root = &$root[$i];
3188
        }
3189
 
3190
        return $root;
3191
    }
3192
 
3193
    /**
3194
     * Get a reference to an extension subarray
3195
     *
3196
     * @param array $root
3197
     * @param string $path optional absolute path with / as component separator
3198
     * @param bool $create optional
3199
     * @return array|false
3200
     */
1042 daniel-mar 3201
    private function &extensions(array &$root = null, $path = null, $create = false)
827 daniel-mar 3202
    {
3203
        if (!isset($root)) {
3204
            $root = $this->currentCert;
3205
        }
3206
 
3207
        switch (true) {
3208
            case !empty($path):
3209
            case !is_array($root):
3210
                break;
3211
            case isset($root['tbsCertificate']):
3212
                $path = 'tbsCertificate/extensions';
3213
                break;
3214
            case isset($root['tbsCertList']):
3215
                $path = 'tbsCertList/crlExtensions';
3216
                break;
3217
            case isset($root['certificationRequestInfo']):
3218
                $pth = 'certificationRequestInfo/attributes';
3219
                $attributes = &$this->subArray($root, $pth, $create);
3220
 
3221
                if (is_array($attributes)) {
3222
                    foreach ($attributes as $key => $value) {
3223
                        if ($value['type'] == 'pkcs-9-at-extensionRequest') {
3224
                            $path = "$pth/$key/value/0";
3225
                            break 2;
3226
                        }
3227
                    }
3228
                    if ($create) {
3229
                        $key = count($attributes);
3230
                        $attributes[] = ['type' => 'pkcs-9-at-extensionRequest', 'value' => []];
3231
                        $path = "$pth/$key/value/0";
3232
                    }
3233
                }
3234
                break;
3235
        }
3236
 
3237
        $extensions = &$this->subArray($root, $path, $create);
3238
 
3239
        if (!is_array($extensions)) {
3240
            $false = false;
3241
            return $false;
3242
        }
3243
 
3244
        return $extensions;
3245
    }
3246
 
3247
    /**
3248
     * Remove an Extension
3249
     *
3250
     * @param string $id
3251
     * @param string $path optional
3252
     * @return bool
3253
     */
3254
    private function removeExtensionHelper($id, $path = null)
3255
    {
3256
        $extensions = &$this->extensions($this->currentCert, $path);
3257
 
3258
        if (!is_array($extensions)) {
3259
            return false;
3260
        }
3261
 
3262
        $result = false;
3263
        foreach ($extensions as $key => $value) {
3264
            if ($value['extnId'] == $id) {
3265
                unset($extensions[$key]);
3266
                $result = true;
3267
            }
3268
        }
3269
 
3270
        $extensions = array_values($extensions);
3271
        // fix for https://bugs.php.net/75433 affecting PHP 7.2
3272
        if (!isset($extensions[0])) {
3273
            $extensions = array_splice($extensions, 0, 0);
3274
        }
3275
        return $result;
3276
    }
3277
 
3278
    /**
3279
     * Get an Extension
3280
     *
3281
     * Returns the extension if it exists and false if not
3282
     *
3283
     * @param string $id
3284
     * @param array $cert optional
3285
     * @param string $path optional
3286
     * @return mixed
3287
     */
1042 daniel-mar 3288
    private function getExtensionHelper($id, array $cert = null, $path = null)
827 daniel-mar 3289
    {
3290
        $extensions = $this->extensions($cert, $path);
3291
 
3292
        if (!is_array($extensions)) {
3293
            return false;
3294
        }
3295
 
3296
        foreach ($extensions as $key => $value) {
3297
            if ($value['extnId'] == $id) {
3298
                return $value['extnValue'];
3299
            }
3300
        }
3301
 
3302
        return false;
3303
    }
3304
 
3305
    /**
3306
     * Returns a list of all extensions in use
3307
     *
3308
     * @param array $cert optional
3309
     * @param string $path optional
3310
     * @return array
3311
     */
1042 daniel-mar 3312
    private function getExtensionsHelper(array $cert = null, $path = null)
827 daniel-mar 3313
    {
3314
        $exts = $this->extensions($cert, $path);
3315
        $extensions = [];
3316
 
3317
        if (is_array($exts)) {
3318
            foreach ($exts as $extension) {
3319
                $extensions[] = $extension['extnId'];
3320
            }
3321
        }
3322
 
3323
        return $extensions;
3324
    }
3325
 
3326
    /**
3327
     * Set an Extension
3328
     *
3329
     * @param string $id
3330
     * @param mixed $value
3331
     * @param bool $critical optional
3332
     * @param bool $replace optional
3333
     * @param string $path optional
3334
     * @return bool
3335
     */
3336
    private function setExtensionHelper($id, $value, $critical = false, $replace = true, $path = null)
3337
    {
3338
        $extensions = &$this->extensions($this->currentCert, $path, true);
3339
 
3340
        if (!is_array($extensions)) {
3341
            return false;
3342
        }
3343
 
3344
        $newext = ['extnId'  => $id, 'critical' => $critical, 'extnValue' => $value];
3345
 
3346
        foreach ($extensions as $key => $value) {
3347
            if ($value['extnId'] == $id) {
3348
                if (!$replace) {
3349
                    return false;
3350
                }
3351
 
3352
                $extensions[$key] = $newext;
3353
                return true;
3354
            }
3355
        }
3356
 
3357
        $extensions[] = $newext;
3358
        return true;
3359
    }
3360
 
3361
    /**
3362
     * Remove a certificate, CSR or CRL Extension
3363
     *
3364
     * @param string $id
3365
     * @return bool
3366
     */
3367
    public function removeExtension($id)
3368
    {
3369
        return $this->removeExtensionHelper($id);
3370
    }
3371
 
3372
    /**
3373
     * Get a certificate, CSR or CRL Extension
3374
     *
3375
     * Returns the extension if it exists and false if not
3376
     *
3377
     * @param string $id
3378
     * @param array $cert optional
3379
     * @param string $path
3380
     * @return mixed
3381
     */
1042 daniel-mar 3382
    public function getExtension($id, array $cert = null, $path = null)
827 daniel-mar 3383
    {
3384
        return $this->getExtensionHelper($id, $cert, $path);
3385
    }
3386
 
3387
    /**
3388
     * Returns a list of all extensions in use in certificate, CSR or CRL
3389
     *
3390
     * @param array $cert optional
3391
     * @param string $path optional
3392
     * @return array
3393
     */
1042 daniel-mar 3394
    public function getExtensions(array $cert = null, $path = null)
827 daniel-mar 3395
    {
3396
        return $this->getExtensionsHelper($cert, $path);
3397
    }
3398
 
3399
    /**
3400
     * Set a certificate, CSR or CRL Extension
3401
     *
3402
     * @param string $id
3403
     * @param mixed $value
3404
     * @param bool $critical optional
3405
     * @param bool $replace optional
3406
     * @return bool
3407
     */
3408
    public function setExtension($id, $value, $critical = false, $replace = true)
3409
    {
3410
        return $this->setExtensionHelper($id, $value, $critical, $replace);
3411
    }
3412
 
3413
    /**
3414
     * Remove a CSR attribute.
3415
     *
3416
     * @param string $id
3417
     * @param int $disposition optional
3418
     * @return bool
3419
     */
3420
    public function removeAttribute($id, $disposition = self::ATTR_ALL)
3421
    {
3422
        $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes');
3423
 
3424
        if (!is_array($attributes)) {
3425
            return false;
3426
        }
3427
 
3428
        $result = false;
3429
        foreach ($attributes as $key => $attribute) {
3430
            if ($attribute['type'] == $id) {
3431
                $n = count($attribute['value']);
3432
                switch (true) {
3433
                    case $disposition == self::ATTR_APPEND:
3434
                    case $disposition == self::ATTR_REPLACE:
3435
                        return false;
3436
                    case $disposition >= $n:
3437
                        $disposition -= $n;
3438
                        break;
3439
                    case $disposition == self::ATTR_ALL:
3440
                    case $n == 1:
3441
                        unset($attributes[$key]);
3442
                        $result = true;
3443
                        break;
3444
                    default:
3445
                        unset($attributes[$key]['value'][$disposition]);
3446
                        $attributes[$key]['value'] = array_values($attributes[$key]['value']);
3447
                        $result = true;
3448
                        break;
3449
                }
3450
                if ($result && $disposition != self::ATTR_ALL) {
3451
                    break;
3452
                }
3453
            }
3454
        }
3455
 
3456
        $attributes = array_values($attributes);
3457
        return $result;
3458
    }
3459
 
3460
    /**
3461
     * Get a CSR attribute
3462
     *
3463
     * Returns the attribute if it exists and false if not
3464
     *
3465
     * @param string $id
3466
     * @param int $disposition optional
3467
     * @param array $csr optional
3468
     * @return mixed
3469
     */
1042 daniel-mar 3470
    public function getAttribute($id, $disposition = self::ATTR_ALL, array $csr = null)
827 daniel-mar 3471
    {
3472
        if (empty($csr)) {
3473
            $csr = $this->currentCert;
3474
        }
3475
 
3476
        $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes');
3477
 
3478
        if (!is_array($attributes)) {
3479
            return false;
3480
        }
3481
 
3482
        foreach ($attributes as $key => $attribute) {
3483
            if ($attribute['type'] == $id) {
3484
                $n = count($attribute['value']);
3485
                switch (true) {
3486
                    case $disposition == self::ATTR_APPEND:
3487
                    case $disposition == self::ATTR_REPLACE:
3488
                        return false;
3489
                    case $disposition == self::ATTR_ALL:
3490
                        return $attribute['value'];
3491
                    case $disposition >= $n:
3492
                        $disposition -= $n;
3493
                        break;
3494
                    default:
3495
                        return $attribute['value'][$disposition];
3496
                }
3497
            }
3498
        }
3499
 
3500
        return false;
3501
    }
3502
 
3503
    /**
3504
     * Returns a list of all CSR attributes in use
3505
     *
3506
     * @param array $csr optional
3507
     * @return array
3508
     */
1042 daniel-mar 3509
    public function getAttributes(array $csr = null)
827 daniel-mar 3510
    {
3511
        if (empty($csr)) {
3512
            $csr = $this->currentCert;
3513
        }
3514
 
3515
        $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes');
3516
        $attrs = [];
3517
 
3518
        if (is_array($attributes)) {
3519
            foreach ($attributes as $attribute) {
3520
                $attrs[] = $attribute['type'];
3521
            }
3522
        }
3523
 
3524
        return $attrs;
3525
    }
3526
 
3527
    /**
3528
     * Set a CSR attribute
3529
     *
3530
     * @param string $id
3531
     * @param mixed $value
3532
     * @param int $disposition optional
3533
     * @return bool
3534
     */
3535
    public function setAttribute($id, $value, $disposition = self::ATTR_ALL)
3536
    {
3537
        $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes', true);
3538
 
3539
        if (!is_array($attributes)) {
3540
            return false;
3541
        }
3542
 
3543
        switch ($disposition) {
3544
            case self::ATTR_REPLACE:
3545
                $disposition = self::ATTR_APPEND;
3546
                // fall-through
3547
            case self::ATTR_ALL:
3548
                $this->removeAttribute($id);
3549
                break;
3550
        }
3551
 
3552
        foreach ($attributes as $key => $attribute) {
3553
            if ($attribute['type'] == $id) {
3554
                $n = count($attribute['value']);
3555
                switch (true) {
3556
                    case $disposition == self::ATTR_APPEND:
3557
                        $last = $key;
3558
                        break;
3559
                    case $disposition >= $n:
3560
                        $disposition -= $n;
3561
                        break;
3562
                    default:
3563
                        $attributes[$key]['value'][$disposition] = $value;
3564
                        return true;
3565
                }
3566
            }
3567
        }
3568
 
3569
        switch (true) {
3570
            case $disposition >= 0:
3571
                return false;
3572
            case isset($last):
3573
                $attributes[$last]['value'][] = $value;
3574
                break;
3575
            default:
3576
                $attributes[] = ['type' => $id, 'value' => $disposition == self::ATTR_ALL ? $value : [$value]];
3577
                break;
3578
        }
3579
 
3580
        return true;
3581
    }
3582
 
3583
    /**
3584
     * Sets the subject key identifier
3585
     *
3586
     * This is used by the id-ce-authorityKeyIdentifier and the id-ce-subjectKeyIdentifier extensions.
3587
     *
3588
     * @param string $value
3589
     */
3590
    public function setKeyIdentifier($value)
3591
    {
3592
        if (empty($value)) {
3593
            unset($this->currentKeyIdentifier);
3594
        } else {
3595
            $this->currentKeyIdentifier = $value;
3596
        }
3597
    }
3598
 
3599
    /**
3600
     * Compute a public key identifier.
3601
     *
3602
     * Although key identifiers may be set to any unique value, this function
3603
     * computes key identifiers from public key according to the two
3604
     * recommended methods (4.2.1.2 RFC 3280).
3605
     * Highly polymorphic: try to accept all possible forms of key:
3606
     * - Key object
3607
     * - \phpseclib3\File\X509 object with public or private key defined
3608
     * - Certificate or CSR array
3609
     * - \phpseclib3\File\ASN1\Element object
3610
     * - PEM or DER string
3611
     *
3612
     * @param mixed $key optional
3613
     * @param int $method optional
3614
     * @return string binary key identifier
3615
     */
3616
    public function computeKeyIdentifier($key = null, $method = 1)
3617
    {
3618
        if (is_null($key)) {
3619
            $key = $this;
3620
        }
3621
 
3622
        switch (true) {
3623
            case is_string($key):
3624
                break;
3625
            case is_array($key) && isset($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']):
3626
                return $this->computeKeyIdentifier($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], $method);
3627
            case is_array($key) && isset($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']):
3628
                return $this->computeKeyIdentifier($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], $method);
3629
            case !is_object($key):
3630
                return false;
3631
            case $key instanceof Element:
3632
                // Assume the element is a bitstring-packed key.
3633
                $decoded = ASN1::decodeBER($key->element);
1042 daniel-mar 3634
                if (!$decoded) {
827 daniel-mar 3635
                    return false;
3636
                }
3637
                $raw = ASN1::asn1map($decoded[0], ['type' => ASN1::TYPE_BIT_STRING]);
3638
                if (empty($raw)) {
3639
                    return false;
3640
                }
3641
                // If the key is private, compute identifier from its corresponding public key.
3642
                $key = PublicKeyLoader::load($raw);
3643
                if ($key instanceof PrivateKey) {  // If private.
3644
                    return $this->computeKeyIdentifier($key, $method);
3645
                }
3646
                $key = $raw; // Is a public key.
3647
                break;
3648
            case $key instanceof X509:
3649
                if (isset($key->publicKey)) {
3650
                    return $this->computeKeyIdentifier($key->publicKey, $method);
3651
                }
3652
                if (isset($key->privateKey)) {
3653
                    return $this->computeKeyIdentifier($key->privateKey, $method);
3654
                }
3655
                if (isset($key->currentCert['tbsCertificate']) || isset($key->currentCert['certificationRequestInfo'])) {
3656
                    return $this->computeKeyIdentifier($key->currentCert, $method);
3657
                }
3658
                return false;
3659
            default: // Should be a key object (i.e.: \phpseclib3\Crypt\RSA).
3660
                $key = $key->getPublicKey();
3661
                break;
3662
        }
3663
 
3664
        // If in PEM format, convert to binary.
3665
        $key = ASN1::extractBER($key);
3666
 
3667
        // Now we have the key string: compute its sha-1 sum.
3668
        $hash = new Hash('sha1');
3669
        $hash = $hash->hash($key);
3670
 
3671
        if ($method == 2) {
3672
            $hash = substr($hash, -8);
3673
            $hash[0] = chr((ord($hash[0]) & 0x0F) | 0x40);
3674
        }
3675
 
3676
        return $hash;
3677
    }
3678
 
3679
    /**
3680
     * Format a public key as appropriate
3681
     *
3682
     * @return array|false
3683
     */
3684
    private function formatSubjectPublicKey()
3685
    {
3686
        $format = $this->publicKey instanceof RSA && ($this->publicKey->getPadding() & RSA::SIGNATURE_PSS) ?
3687
            'PSS' :
3688
            'PKCS8';
3689
 
3690
        $publicKey = base64_decode(preg_replace('#-.+-|[\r\n]#', '', $this->publicKey->toString($format)));
3691
 
3692
        $decoded = ASN1::decodeBER($publicKey);
1042 daniel-mar 3693
        if (!$decoded) {
3694
            return false;
3695
        }
827 daniel-mar 3696
        $mapped = ASN1::asn1map($decoded[0], Maps\SubjectPublicKeyInfo::MAP);
3697
        if (!is_array($mapped)) {
3698
            return false;
3699
        }
3700
 
3701
        $mapped['subjectPublicKey'] = $this->publicKey->toString($format);
3702
 
3703
        return $mapped;
3704
    }
3705
 
3706
    /**
3707
     * Set the domain name's which the cert is to be valid for
3708
     *
3709
     * @param mixed ...$domains
3710
     * @return void
3711
     */
3712
    public function setDomain(...$domains)
3713
    {
3714
        $this->domains = $domains;
3715
        $this->removeDNProp('id-at-commonName');
3716
        $this->setDNProp('id-at-commonName', $this->domains[0]);
3717
    }
3718
 
3719
    /**
3720
     * Set the IP Addresses's which the cert is to be valid for
3721
     *
3722
     * @param mixed[] ...$ipAddresses
3723
     */
3724
    public function setIPAddress(...$ipAddresses)
3725
    {
3726
        $this->ipAddresses = $ipAddresses;
3727
        /*
3728
        if (!isset($this->domains)) {
3729
            $this->removeDNProp('id-at-commonName');
3730
            $this->setDNProp('id-at-commonName', $this->ipAddresses[0]);
3731
        }
3732
        */
3733
    }
3734
 
3735
    /**
3736
     * Helper function to build domain array
3737
     *
3738
     * @param string $domain
3739
     * @return array
3740
     */
1042 daniel-mar 3741
    private static function dnsName($domain)
827 daniel-mar 3742
    {
3743
        return ['dNSName' => $domain];
3744
    }
3745
 
3746
    /**
3747
     * Helper function to build IP Address array
3748
     *
3749
     * (IPv6 is not currently supported)
3750
     *
3751
     * @param string $address
3752
     * @return array
3753
     */
3754
    private function iPAddress($address)
3755
    {
3756
        return ['iPAddress' => $address];
3757
    }
3758
 
3759
    /**
3760
     * Get the index of a revoked certificate.
3761
     *
3762
     * @param array $rclist
3763
     * @param string $serial
3764
     * @param bool $create optional
3765
     * @return int|false
3766
     */
1042 daniel-mar 3767
    private function revokedCertificate(array &$rclist, $serial, $create = false)
827 daniel-mar 3768
    {
3769
        $serial = new BigInteger($serial);
3770
 
3771
        foreach ($rclist as $i => $rc) {
3772
            if (!($serial->compare($rc['userCertificate']))) {
3773
                return $i;
3774
            }
3775
        }
3776
 
3777
        if (!$create) {
3778
            return false;
3779
        }
3780
 
3781
        $i = count($rclist);
3782
        $revocationDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
3783
        $rclist[] = ['userCertificate' => $serial,
3784
                          'revocationDate'  => $this->timeField($revocationDate->format('D, d M Y H:i:s O'))];
3785
        return $i;
3786
    }
3787
 
3788
    /**
3789
     * Revoke a certificate.
3790
     *
3791
     * @param string $serial
3792
     * @param string $date optional
3793
     * @return bool
3794
     */
3795
    public function revoke($serial, $date = null)
3796
    {
3797
        if (isset($this->currentCert['tbsCertList'])) {
3798
            if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) {
3799
                if ($this->revokedCertificate($rclist, $serial) === false) { // If not yet revoked
3800
                    if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) {
3801
                        if (!empty($date)) {
3802
                            $rclist[$i]['revocationDate'] = $this->timeField($date);
3803
                        }
3804
 
3805
                        return true;
3806
                    }
3807
                }
3808
            }
3809
        }
3810
 
3811
        return false;
3812
    }
3813
 
3814
    /**
3815
     * Unrevoke a certificate.
3816
     *
3817
     * @param string $serial
3818
     * @return bool
3819
     */
3820
    public function unrevoke($serial)
3821
    {
3822
        if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) {
3823
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3824
                unset($rclist[$i]);
3825
                $rclist = array_values($rclist);
3826
                return true;
3827
            }
3828
        }
3829
 
3830
        return false;
3831
    }
3832
 
3833
    /**
3834
     * Get a revoked certificate.
3835
     *
3836
     * @param string $serial
3837
     * @return mixed
3838
     */
3839
    public function getRevoked($serial)
3840
    {
3841
        if (is_array($rclist = $this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) {
3842
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3843
                return $rclist[$i];
3844
            }
3845
        }
3846
 
3847
        return false;
3848
    }
3849
 
3850
    /**
3851
     * List revoked certificates
3852
     *
3853
     * @param array $crl optional
3854
     * @return array|bool
3855
     */
1042 daniel-mar 3856
    public function listRevoked(array $crl = null)
827 daniel-mar 3857
    {
3858
        if (!isset($crl)) {
3859
            $crl = $this->currentCert;
3860
        }
3861
 
3862
        if (!isset($crl['tbsCertList'])) {
3863
            return false;
3864
        }
3865
 
3866
        $result = [];
3867
 
3868
        if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) {
3869
            foreach ($rclist as $rc) {
3870
                $result[] = $rc['userCertificate']->toString();
3871
            }
3872
        }
3873
 
3874
        return $result;
3875
    }
3876
 
3877
    /**
3878
     * Remove a Revoked Certificate Extension
3879
     *
3880
     * @param string $serial
3881
     * @param string $id
3882
     * @return bool
3883
     */
3884
    public function removeRevokedCertificateExtension($serial, $id)
3885
    {
3886
        if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) {
3887
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3888
                return $this->removeExtensionHelper($id, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3889
            }
3890
        }
3891
 
3892
        return false;
3893
    }
3894
 
3895
    /**
3896
     * Get a Revoked Certificate Extension
3897
     *
3898
     * Returns the extension if it exists and false if not
3899
     *
3900
     * @param string $serial
3901
     * @param string $id
3902
     * @param array $crl optional
3903
     * @return mixed
3904
     */
1042 daniel-mar 3905
    public function getRevokedCertificateExtension($serial, $id, array $crl = null)
827 daniel-mar 3906
    {
3907
        if (!isset($crl)) {
3908
            $crl = $this->currentCert;
3909
        }
3910
 
3911
        if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) {
3912
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3913
                return $this->getExtension($id, $crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3914
            }
3915
        }
3916
 
3917
        return false;
3918
    }
3919
 
3920
    /**
3921
     * Returns a list of all extensions in use for a given revoked certificate
3922
     *
3923
     * @param string $serial
3924
     * @param array $crl optional
3925
     * @return array|bool
3926
     */
1042 daniel-mar 3927
    public function getRevokedCertificateExtensions($serial, array $crl = null)
827 daniel-mar 3928
    {
3929
        if (!isset($crl)) {
3930
            $crl = $this->currentCert;
3931
        }
3932
 
3933
        if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) {
3934
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3935
                return $this->getExtensions($crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3936
            }
3937
        }
3938
 
3939
        return false;
3940
    }
3941
 
3942
    /**
3943
     * Set a Revoked Certificate Extension
3944
     *
3945
     * @param string $serial
3946
     * @param string $id
3947
     * @param mixed $value
3948
     * @param bool $critical optional
3949
     * @param bool $replace optional
3950
     * @return bool
3951
     */
3952
    public function setRevokedCertificateExtension($serial, $id, $value, $critical = false, $replace = true)
3953
    {
3954
        if (isset($this->currentCert['tbsCertList'])) {
3955
            if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) {
3956
                if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) {
3957
                    return $this->setExtensionHelper($id, $value, $critical, $replace, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3958
                }
3959
            }
3960
        }
3961
 
3962
        return false;
3963
    }
3964
 
3965
    /**
3966
     * Register the mapping for a custom/unsupported extension.
3967
     *
3968
     * @param string $id
3969
     * @param array $mapping
3970
     */
3971
    public static function registerExtension($id, array $mapping)
3972
    {
3973
        if (isset(self::$extensions[$id]) && self::$extensions[$id] !== $mapping) {
3974
            throw new \RuntimeException(
3975
                'Extension ' . $id . ' has already been defined with a different mapping.'
3976
            );
3977
        }
3978
 
3979
        self::$extensions[$id] = $mapping;
3980
    }
3981
 
3982
    /**
3983
     * Register the mapping for a custom/unsupported extension.
3984
     *
3985
     * @param string $id
3986
     *
3987
     * @return array|null
3988
     */
3989
    public static function getRegisteredExtension($id)
3990
    {
3991
        return isset(self::$extensions[$id]) ? self::$extensions[$id] : null;
3992
    }
3993
 
3994
    /**
3995
     * Register the mapping for a custom/unsupported extension.
3996
     *
3997
     * @param string $id
3998
     * @param mixed $value
3999
     * @param bool $critical
4000
     * @param bool $replace
4001
     */
4002
    public function setExtensionValue($id, $value, $critical = false, $replace = false)
4003
    {
4004
        $this->extensionValues[$id] = compact('critical', 'replace', 'value');
4005
    }
4006
}