Subversion Repositories oidplus

Rev

Rev 1042 | Go to most recent revision | 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) {
1045
                    $value = str_replace(['.', '*'], ['\.', '[^.]*'], $value);
1046
                    switch ($key) {
1047
                        case 'dNSName':
1048
                            /* From RFC2818 "HTTP over TLS":
1049
 
1050
                               If a subjectAltName extension of type dNSName is present, that MUST
1051
                               be used as the identity. Otherwise, the (most specific) Common Name
1052
                               field in the Subject field of the certificate MUST be used. Although
1053
                               the use of the Common Name is existing practice, it is deprecated and
1054
                               Certification Authorities are encouraged to use the dNSName instead. */
1055
                            if (preg_match('#^' . $value . '$#', $components['host'])) {
1056
                                return true;
1057
                            }
1058
                            break;
1059
                        case 'iPAddress':
1060
                            /* From RFC2818 "HTTP over TLS":
1061
 
1062
                               In some cases, the URI is specified as an IP address rather than a
1063
                               hostname. In this case, the iPAddress subjectAltName must be present
1064
                               in the certificate and must exactly match the IP in the URI. */
1065
                            if (preg_match('#(?:\d{1-3}\.){4}#', $components['host'] . '.') && preg_match('#^' . $value . '$#', $components['host'])) {
1066
                                return true;
1067
                            }
1068
                    }
1069
                }
1070
            }
1071
            return false;
1072
        }
1073
 
1074
        if ($value = $this->getDNProp('id-at-commonName')) {
1075
            $value = str_replace(['.', '*'], ['\.', '[^.]*'], $value[0]);
1076
            return preg_match('#^' . $value . '$#', $components['host']) === 1;
1077
        }
1078
 
1079
        return false;
1080
    }
1081
 
1082
    /**
1083
     * Validate a date
1084
     *
1085
     * If $date isn't defined it is assumed to be the current date.
1086
     *
1087
     * @param \DateTimeInterface|string $date optional
1088
     * @return bool
1089
     */
1090
    public function validateDate($date = null)
1091
    {
1092
        if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) {
1093
            return false;
1094
        }
1095
 
1096
        if (!isset($date)) {
1097
            $date = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
1098
        }
1099
 
1100
        $notBefore = $this->currentCert['tbsCertificate']['validity']['notBefore'];
1101
        $notBefore = isset($notBefore['generalTime']) ? $notBefore['generalTime'] : $notBefore['utcTime'];
1102
 
1103
        $notAfter = $this->currentCert['tbsCertificate']['validity']['notAfter'];
1104
        $notAfter = isset($notAfter['generalTime']) ? $notAfter['generalTime'] : $notAfter['utcTime'];
1105
 
1106
        if (is_string($date)) {
1107
            $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get()));
1108
        }
1109
 
1110
        $notBefore = new \DateTimeImmutable($notBefore, new \DateTimeZone(@date_default_timezone_get()));
1111
        $notAfter = new \DateTimeImmutable($notAfter, new \DateTimeZone(@date_default_timezone_get()));
1112
 
1113
        return $date >= $notBefore && $date <= $notAfter;
1114
    }
1115
 
1116
    /**
1117
     * Fetches a URL
1118
     *
1119
     * @param string $url
1120
     * @return bool|string
1121
     */
1122
    private static function fetchURL($url)
1123
    {
1124
        if (self::$disable_url_fetch) {
1125
            return false;
1126
        }
1127
 
1128
        $parts = parse_url($url);
1129
        $data = '';
1130
        switch ($parts['scheme']) {
1131
            case 'http':
1132
                $fsock = @fsockopen($parts['host'], isset($parts['port']) ? $parts['port'] : 80);
1133
                if (!$fsock) {
1134
                    return false;
1135
                }
1042 daniel-mar 1136
                $path = $parts['path'];
1137
                if (isset($parts['query'])) {
1138
                    $path .= '?' . $parts['query'];
1139
                }
1140
                fputs($fsock, "GET $path HTTP/1.0\r\n");
827 daniel-mar 1141
                fputs($fsock, "Host: $parts[host]\r\n\r\n");
1142
                $line = fgets($fsock, 1024);
1143
                if (strlen($line) < 3) {
1144
                    return false;
1145
                }
1146
                preg_match('#HTTP/1.\d (\d{3})#', $line, $temp);
1147
                if ($temp[1] != '200') {
1148
                    return false;
1149
                }
1150
 
1151
                // skip the rest of the headers in the http response
1152
                while (!feof($fsock) && fgets($fsock, 1024) != "\r\n") {
1153
                }
1154
 
1155
                while (!feof($fsock)) {
1156
                    $temp = fread($fsock, 1024);
1157
                    if ($temp === false) {
1158
                        return false;
1159
                    }
1160
                    $data .= $temp;
1161
                }
1162
 
1163
                break;
1164
            //case 'ftp':
1165
            //case 'ldap':
1166
            //default:
1167
        }
1168
 
1169
        return $data;
1170
    }
1171
 
1172
    /**
1173
     * Validates an intermediate cert as identified via authority info access extension
1174
     *
1175
     * See https://tools.ietf.org/html/rfc4325 for more info
1176
     *
1177
     * @param bool $caonly
1178
     * @param int $count
1179
     * @return bool
1180
     */
1181
    private function testForIntermediate($caonly, $count)
1182
    {
1183
        $opts = $this->getExtension('id-pe-authorityInfoAccess');
1184
        if (!is_array($opts)) {
1185
            return false;
1186
        }
1187
        foreach ($opts as $opt) {
1188
            if ($opt['accessMethod'] == 'id-ad-caIssuers') {
1189
                // accessLocation is a GeneralName. GeneralName fields support stuff like email addresses, IP addresses, LDAP,
1190
                // etc, but we're only supporting URI's. URI's and LDAP are the only thing https://tools.ietf.org/html/rfc4325
1191
                // discusses
1192
                if (isset($opt['accessLocation']['uniformResourceIdentifier'])) {
1193
                    $url = $opt['accessLocation']['uniformResourceIdentifier'];
1194
                    break;
1195
                }
1196
            }
1197
        }
1198
 
1199
        if (!isset($url)) {
1200
            return false;
1201
        }
1202
 
1203
        $cert = static::fetchURL($url);
1204
        if (!is_string($cert)) {
1205
            return false;
1206
        }
1207
 
1208
        $parent = new static();
1209
        $parent->CAs = $this->CAs;
1210
        /*
1211
         "Conforming applications that support HTTP or FTP for accessing
1212
          certificates MUST be able to accept .cer files and SHOULD be able
1213
          to accept .p7c files." -- https://tools.ietf.org/html/rfc4325
1214
 
1215
         A .p7c file is 'a "certs-only" CMS message as specified in RFC 2797"
1216
 
1217
         These are currently unsupported
1218
        */
1219
        if (!is_array($parent->loadX509($cert))) {
1220
            return false;
1221
        }
1222
 
1223
        if (!$parent->validateSignatureCountable($caonly, ++$count)) {
1224
            return false;
1225
        }
1226
 
1227
        $this->CAs[] = $parent->currentCert;
1228
        //$this->loadCA($cert);
1229
 
1230
        return true;
1231
    }
1232
 
1233
    /**
1234
     * Validate a signature
1235
     *
1236
     * Works on X.509 certs, CSR's and CRL's.
1237
     * Returns true if the signature is verified, false if it is not correct or null on error
1238
     *
1239
     * By default returns false for self-signed certs. Call validateSignature(false) to make this support
1240
     * self-signed.
1241
     *
1242
     * The behavior of this function is inspired by {@link http://php.net/openssl-verify openssl_verify}.
1243
     *
1244
     * @param bool $caonly optional
1245
     * @return mixed
1246
     */
1247
    public function validateSignature($caonly = true)
1248
    {
1249
        return $this->validateSignatureCountable($caonly, 0);
1250
    }
1251
 
1252
    /**
1253
     * Validate a signature
1254
     *
1255
     * Performs said validation whilst keeping track of how many times validation method is called
1256
     *
1257
     * @param bool $caonly
1258
     * @param int $count
1259
     * @return mixed
1260
     */
1261
    private function validateSignatureCountable($caonly, $count)
1262
    {
1263
        if (!is_array($this->currentCert) || !isset($this->signatureSubject)) {
1264
            return null;
1265
        }
1266
 
1267
        if ($count == self::$recur_limit) {
1268
            return false;
1269
        }
1270
 
1271
        /* TODO:
1272
           "emailAddress attribute values are not case-sensitive (e.g., "subscriber@example.com" is the same as "SUBSCRIBER@EXAMPLE.COM")."
1273
            -- http://tools.ietf.org/html/rfc5280#section-4.1.2.6
1274
 
1275
           implement pathLenConstraint in the id-ce-basicConstraints extension */
1276
 
1277
        switch (true) {
1278
            case isset($this->currentCert['tbsCertificate']):
1279
                // self-signed cert
1280
                switch (true) {
1281
                    case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $this->currentCert['tbsCertificate']['subject']:
1282
                    case defined('FILE_X509_IGNORE_TYPE') && $this->getIssuerDN(self::DN_STRING) === $this->getDN(self::DN_STRING):
1283
                        $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier');
1284
                        $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier');
1285
                        switch (true) {
1286
                            case !is_array($authorityKey):
1287
                            case !$subjectKeyID:
1288
                            case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
1289
                                $signingCert = $this->currentCert; // working cert
1290
                        }
1291
                }
1292
 
1293
                if (!empty($this->CAs)) {
1294
                    for ($i = 0; $i < count($this->CAs); $i++) {
1295
                        // even if the cert is a self-signed one we still want to see if it's a CA;
1296
                        // if not, we'll conditionally return an error
1297
                        $ca = $this->CAs[$i];
1298
                        switch (true) {
1299
                            case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']:
1300
                            case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertificate']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']):
1301
                                $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier');
1302
                                $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca);
1303
                                switch (true) {
1304
                                    case !is_array($authorityKey):
1305
                                    case !$subjectKeyID:
1306
                                    case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
1307
                                        if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) {
1308
                                            break 2; // serial mismatch - check other ca
1309
                                        }
1310
                                        $signingCert = $ca; // working cert
1311
                                        break 3;
1312
                                }
1313
                        }
1314
                    }
1315
                    if (count($this->CAs) == $i && $caonly) {
1316
                        return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly);
1317
                    }
1318
                } elseif (!isset($signingCert) || $caonly) {
1319
                    return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly);
1320
                }
1321
                return $this->validateSignatureHelper(
1322
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'],
1323
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'],
1324
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1325
                    substr($this->currentCert['signature'], 1),
1326
                    $this->signatureSubject
1327
                );
1328
            case isset($this->currentCert['certificationRequestInfo']):
1329
                return $this->validateSignatureHelper(
1330
                    $this->currentCert['certificationRequestInfo']['subjectPKInfo']['algorithm']['algorithm'],
1331
                    $this->currentCert['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'],
1332
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1333
                    substr($this->currentCert['signature'], 1),
1334
                    $this->signatureSubject
1335
                );
1336
            case isset($this->currentCert['publicKeyAndChallenge']):
1337
                return $this->validateSignatureHelper(
1338
                    $this->currentCert['publicKeyAndChallenge']['spki']['algorithm']['algorithm'],
1339
                    $this->currentCert['publicKeyAndChallenge']['spki']['subjectPublicKey'],
1340
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1341
                    substr($this->currentCert['signature'], 1),
1342
                    $this->signatureSubject
1343
                );
1344
            case isset($this->currentCert['tbsCertList']):
1345
                if (!empty($this->CAs)) {
1346
                    for ($i = 0; $i < count($this->CAs); $i++) {
1347
                        $ca = $this->CAs[$i];
1348
                        switch (true) {
1349
                            case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertList']['issuer'] === $ca['tbsCertificate']['subject']:
1350
                            case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertList']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']):
1351
                                $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier');
1352
                                $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca);
1353
                                switch (true) {
1354
                                    case !is_array($authorityKey):
1355
                                    case !$subjectKeyID:
1356
                                    case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
1357
                                        if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) {
1358
                                            break 2; // serial mismatch - check other ca
1359
                                        }
1360
                                        $signingCert = $ca; // working cert
1361
                                        break 3;
1362
                                }
1363
                        }
1364
                    }
1365
                }
1366
                if (!isset($signingCert)) {
1367
                    return false;
1368
                }
1369
                return $this->validateSignatureHelper(
1370
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'],
1371
                    $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'],
1372
                    $this->currentCert['signatureAlgorithm']['algorithm'],
1373
                    substr($this->currentCert['signature'], 1),
1374
                    $this->signatureSubject
1375
                );
1376
            default:
1377
                return false;
1378
        }
1379
    }
1380
 
1381
    /**
1382
     * Validates a signature
1383
     *
1384
     * Returns true if the signature is verified and false if it is not correct.
1385
     * If the algorithms are unsupposed an exception is thrown.
1386
     *
1387
     * @param string $publicKeyAlgorithm
1388
     * @param string $publicKey
1389
     * @param string $signatureAlgorithm
1390
     * @param string $signature
1391
     * @param string $signatureSubject
1392
     * @throws \phpseclib3\Exception\UnsupportedAlgorithmException if the algorithm is unsupported
1393
     * @return bool
1394
     */
1395
    private function validateSignatureHelper($publicKeyAlgorithm, $publicKey, $signatureAlgorithm, $signature, $signatureSubject)
1396
    {
1397
        switch ($publicKeyAlgorithm) {
1398
            case 'id-RSASSA-PSS':
1399
                $key = RSA::loadFormat('PSS', $publicKey);
1400
                break;
1401
            case 'rsaEncryption':
1402
                $key = RSA::loadFormat('PKCS8', $publicKey);
1403
                switch ($signatureAlgorithm) {
1042 daniel-mar 1404
                    case 'id-RSASSA-PSS':
1405
                        break;
827 daniel-mar 1406
                    case 'md2WithRSAEncryption':
1407
                    case 'md5WithRSAEncryption':
1408
                    case 'sha1WithRSAEncryption':
1409
                    case 'sha224WithRSAEncryption':
1410
                    case 'sha256WithRSAEncryption':
1411
                    case 'sha384WithRSAEncryption':
1412
                    case 'sha512WithRSAEncryption':
1413
                        $key = $key
1414
                            ->withHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm))
1415
                            ->withPadding(RSA::SIGNATURE_PKCS1);
1416
                        break;
1417
                    default:
1418
                        throw new UnsupportedAlgorithmException('Signature algorithm unsupported');
1419
                }
1420
                break;
1421
            case 'id-Ed25519':
1422
            case 'id-Ed448':
1423
                $key = EC::loadFormat('PKCS8', $publicKey);
1424
                break;
1425
            case 'id-ecPublicKey':
1426
                $key = EC::loadFormat('PKCS8', $publicKey);
1427
                switch ($signatureAlgorithm) {
1428
                    case 'ecdsa-with-SHA1':
1429
                    case 'ecdsa-with-SHA224':
1430
                    case 'ecdsa-with-SHA256':
1431
                    case 'ecdsa-with-SHA384':
1432
                    case 'ecdsa-with-SHA512':
1433
                        $key = $key
1434
                            ->withHash(preg_replace('#^ecdsa-with-#', '', strtolower($signatureAlgorithm)));
1435
                        break;
1436
                    default:
1437
                        throw new UnsupportedAlgorithmException('Signature algorithm unsupported');
1438
                }
1439
                break;
1440
            case 'id-dsa':
1441
                $key = DSA::loadFormat('PKCS8', $publicKey);
1442
                switch ($signatureAlgorithm) {
1443
                    case 'id-dsa-with-sha1':
1444
                    case 'id-dsa-with-sha224':
1445
                    case 'id-dsa-with-sha256':
1446
                        $key = $key
1447
                            ->withHash(preg_replace('#^id-dsa-with-#', '', strtolower($signatureAlgorithm)));
1448
                        break;
1449
                    default:
1450
                        throw new UnsupportedAlgorithmException('Signature algorithm unsupported');
1451
                }
1452
                break;
1453
            default:
1454
                throw new UnsupportedAlgorithmException('Public key algorithm unsupported');
1455
        }
1456
 
1457
        return $key->verify($signatureSubject, $signature);
1458
    }
1459
 
1460
    /**
1461
     * Sets the recursion limit
1462
     *
1463
     * When validating a signature it may be necessary to download intermediate certs from URI's.
1464
     * An intermediate cert that linked to itself would result in an infinite loop so to prevent
1465
     * that we set a recursion limit. A negative number means that there is no recursion limit.
1466
     *
1467
     * @param int $count
1468
     */
1469
    public static function setRecurLimit($count)
1470
    {
1471
        self::$recur_limit = $count;
1472
    }
1473
 
1474
    /**
1475
     * Prevents URIs from being automatically retrieved
1476
     *
1477
     */
1478
    public static function disableURLFetch()
1479
    {
1480
        self::$disable_url_fetch = true;
1481
    }
1482
 
1483
    /**
1484
     * Allows URIs to be automatically retrieved
1485
     *
1486
     */
1487
    public static function enableURLFetch()
1488
    {
1489
        self::$disable_url_fetch = false;
1490
    }
1491
 
1492
    /**
1493
     * Decodes an IP address
1494
     *
1495
     * Takes in a base64 encoded "blob" and returns a human readable IP address
1496
     *
1497
     * @param string $ip
1498
     * @return string
1499
     */
1500
    public static function decodeIP($ip)
1501
    {
1502
        return inet_ntop($ip);
1503
    }
1504
 
1505
    /**
1506
     * Decodes an IP address in a name constraints extension
1507
     *
1508
     * Takes in a base64 encoded "blob" and returns a human readable IP address / mask
1509
     *
1510
     * @param string $ip
1511
     * @return array
1512
     */
1513
    public static function decodeNameConstraintIP($ip)
1514
    {
1515
        $size = strlen($ip) >> 1;
1516
        $mask = substr($ip, $size);
1517
        $ip = substr($ip, 0, $size);
1518
        return [inet_ntop($ip), inet_ntop($mask)];
1519
    }
1520
 
1521
    /**
1522
     * Encodes an IP address
1523
     *
1524
     * Takes a human readable IP address into a base64-encoded "blob"
1525
     *
1526
     * @param string|array $ip
1527
     * @return string
1528
     */
1529
    public static function encodeIP($ip)
1530
    {
1531
        return is_string($ip) ?
1532
            inet_pton($ip) :
1533
            inet_pton($ip[0]) . inet_pton($ip[1]);
1534
    }
1535
 
1536
    /**
1537
     * "Normalizes" a Distinguished Name property
1538
     *
1539
     * @param string $propName
1540
     * @return mixed
1541
     */
1542
    private function translateDNProp($propName)
1543
    {
1544
        switch (strtolower($propName)) {
1308 daniel-mar 1545
            case 'jurisdictionofincorporationcountryname':
1546
            case 'jurisdictioncountryname':
1547
            case 'jurisdictionc':
1548
                return 'jurisdictionOfIncorporationCountryName';
1549
            case 'jurisdictionofincorporationstateorprovincename':
1550
            case 'jurisdictionstateorprovincename':
1551
            case 'jurisdictionst':
1552
                return 'jurisdictionOfIncorporationStateOrProvinceName';
1553
            case 'jurisdictionlocalityname':
1554
            case 'jurisdictionl':
1555
                return 'jurisdictionLocalityName';
1556
            case 'id-at-businesscategory':
1557
            case 'businesscategory':
1558
                return 'id-at-businessCategory';
827 daniel-mar 1559
            case 'id-at-countryname':
1560
            case 'countryname':
1561
            case 'c':
1562
                return 'id-at-countryName';
1563
            case 'id-at-organizationname':
1564
            case 'organizationname':
1565
            case 'o':
1566
                return 'id-at-organizationName';
1567
            case 'id-at-dnqualifier':
1568
            case 'dnqualifier':
1569
                return 'id-at-dnQualifier';
1570
            case 'id-at-commonname':
1571
            case 'commonname':
1572
            case 'cn':
1573
                return 'id-at-commonName';
1574
            case 'id-at-stateorprovincename':
1575
            case 'stateorprovincename':
1576
            case 'state':
1577
            case 'province':
1578
            case 'provincename':
1579
            case 'st':
1580
                return 'id-at-stateOrProvinceName';
1581
            case 'id-at-localityname':
1582
            case 'localityname':
1583
            case 'l':
1584
                return 'id-at-localityName';
1585
            case 'id-emailaddress':
1586
            case 'emailaddress':
1587
                return 'pkcs-9-at-emailAddress';
1588
            case 'id-at-serialnumber':
1589
            case 'serialnumber':
1590
                return 'id-at-serialNumber';
1591
            case 'id-at-postalcode':
1592
            case 'postalcode':
1593
                return 'id-at-postalCode';
1594
            case 'id-at-streetaddress':
1595
            case 'streetaddress':
1596
                return 'id-at-streetAddress';
1597
            case 'id-at-name':
1598
            case 'name':
1599
                return 'id-at-name';
1600
            case 'id-at-givenname':
1601
            case 'givenname':
1602
                return 'id-at-givenName';
1603
            case 'id-at-surname':
1604
            case 'surname':
1605
            case 'sn':
1606
                return 'id-at-surname';
1607
            case 'id-at-initials':
1608
            case 'initials':
1609
                return 'id-at-initials';
1610
            case 'id-at-generationqualifier':
1611
            case 'generationqualifier':
1612
                return 'id-at-generationQualifier';
1613
            case 'id-at-organizationalunitname':
1614
            case 'organizationalunitname':
1615
            case 'ou':
1616
                return 'id-at-organizationalUnitName';
1617
            case 'id-at-pseudonym':
1618
            case 'pseudonym':
1619
                return 'id-at-pseudonym';
1620
            case 'id-at-title':
1621
            case 'title':
1622
                return 'id-at-title';
1623
            case 'id-at-description':
1624
            case 'description':
1625
                return 'id-at-description';
1626
            case 'id-at-role':
1627
            case 'role':
1628
                return 'id-at-role';
1629
            case 'id-at-uniqueidentifier':
1630
            case 'uniqueidentifier':
1631
            case 'x500uniqueidentifier':
1632
                return 'id-at-uniqueIdentifier';
1633
            case 'postaladdress':
1634
            case 'id-at-postaladdress':
1635
                return 'id-at-postalAddress';
1636
            default:
1637
                return false;
1638
        }
1639
    }
1640
 
1641
    /**
1642
     * Set a Distinguished Name property
1643
     *
1644
     * @param string $propName
1645
     * @param mixed $propValue
1646
     * @param string $type optional
1647
     * @return bool
1648
     */
1649
    public function setDNProp($propName, $propValue, $type = 'utf8String')
1650
    {
1651
        if (empty($this->dn)) {
1652
            $this->dn = ['rdnSequence' => []];
1653
        }
1654
 
1655
        if (($propName = $this->translateDNProp($propName)) === false) {
1656
            return false;
1657
        }
1658
 
1659
        foreach ((array) $propValue as $v) {
1660
            if (!is_array($v) && isset($type)) {
1661
                $v = [$type => $v];
1662
            }
1663
            $this->dn['rdnSequence'][] = [
1664
                [
1665
                    'type' => $propName,
1666
                    'value' => $v
1667
                ]
1668
            ];
1669
        }
1670
 
1671
        return true;
1672
    }
1673
 
1674
    /**
1675
     * Remove Distinguished Name properties
1676
     *
1677
     * @param string $propName
1678
     */
1679
    public function removeDNProp($propName)
1680
    {
1681
        if (empty($this->dn)) {
1682
            return;
1683
        }
1684
 
1685
        if (($propName = $this->translateDNProp($propName)) === false) {
1686
            return;
1687
        }
1688
 
1689
        $dn = &$this->dn['rdnSequence'];
1690
        $size = count($dn);
1691
        for ($i = 0; $i < $size; $i++) {
1692
            if ($dn[$i][0]['type'] == $propName) {
1693
                unset($dn[$i]);
1694
            }
1695
        }
1696
 
1697
        $dn = array_values($dn);
1698
        // fix for https://bugs.php.net/75433 affecting PHP 7.2
1699
        if (!isset($dn[0])) {
1700
            $dn = array_splice($dn, 0, 0);
1701
        }
1702
    }
1703
 
1704
    /**
1705
     * Get Distinguished Name properties
1706
     *
1707
     * @param string $propName
1708
     * @param array $dn optional
1709
     * @param bool $withType optional
1710
     * @return mixed
1711
     */
1042 daniel-mar 1712
    public function getDNProp($propName, array $dn = null, $withType = false)
827 daniel-mar 1713
    {
1714
        if (!isset($dn)) {
1715
            $dn = $this->dn;
1716
        }
1717
 
1718
        if (empty($dn)) {
1719
            return false;
1720
        }
1721
 
1722
        if (($propName = $this->translateDNProp($propName)) === false) {
1723
            return false;
1724
        }
1725
 
1726
        $filters = [];
1727
        $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1728
        ASN1::setFilters($filters);
1729
        $this->mapOutDNs($dn, 'rdnSequence');
1730
        $dn = $dn['rdnSequence'];
1731
        $result = [];
1732
        for ($i = 0; $i < count($dn); $i++) {
1733
            if ($dn[$i][0]['type'] == $propName) {
1734
                $v = $dn[$i][0]['value'];
1735
                if (!$withType) {
1736
                    if (is_array($v)) {
1737
                        foreach ($v as $type => $s) {
1738
                            $type = array_search($type, ASN1::ANY_MAP);
1739
                            if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) {
1740
                                $s = ASN1::convert($s, $type);
1741
                                if ($s !== false) {
1742
                                    $v = $s;
1743
                                    break;
1744
                                }
1745
                            }
1746
                        }
1747
                        if (is_array($v)) {
1748
                            $v = array_pop($v); // Always strip data type.
1749
                        }
1750
                    } elseif (is_object($v) && $v instanceof Element) {
1751
                        $map = $this->getMapping($propName);
1752
                        if (!is_bool($map)) {
1753
                            $decoded = ASN1::decodeBER($v);
1042 daniel-mar 1754
                            if (!$decoded) {
1755
                                return false;
1756
                            }
827 daniel-mar 1757
                            $v = ASN1::asn1map($decoded[0], $map);
1758
                        }
1759
                    }
1760
                }
1761
                $result[] = $v;
1762
            }
1763
        }
1764
 
1765
        return $result;
1766
    }
1767
 
1768
    /**
1769
     * Set a Distinguished Name
1770
     *
1771
     * @param mixed $dn
1772
     * @param bool $merge optional
1773
     * @param string $type optional
1774
     * @return bool
1775
     */
1776
    public function setDN($dn, $merge = false, $type = 'utf8String')
1777
    {
1778
        if (!$merge) {
1779
            $this->dn = null;
1780
        }
1781
 
1782
        if (is_array($dn)) {
1783
            if (isset($dn['rdnSequence'])) {
1784
                $this->dn = $dn; // No merge here.
1785
                return true;
1786
            }
1787
 
1788
            // handles stuff generated by openssl_x509_parse()
1789
            foreach ($dn as $prop => $value) {
1790
                if (!$this->setDNProp($prop, $value, $type)) {
1791
                    return false;
1792
                }
1793
            }
1794
            return true;
1795
        }
1796
 
1797
        // handles everything else
1798
        $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);
1799
        for ($i = 1; $i < count($results); $i += 2) {
1800
            $prop = trim($results[$i], ', =/');
1801
            $value = $results[$i + 1];
1802
            if (!$this->setDNProp($prop, $value, $type)) {
1803
                return false;
1804
            }
1805
        }
1806
 
1807
        return true;
1808
    }
1809
 
1810
    /**
1811
     * Get the Distinguished Name for a certificates subject
1812
     *
1813
     * @param mixed $format optional
1814
     * @param array $dn optional
1815
     * @return array|bool|string
1816
     */
1042 daniel-mar 1817
    public function getDN($format = self::DN_ARRAY, array $dn = null)
827 daniel-mar 1818
    {
1819
        if (!isset($dn)) {
1820
            $dn = isset($this->currentCert['tbsCertList']) ? $this->currentCert['tbsCertList']['issuer'] : $this->dn;
1821
        }
1822
 
1823
        switch ((int) $format) {
1824
            case self::DN_ARRAY:
1825
                return $dn;
1826
            case self::DN_ASN1:
1827
                $filters = [];
1828
                $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1829
                ASN1::setFilters($filters);
1830
                $this->mapOutDNs($dn, 'rdnSequence');
1831
                return ASN1::encodeDER($dn, Maps\Name::MAP);
1832
            case self::DN_CANON:
1833
                //  No SEQUENCE around RDNs and all string values normalized as
1834
                // trimmed lowercase UTF-8 with all spacing as one blank.
1835
                // constructed RDNs will not be canonicalized
1836
                $filters = [];
1837
                $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1838
                ASN1::setFilters($filters);
1839
                $result = '';
1840
                $this->mapOutDNs($dn, 'rdnSequence');
1841
                foreach ($dn['rdnSequence'] as $rdn) {
1842
                    foreach ($rdn as $i => $attr) {
1843
                        $attr = &$rdn[$i];
1844
                        if (is_array($attr['value'])) {
1845
                            foreach ($attr['value'] as $type => $v) {
1846
                                $type = array_search($type, ASN1::ANY_MAP, true);
1847
                                if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) {
1848
                                    $v = ASN1::convert($v, $type);
1849
                                    if ($v !== false) {
1850
                                        $v = preg_replace('/\s+/', ' ', $v);
1851
                                        $attr['value'] = strtolower(trim($v));
1852
                                        break;
1853
                                    }
1854
                                }
1855
                            }
1856
                        }
1857
                    }
1858
                    $result .= ASN1::encodeDER($rdn, Maps\RelativeDistinguishedName::MAP);
1859
                }
1860
                return $result;
1861
            case self::DN_HASH:
1862
                $dn = $this->getDN(self::DN_CANON, $dn);
1863
                $hash = new Hash('sha1');
1864
                $hash = $hash->hash($dn);
1865
                extract(unpack('Vhash', $hash));
1042 daniel-mar 1866
                return strtolower(Strings::bin2hex(pack('N', $hash)));
827 daniel-mar 1867
        }
1868
 
1869
        // Default is to return a string.
1870
        $start = true;
1871
        $output = '';
1872
 
1873
        $result = [];
1874
        $filters = [];
1875
        $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING];
1876
        ASN1::setFilters($filters);
1877
        $this->mapOutDNs($dn, 'rdnSequence');
1878
 
1879
        foreach ($dn['rdnSequence'] as $field) {
1880
            $prop = $field[0]['type'];
1881
            $value = $field[0]['value'];
1882
 
1883
            $delim = ', ';
1884
            switch ($prop) {
1885
                case 'id-at-countryName':
1886
                    $desc = 'C';
1887
                    break;
1888
                case 'id-at-stateOrProvinceName':
1889
                    $desc = 'ST';
1890
                    break;
1891
                case 'id-at-organizationName':
1892
                    $desc = 'O';
1893
                    break;
1894
                case 'id-at-organizationalUnitName':
1895
                    $desc = 'OU';
1896
                    break;
1897
                case 'id-at-commonName':
1898
                    $desc = 'CN';
1899
                    break;
1900
                case 'id-at-localityName':
1901
                    $desc = 'L';
1902
                    break;
1903
                case 'id-at-surname':
1904
                    $desc = 'SN';
1905
                    break;
1906
                case 'id-at-uniqueIdentifier':
1907
                    $delim = '/';
1908
                    $desc = 'x500UniqueIdentifier';
1909
                    break;
1910
                case 'id-at-postalAddress':
1911
                    $delim = '/';
1912
                    $desc = 'postalAddress';
1913
                    break;
1914
                default:
1915
                    $delim = '/';
1916
                    $desc = preg_replace('#.+-([^-]+)$#', '$1', $prop);
1917
            }
1918
 
1919
            if (!$start) {
1920
                $output .= $delim;
1921
            }
1922
            if (is_array($value)) {
1923
                foreach ($value as $type => $v) {
1924
                    $type = array_search($type, ASN1::ANY_MAP, true);
1925
                    if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) {
1926
                        $v = ASN1::convert($v, $type);
1927
                        if ($v !== false) {
1928
                            $value = $v;
1929
                            break;
1930
                        }
1931
                    }
1932
                }
1933
                if (is_array($value)) {
1934
                    $value = array_pop($value); // Always strip data type.
1935
                }
1936
            } elseif (is_object($value) && $value instanceof Element) {
1937
                $callback = function ($x) {
1938
                    return '\x' . bin2hex($x[0]);
1939
                };
1940
                $value = strtoupper(preg_replace_callback('#[^\x20-\x7E]#', $callback, $value->element));
1941
            }
1942
            $output .= $desc . '=' . $value;
1943
            $result[$desc] = isset($result[$desc]) ?
1944
                array_merge((array) $result[$desc], [$value]) :
1945
                $value;
1946
            $start = false;
1947
        }
1948
 
1949
        return $format == self::DN_OPENSSL ? $result : $output;
1950
    }
1951
 
1952
    /**
1953
     * Get the Distinguished Name for a certificate/crl issuer
1954
     *
1955
     * @param int $format optional
1956
     * @return mixed
1957
     */
1958
    public function getIssuerDN($format = self::DN_ARRAY)
1959
    {
1960
        switch (true) {
1961
            case !isset($this->currentCert) || !is_array($this->currentCert):
1962
                break;
1963
            case isset($this->currentCert['tbsCertificate']):
1964
                return $this->getDN($format, $this->currentCert['tbsCertificate']['issuer']);
1965
            case isset($this->currentCert['tbsCertList']):
1966
                return $this->getDN($format, $this->currentCert['tbsCertList']['issuer']);
1967
        }
1968
 
1969
        return false;
1970
    }
1971
 
1972
    /**
1973
     * Get the Distinguished Name for a certificate/csr subject
1974
     * Alias of getDN()
1975
     *
1976
     * @param int $format optional
1977
     * @return mixed
1978
     */
1979
    public function getSubjectDN($format = self::DN_ARRAY)
1980
    {
1981
        switch (true) {
1982
            case !empty($this->dn):
1983
                return $this->getDN($format);
1984
            case !isset($this->currentCert) || !is_array($this->currentCert):
1985
                break;
1986
            case isset($this->currentCert['tbsCertificate']):
1987
                return $this->getDN($format, $this->currentCert['tbsCertificate']['subject']);
1988
            case isset($this->currentCert['certificationRequestInfo']):
1989
                return $this->getDN($format, $this->currentCert['certificationRequestInfo']['subject']);
1990
        }
1991
 
1992
        return false;
1993
    }
1994
 
1995
    /**
1996
     * Get an individual Distinguished Name property for a certificate/crl issuer
1997
     *
1998
     * @param string $propName
1999
     * @param bool $withType optional
2000
     * @return mixed
2001
     */
2002
    public function getIssuerDNProp($propName, $withType = false)
2003
    {
2004
        switch (true) {
2005
            case !isset($this->currentCert) || !is_array($this->currentCert):
2006
                break;
2007
            case isset($this->currentCert['tbsCertificate']):
2008
                return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['issuer'], $withType);
2009
            case isset($this->currentCert['tbsCertList']):
2010
                return $this->getDNProp($propName, $this->currentCert['tbsCertList']['issuer'], $withType);
2011
        }
2012
 
2013
        return false;
2014
    }
2015
 
2016
    /**
2017
     * Get an individual Distinguished Name property for a certificate/csr subject
2018
     *
2019
     * @param string $propName
2020
     * @param bool $withType optional
2021
     * @return mixed
2022
     */
2023
    public function getSubjectDNProp($propName, $withType = false)
2024
    {
2025
        switch (true) {
2026
            case !empty($this->dn):
2027
                return $this->getDNProp($propName, null, $withType);
2028
            case !isset($this->currentCert) || !is_array($this->currentCert):
2029
                break;
2030
            case isset($this->currentCert['tbsCertificate']):
2031
                return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['subject'], $withType);
2032
            case isset($this->currentCert['certificationRequestInfo']):
2033
                return $this->getDNProp($propName, $this->currentCert['certificationRequestInfo']['subject'], $withType);
2034
        }
2035
 
2036
        return false;
2037
    }
2038
 
2039
    /**
2040
     * Get the certificate chain for the current cert
2041
     *
2042
     * @return mixed
2043
     */
2044
    public function getChain()
2045
    {
2046
        $chain = [$this->currentCert];
2047
 
2048
        if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) {
2049
            return false;
2050
        }
2051
        while (true) {
2052
            $currentCert = $chain[count($chain) - 1];
2053
            for ($i = 0; $i < count($this->CAs); $i++) {
2054
                $ca = $this->CAs[$i];
2055
                if ($currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']) {
2056
                    $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier', $currentCert);
2057
                    $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca);
2058
                    switch (true) {
2059
                        case !is_array($authorityKey):
2060
                        case is_array($authorityKey) && isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID:
2061
                            if ($currentCert === $ca) {
2062
                                break 3;
2063
                            }
2064
                            $chain[] = $ca;
2065
                            break 2;
2066
                    }
2067
                }
2068
            }
2069
            if ($i == count($this->CAs)) {
2070
                break;
2071
            }
2072
        }
2073
        foreach ($chain as $key => $value) {
2074
            $chain[$key] = new X509();
2075
            $chain[$key]->loadX509($value);
2076
        }
2077
        return $chain;
2078
    }
2079
 
2080
    /**
2081
     * Returns the current cert
2082
     *
2083
     * @return array|bool
2084
     */
2085
    public function &getCurrentCert()
2086
    {
2087
        return $this->currentCert;
2088
    }
2089
 
2090
    /**
2091
     * Set public key
2092
     *
2093
     * Key needs to be a \phpseclib3\Crypt\RSA object
2094
     *
2095
     * @param PublicKey $key
2096
     * @return void
2097
     */
2098
    public function setPublicKey(PublicKey $key)
2099
    {
2100
        $this->publicKey = $key;
2101
    }
2102
 
2103
    /**
2104
     * Set private key
2105
     *
2106
     * Key needs to be a \phpseclib3\Crypt\RSA object
2107
     *
2108
     * @param PrivateKey $key
2109
     */
2110
    public function setPrivateKey(PrivateKey $key)
2111
    {
2112
        $this->privateKey = $key;
2113
    }
2114
 
2115
    /**
2116
     * Set challenge
2117
     *
2118
     * Used for SPKAC CSR's
2119
     *
2120
     * @param string $challenge
2121
     */
2122
    public function setChallenge($challenge)
2123
    {
2124
        $this->challenge = $challenge;
2125
    }
2126
 
2127
    /**
2128
     * Gets the public key
2129
     *
2130
     * Returns a \phpseclib3\Crypt\RSA object or a false.
2131
     *
2132
     * @return mixed
2133
     */
2134
    public function getPublicKey()
2135
    {
2136
        if (isset($this->publicKey)) {
2137
            return $this->publicKey;
2138
        }
2139
 
2140
        if (isset($this->currentCert) && is_array($this->currentCert)) {
2141
            $paths = [
2142
                'tbsCertificate/subjectPublicKeyInfo',
2143
                'certificationRequestInfo/subjectPKInfo',
2144
                'publicKeyAndChallenge/spki'
2145
            ];
2146
            foreach ($paths as $path) {
2147
                $keyinfo = $this->subArray($this->currentCert, $path);
2148
                if (!empty($keyinfo)) {
2149
                    break;
2150
                }
2151
            }
2152
        }
2153
        if (empty($keyinfo)) {
2154
            return false;
2155
        }
2156
 
2157
        $key = $keyinfo['subjectPublicKey'];
2158
 
2159
        switch ($keyinfo['algorithm']['algorithm']) {
2160
            case 'id-RSASSA-PSS':
2161
                return RSA::loadFormat('PSS', $key);
2162
            case 'rsaEncryption':
2163
                return RSA::loadFormat('PKCS8', $key)->withPadding(RSA::SIGNATURE_PKCS1);
2164
            case 'id-ecPublicKey':
2165
            case 'id-Ed25519':
2166
            case 'id-Ed448':
2167
                return EC::loadFormat('PKCS8', $key);
2168
            case 'id-dsa':
2169
                return DSA::loadFormat('PKCS8', $key);
2170
        }
2171
 
2172
        return false;
2173
    }
2174
 
2175
    /**
2176
     * Load a Certificate Signing Request
2177
     *
2178
     * @param string $csr
2179
     * @param int $mode
2180
     * @return mixed
2181
     */
2182
    public function loadCSR($csr, $mode = self::FORMAT_AUTO_DETECT)
2183
    {
2184
        if (is_array($csr) && isset($csr['certificationRequestInfo'])) {
2185
            unset($this->currentCert);
2186
            unset($this->currentKeyIdentifier);
2187
            unset($this->signatureSubject);
2188
            $this->dn = $csr['certificationRequestInfo']['subject'];
2189
            if (!isset($this->dn)) {
2190
                return false;
2191
            }
2192
 
2193
            $this->currentCert = $csr;
2194
            return $csr;
2195
        }
2196
 
2197
        // see http://tools.ietf.org/html/rfc2986
2198
 
2199
        if ($mode != self::FORMAT_DER) {
2200
            $newcsr = ASN1::extractBER($csr);
2201
            if ($mode == self::FORMAT_PEM && $csr == $newcsr) {
2202
                return false;
2203
            }
2204
            $csr = $newcsr;
2205
        }
2206
        $orig = $csr;
2207
 
2208
        if ($csr === false) {
2209
            $this->currentCert = false;
2210
            return false;
2211
        }
2212
 
2213
        $decoded = ASN1::decodeBER($csr);
2214
 
1042 daniel-mar 2215
        if (!$decoded) {
827 daniel-mar 2216
            $this->currentCert = false;
2217
            return false;
2218
        }
2219
 
2220
        $csr = ASN1::asn1map($decoded[0], Maps\CertificationRequest::MAP);
2221
        if (!isset($csr) || $csr === false) {
2222
            $this->currentCert = false;
2223
            return false;
2224
        }
2225
 
2226
        $this->mapInAttributes($csr, 'certificationRequestInfo/attributes');
2227
        $this->mapInDNs($csr, 'certificationRequestInfo/subject/rdnSequence');
2228
 
2229
        $this->dn = $csr['certificationRequestInfo']['subject'];
2230
 
2231
        $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']);
2232
 
2233
        $key = $csr['certificationRequestInfo']['subjectPKInfo'];
2234
        $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP);
2235
        $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'] =
2236
            "-----BEGIN PUBLIC KEY-----\r\n" .
2237
            chunk_split(base64_encode($key), 64) .
2238
            "-----END PUBLIC KEY-----";
2239
 
2240
        $this->currentKeyIdentifier = null;
2241
        $this->currentCert = $csr;
2242
 
2243
        $this->publicKey = null;
2244
        $this->publicKey = $this->getPublicKey();
2245
 
2246
        return $csr;
2247
    }
2248
 
2249
    /**
2250
     * Save CSR request
2251
     *
2252
     * @param array $csr
2253
     * @param int $format optional
2254
     * @return string
2255
     */
1042 daniel-mar 2256
    public function saveCSR(array $csr, $format = self::FORMAT_PEM)
827 daniel-mar 2257
    {
2258
        if (!is_array($csr) || !isset($csr['certificationRequestInfo'])) {
2259
            return false;
2260
        }
2261
 
2262
        switch (true) {
2263
            case !($algorithm = $this->subArray($csr, 'certificationRequestInfo/subjectPKInfo/algorithm/algorithm')):
2264
            case is_object($csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']):
2265
                break;
2266
            default:
2267
                $csr['certificationRequestInfo']['subjectPKInfo'] = new Element(
2268
                    base64_decode(preg_replace('#-.+-|[\r\n]#', '', $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']))
2269
                );
2270
        }
2271
 
2272
        $filters = [];
2273
        $filters['certificationRequestInfo']['subject']['rdnSequence']['value']
2274
            = ['type' => ASN1::TYPE_UTF8_STRING];
2275
 
2276
        ASN1::setFilters($filters);
2277
 
2278
        $this->mapOutDNs($csr, 'certificationRequestInfo/subject/rdnSequence');
2279
        $this->mapOutAttributes($csr, 'certificationRequestInfo/attributes');
2280
        $csr = ASN1::encodeDER($csr, Maps\CertificationRequest::MAP);
2281
 
2282
        switch ($format) {
2283
            case self::FORMAT_DER:
2284
                return $csr;
2285
            // case self::FORMAT_PEM:
2286
            default:
1042 daniel-mar 2287
                return "-----BEGIN CERTIFICATE REQUEST-----\r\n" . chunk_split(Strings::base64_encode($csr), 64) . '-----END CERTIFICATE REQUEST-----';
827 daniel-mar 2288
        }
2289
    }
2290
 
2291
    /**
2292
     * Load a SPKAC CSR
2293
     *
2294
     * SPKAC's are produced by the HTML5 keygen element:
2295
     *
2296
     * https://developer.mozilla.org/en-US/docs/HTML/Element/keygen
2297
     *
2298
     * @param string $spkac
2299
     * @return mixed
2300
     */
2301
    public function loadSPKAC($spkac)
2302
    {
2303
        if (is_array($spkac) && isset($spkac['publicKeyAndChallenge'])) {
2304
            unset($this->currentCert);
2305
            unset($this->currentKeyIdentifier);
2306
            unset($this->signatureSubject);
2307
            $this->currentCert = $spkac;
2308
            return $spkac;
2309
        }
2310
 
2311
        // see http://www.w3.org/html/wg/drafts/html/master/forms.html#signedpublickeyandchallenge
2312
 
2313
        // OpenSSL produces SPKAC's that are preceded by the string SPKAC=
2314
        $temp = preg_replace('#(?:SPKAC=)|[ \r\n\\\]#', '', $spkac);
1042 daniel-mar 2315
        $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? Strings::base64_decode($temp) : false;
827 daniel-mar 2316
        if ($temp != false) {
2317
            $spkac = $temp;
2318
        }
2319
        $orig = $spkac;
2320
 
2321
        if ($spkac === false) {
2322
            $this->currentCert = false;
2323
            return false;
2324
        }
2325
 
2326
        $decoded = ASN1::decodeBER($spkac);
2327
 
1042 daniel-mar 2328
        if (!$decoded) {
827 daniel-mar 2329
            $this->currentCert = false;
2330
            return false;
2331
        }
2332
 
2333
        $spkac = ASN1::asn1map($decoded[0], Maps\SignedPublicKeyAndChallenge::MAP);
2334
 
2335
        if (!isset($spkac) || !is_array($spkac)) {
2336
            $this->currentCert = false;
2337
            return false;
2338
        }
2339
 
2340
        $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']);
2341
 
2342
        $key = $spkac['publicKeyAndChallenge']['spki'];
2343
        $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP);
2344
        $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey'] =
2345
            "-----BEGIN PUBLIC KEY-----\r\n" .
2346
            chunk_split(base64_encode($key), 64) .
2347
            "-----END PUBLIC KEY-----";
2348
 
2349
        $this->currentKeyIdentifier = null;
2350
        $this->currentCert = $spkac;
2351
 
2352
        $this->publicKey = null;
2353
        $this->publicKey = $this->getPublicKey();
2354
 
2355
        return $spkac;
2356
    }
2357
 
2358
    /**
2359
     * Save a SPKAC CSR request
2360
     *
2361
     * @param array $spkac
2362
     * @param int $format optional
2363
     * @return string
2364
     */
1042 daniel-mar 2365
    public function saveSPKAC(array $spkac, $format = self::FORMAT_PEM)
827 daniel-mar 2366
    {
2367
        if (!is_array($spkac) || !isset($spkac['publicKeyAndChallenge'])) {
2368
            return false;
2369
        }
2370
 
2371
        $algorithm = $this->subArray($spkac, 'publicKeyAndChallenge/spki/algorithm/algorithm');
2372
        switch (true) {
2373
            case !$algorithm:
2374
            case is_object($spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']):
2375
                break;
2376
            default:
2377
                $spkac['publicKeyAndChallenge']['spki'] = new Element(
2378
                    base64_decode(preg_replace('#-.+-|[\r\n]#', '', $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']))
2379
                );
2380
        }
2381
 
2382
        $spkac = ASN1::encodeDER($spkac, Maps\SignedPublicKeyAndChallenge::MAP);
2383
 
2384
        switch ($format) {
2385
            case self::FORMAT_DER:
2386
                return $spkac;
2387
            // case self::FORMAT_PEM:
2388
            default:
2389
                // OpenSSL's implementation of SPKAC requires the SPKAC be preceded by SPKAC= and since there are pretty much
2390
                // no other SPKAC decoders phpseclib will use that same format
1042 daniel-mar 2391
                return 'SPKAC=' . Strings::base64_encode($spkac);
827 daniel-mar 2392
        }
2393
    }
2394
 
2395
    /**
2396
     * Load a Certificate Revocation List
2397
     *
2398
     * @param string $crl
2399
     * @param int $mode
2400
     * @return mixed
2401
     */
2402
    public function loadCRL($crl, $mode = self::FORMAT_AUTO_DETECT)
2403
    {
2404
        if (is_array($crl) && isset($crl['tbsCertList'])) {
2405
            $this->currentCert = $crl;
2406
            unset($this->signatureSubject);
2407
            return $crl;
2408
        }
2409
 
2410
        if ($mode != self::FORMAT_DER) {
2411
            $newcrl = ASN1::extractBER($crl);
2412
            if ($mode == self::FORMAT_PEM && $crl == $newcrl) {
2413
                return false;
2414
            }
2415
            $crl = $newcrl;
2416
        }
2417
        $orig = $crl;
2418
 
2419
        if ($crl === false) {
2420
            $this->currentCert = false;
2421
            return false;
2422
        }
2423
 
2424
        $decoded = ASN1::decodeBER($crl);
2425
 
1042 daniel-mar 2426
        if (!$decoded) {
827 daniel-mar 2427
            $this->currentCert = false;
2428
            return false;
2429
        }
2430
 
2431
        $crl = ASN1::asn1map($decoded[0], Maps\CertificateList::MAP);
2432
        if (!isset($crl) || $crl === false) {
2433
            $this->currentCert = false;
2434
            return false;
2435
        }
2436
 
2437
        $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']);
2438
 
2439
        $this->mapInDNs($crl, 'tbsCertList/issuer/rdnSequence');
2440
        if ($this->isSubArrayValid($crl, 'tbsCertList/crlExtensions')) {
2441
            $this->mapInExtensions($crl, 'tbsCertList/crlExtensions');
2442
        }
2443
        if ($this->isSubArrayValid($crl, 'tbsCertList/revokedCertificates')) {
2444
            $rclist_ref = &$this->subArrayUnchecked($crl, 'tbsCertList/revokedCertificates');
2445
            if ($rclist_ref) {
2446
                $rclist = $crl['tbsCertList']['revokedCertificates'];
2447
                foreach ($rclist as $i => $extension) {
2448
                    if ($this->isSubArrayValid($rclist, "$i/crlEntryExtensions")) {
2449
                        $this->mapInExtensions($rclist_ref, "$i/crlEntryExtensions");
2450
                    }
2451
                }
2452
            }
2453
        }
2454
 
2455
        $this->currentKeyIdentifier = null;
2456
        $this->currentCert = $crl;
2457
 
2458
        return $crl;
2459
    }
2460
 
2461
    /**
2462
     * Save Certificate Revocation List.
2463
     *
2464
     * @param array $crl
2465
     * @param int $format optional
2466
     * @return string
2467
     */
1042 daniel-mar 2468
    public function saveCRL(array $crl, $format = self::FORMAT_PEM)
827 daniel-mar 2469
    {
2470
        if (!is_array($crl) || !isset($crl['tbsCertList'])) {
2471
            return false;
2472
        }
2473
 
2474
        $filters = [];
2475
        $filters['tbsCertList']['issuer']['rdnSequence']['value']
2476
            = ['type' => ASN1::TYPE_UTF8_STRING];
2477
        $filters['tbsCertList']['signature']['parameters']
2478
            = ['type' => ASN1::TYPE_UTF8_STRING];
2479
        $filters['signatureAlgorithm']['parameters']
2480
            = ['type' => ASN1::TYPE_UTF8_STRING];
2481
 
2482
        if (empty($crl['tbsCertList']['signature']['parameters'])) {
2483
            $filters['tbsCertList']['signature']['parameters']
2484
                = ['type' => ASN1::TYPE_NULL];
2485
        }
2486
 
2487
        if (empty($crl['signatureAlgorithm']['parameters'])) {
2488
            $filters['signatureAlgorithm']['parameters']
2489
                = ['type' => ASN1::TYPE_NULL];
2490
        }
2491
 
2492
        ASN1::setFilters($filters);
2493
 
2494
        $this->mapOutDNs($crl, 'tbsCertList/issuer/rdnSequence');
2495
        $this->mapOutExtensions($crl, 'tbsCertList/crlExtensions');
2496
        $rclist = &$this->subArray($crl, 'tbsCertList/revokedCertificates');
2497
        if (is_array($rclist)) {
2498
            foreach ($rclist as $i => $extension) {
2499
                $this->mapOutExtensions($rclist, "$i/crlEntryExtensions");
2500
            }
2501
        }
2502
 
2503
        $crl = ASN1::encodeDER($crl, Maps\CertificateList::MAP);
2504
 
2505
        switch ($format) {
2506
            case self::FORMAT_DER:
2507
                return $crl;
2508
            // case self::FORMAT_PEM:
2509
            default:
1042 daniel-mar 2510
                return "-----BEGIN X509 CRL-----\r\n" . chunk_split(Strings::base64_encode($crl), 64) . '-----END X509 CRL-----';
827 daniel-mar 2511
        }
2512
    }
2513
 
2514
    /**
2515
     * Helper function to build a time field according to RFC 3280 section
2516
     *  - 4.1.2.5 Validity
2517
     *  - 5.1.2.4 This Update
2518
     *  - 5.1.2.5 Next Update
2519
     *  - 5.1.2.6 Revoked Certificates
2520
     * by choosing utcTime iff year of date given is before 2050 and generalTime else.
2521
     *
2522
     * @param string $date in format date('D, d M Y H:i:s O')
2523
     * @return array|Element
2524
     */
2525
    private function timeField($date)
2526
    {
2527
        if ($date instanceof Element) {
2528
            return $date;
2529
        }
2530
        $dateObj = new \DateTimeImmutable($date, new \DateTimeZone('GMT'));
2531
        $year = $dateObj->format('Y'); // the same way ASN1.php parses this
2532
        if ($year < 2050) {
2533
            return ['utcTime' => $date];
2534
        } else {
2535
            return ['generalTime' => $date];
2536
        }
2537
    }
2538
 
2539
    /**
2540
     * Sign an X.509 certificate
2541
     *
2542
     * $issuer's private key needs to be loaded.
2543
     * $subject can be either an existing X.509 cert (if you want to resign it),
2544
     * a CSR or something with the DN and public key explicitly set.
2545
     *
2546
     * @return mixed
2547
     */
1042 daniel-mar 2548
    public function sign(X509 $issuer, X509 $subject)
827 daniel-mar 2549
    {
2550
        if (!is_object($issuer->privateKey) || empty($issuer->dn)) {
2551
            return false;
2552
        }
2553
 
2554
        if (isset($subject->publicKey) && !($subjectPublicKey = $subject->formatSubjectPublicKey())) {
2555
            return false;
2556
        }
2557
 
2558
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2559
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2560
        $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey);
2561
 
2562
        if (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertificate'])) {
2563
            $this->currentCert = $subject->currentCert;
2564
            $this->currentCert['tbsCertificate']['signature'] = $signatureAlgorithm;
2565
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
2566
 
2567
            if (!empty($this->startDate)) {
2568
                $this->currentCert['tbsCertificate']['validity']['notBefore'] = $this->timeField($this->startDate);
2569
            }
2570
            if (!empty($this->endDate)) {
2571
                $this->currentCert['tbsCertificate']['validity']['notAfter'] = $this->timeField($this->endDate);
2572
            }
2573
            if (!empty($this->serialNumber)) {
2574
                $this->currentCert['tbsCertificate']['serialNumber'] = $this->serialNumber;
2575
            }
2576
            if (!empty($subject->dn)) {
2577
                $this->currentCert['tbsCertificate']['subject'] = $subject->dn;
2578
            }
2579
            if (!empty($subject->publicKey)) {
2580
                $this->currentCert['tbsCertificate']['subjectPublicKeyInfo'] = $subjectPublicKey;
2581
            }
2582
            $this->removeExtension('id-ce-authorityKeyIdentifier');
2583
            if (isset($subject->domains)) {
2584
                $this->removeExtension('id-ce-subjectAltName');
2585
            }
2586
        } elseif (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertList'])) {
2587
            return false;
2588
        } else {
2589
            if (!isset($subject->publicKey)) {
2590
                return false;
2591
            }
2592
 
2593
            $startDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
2594
            $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O');
2595
 
2596
            $endDate = new \DateTimeImmutable('+1 year', new \DateTimeZone(@date_default_timezone_get()));
2597
            $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O');
2598
 
2599
            /* "The serial number MUST be a positive integer"
2600
               "Conforming CAs MUST NOT use serialNumber values longer than 20 octets."
2601
                -- https://tools.ietf.org/html/rfc5280#section-4.1.2.2
2602
 
2603
               for the integer to be positive the leading bit needs to be 0 hence the
2604
               application of a bitmap
2605
            */
2606
            $serialNumber = !empty($this->serialNumber) ?
2607
                $this->serialNumber :
2608
                new BigInteger(Random::string(20) & ("\x7F" . str_repeat("\xFF", 19)), 256);
2609
 
2610
            $this->currentCert = [
2611
                'tbsCertificate' =>
2612
                    [
2613
                        'version' => 'v3',
2614
                        'serialNumber' => $serialNumber, // $this->setSerialNumber()
2615
                        'signature' => $signatureAlgorithm,
2616
                        'issuer' => false, // this is going to be overwritten later
2617
                        'validity' => [
2618
                            'notBefore' => $this->timeField($startDate), // $this->setStartDate()
2619
                            'notAfter' => $this->timeField($endDate)   // $this->setEndDate()
2620
                        ],
2621
                        'subject' => $subject->dn,
2622
                        'subjectPublicKeyInfo' => $subjectPublicKey
2623
                    ],
2624
                    'signatureAlgorithm' => $signatureAlgorithm,
2625
                    'signature'          => false // this is going to be overwritten later
2626
            ];
2627
 
2628
            // Copy extensions from CSR.
2629
            $csrexts = $subject->getAttribute('pkcs-9-at-extensionRequest', 0);
2630
 
2631
            if (!empty($csrexts)) {
2632
                $this->currentCert['tbsCertificate']['extensions'] = $csrexts;
2633
            }
2634
        }
2635
 
2636
        $this->currentCert['tbsCertificate']['issuer'] = $issuer->dn;
2637
 
2638
        if (isset($issuer->currentKeyIdentifier)) {
2639
            $this->setExtension('id-ce-authorityKeyIdentifier', [
2640
                    //'authorityCertIssuer' => array(
2641
                    //    array(
2642
                    //        'directoryName' => $issuer->dn
2643
                    //    )
2644
                    //),
2645
                    'keyIdentifier' => $issuer->currentKeyIdentifier
2646
                ]);
2647
            //$extensions = &$this->currentCert['tbsCertificate']['extensions'];
2648
            //if (isset($issuer->serialNumber)) {
2649
            //    $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber;
2650
            //}
2651
            //unset($extensions);
2652
        }
2653
 
2654
        if (isset($subject->currentKeyIdentifier)) {
2655
            $this->setExtension('id-ce-subjectKeyIdentifier', $subject->currentKeyIdentifier);
2656
        }
2657
 
2658
        $altName = [];
2659
 
2660
        if (isset($subject->domains) && count($subject->domains)) {
2661
            $altName = array_map(['\phpseclib3\File\X509', 'dnsName'], $subject->domains);
2662
        }
2663
 
2664
        if (isset($subject->ipAddresses) && count($subject->ipAddresses)) {
2665
            // should an IP address appear as the CN if no domain name is specified? idk
2666
            //$ips = count($subject->domains) ? $subject->ipAddresses : array_slice($subject->ipAddresses, 1);
2667
            $ipAddresses = [];
2668
            foreach ($subject->ipAddresses as $ipAddress) {
2669
                $encoded = $subject->ipAddress($ipAddress);
2670
                if ($encoded !== false) {
2671
                    $ipAddresses[] = $encoded;
2672
                }
2673
            }
2674
            if (count($ipAddresses)) {
2675
                $altName = array_merge($altName, $ipAddresses);
2676
            }
2677
        }
2678
 
2679
        if (!empty($altName)) {
2680
            $this->setExtension('id-ce-subjectAltName', $altName);
2681
        }
2682
 
2683
        if ($this->caFlag) {
2684
            $keyUsage = $this->getExtension('id-ce-keyUsage');
2685
            if (!$keyUsage) {
2686
                $keyUsage = [];
2687
            }
2688
 
2689
            $this->setExtension(
2690
                'id-ce-keyUsage',
2691
                array_values(array_unique(array_merge($keyUsage, ['cRLSign', 'keyCertSign'])))
2692
            );
2693
 
2694
            $basicConstraints = $this->getExtension('id-ce-basicConstraints');
2695
            if (!$basicConstraints) {
2696
                $basicConstraints = [];
2697
            }
2698
 
2699
            $this->setExtension(
2700
                'id-ce-basicConstraints',
2701
                array_merge(['cA' => true], $basicConstraints),
2702
                true
2703
            );
2704
 
2705
            if (!isset($subject->currentKeyIdentifier)) {
2706
                $this->setExtension('id-ce-subjectKeyIdentifier', $this->computeKeyIdentifier($this->currentCert), false, false);
2707
            }
2708
        }
2709
 
2710
        // resync $this->signatureSubject
2711
        // save $tbsCertificate in case there are any \phpseclib3\File\ASN1\Element objects in it
2712
        $tbsCertificate = $this->currentCert['tbsCertificate'];
2713
        $this->loadX509($this->saveX509($this->currentCert));
2714
 
2715
        $result = $this->currentCert;
2716
        $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject);
2717
        $result['tbsCertificate'] = $tbsCertificate;
2718
 
2719
        $this->currentCert = $currentCert;
2720
        $this->signatureSubject = $signatureSubject;
2721
 
2722
        return $result;
2723
    }
2724
 
2725
    /**
2726
     * Sign a CSR
2727
     *
2728
     * @return mixed
2729
     */
2730
    public function signCSR()
2731
    {
2732
        if (!is_object($this->privateKey) || empty($this->dn)) {
2733
            return false;
2734
        }
2735
 
2736
        $origPublicKey = $this->publicKey;
2737
        $this->publicKey = $this->privateKey->getPublicKey();
2738
        $publicKey = $this->formatSubjectPublicKey();
2739
        $this->publicKey = $origPublicKey;
2740
 
2741
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2742
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2743
        $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey);
2744
 
2745
        if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['certificationRequestInfo'])) {
1042 daniel-mar 2746
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
827 daniel-mar 2747
            if (!empty($this->dn)) {
2748
                $this->currentCert['certificationRequestInfo']['subject'] = $this->dn;
2749
            }
2750
            $this->currentCert['certificationRequestInfo']['subjectPKInfo'] = $publicKey;
2751
        } else {
2752
            $this->currentCert = [
2753
                'certificationRequestInfo' =>
2754
                    [
2755
                        'version' => 'v1',
2756
                        'subject' => $this->dn,
2757
                        'subjectPKInfo' => $publicKey
2758
                    ],
1042 daniel-mar 2759
                    'signatureAlgorithm' => $signatureAlgorithm,
827 daniel-mar 2760
                    'signature'          => false // this is going to be overwritten later
2761
            ];
2762
        }
2763
 
2764
        // resync $this->signatureSubject
2765
        // save $certificationRequestInfo in case there are any \phpseclib3\File\ASN1\Element objects in it
2766
        $certificationRequestInfo = $this->currentCert['certificationRequestInfo'];
2767
        $this->loadCSR($this->saveCSR($this->currentCert));
2768
 
2769
        $result = $this->currentCert;
2770
        $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject);
2771
        $result['certificationRequestInfo'] = $certificationRequestInfo;
2772
 
2773
        $this->currentCert = $currentCert;
2774
        $this->signatureSubject = $signatureSubject;
2775
 
2776
        return $result;
2777
    }
2778
 
2779
    /**
2780
     * Sign a SPKAC
2781
     *
2782
     * @return mixed
2783
     */
2784
    public function signSPKAC()
2785
    {
2786
        if (!is_object($this->privateKey)) {
2787
            return false;
2788
        }
2789
 
2790
        $origPublicKey = $this->publicKey;
2791
        $this->publicKey = $this->privateKey->getPublicKey();
2792
        $publicKey = $this->formatSubjectPublicKey();
2793
        $this->publicKey = $origPublicKey;
2794
 
2795
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2796
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2797
        $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey);
2798
 
2799
        // re-signing a SPKAC seems silly but since everything else supports re-signing why not?
2800
        if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['publicKeyAndChallenge'])) {
1042 daniel-mar 2801
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
827 daniel-mar 2802
            $this->currentCert['publicKeyAndChallenge']['spki'] = $publicKey;
2803
            if (!empty($this->challenge)) {
2804
                // the bitwise AND ensures that the output is a valid IA5String
2805
                $this->currentCert['publicKeyAndChallenge']['challenge'] = $this->challenge & str_repeat("\x7F", strlen($this->challenge));
2806
            }
2807
        } else {
2808
            $this->currentCert = [
2809
                'publicKeyAndChallenge' =>
2810
                    [
2811
                        'spki' => $publicKey,
2812
                        // quoting <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen>,
2813
                        // "A challenge string that is submitted along with the public key. Defaults to an empty string if not specified."
2814
                        // both Firefox and OpenSSL ("openssl spkac -key private.key") behave this way
2815
                        // we could alternatively do this instead if we ignored the specs:
2816
                        // Random::string(8) & str_repeat("\x7F", 8)
2817
                        'challenge' => !empty($this->challenge) ? $this->challenge : ''
2818
                    ],
1042 daniel-mar 2819
                    'signatureAlgorithm' => $signatureAlgorithm,
827 daniel-mar 2820
                    'signature'          => false // this is going to be overwritten later
2821
            ];
2822
        }
2823
 
2824
        // resync $this->signatureSubject
2825
        // save $publicKeyAndChallenge in case there are any \phpseclib3\File\ASN1\Element objects in it
2826
        $publicKeyAndChallenge = $this->currentCert['publicKeyAndChallenge'];
2827
        $this->loadSPKAC($this->saveSPKAC($this->currentCert));
2828
 
2829
        $result = $this->currentCert;
2830
        $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject);
2831
        $result['publicKeyAndChallenge'] = $publicKeyAndChallenge;
2832
 
2833
        $this->currentCert = $currentCert;
2834
        $this->signatureSubject = $signatureSubject;
2835
 
2836
        return $result;
2837
    }
2838
 
2839
    /**
2840
     * Sign a CRL
2841
     *
2842
     * $issuer's private key needs to be loaded.
2843
     *
2844
     * @return mixed
2845
     */
1042 daniel-mar 2846
    public function signCRL(X509 $issuer, X509 $crl)
827 daniel-mar 2847
    {
2848
        if (!is_object($issuer->privateKey) || empty($issuer->dn)) {
2849
            return false;
2850
        }
2851
 
2852
        $currentCert = isset($this->currentCert) ? $this->currentCert : null;
2853
        $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null;
2854
        $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey);
2855
 
2856
        $thisUpdate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
2857
        $thisUpdate = !empty($this->startDate) ? $this->startDate : $thisUpdate->format('D, d M Y H:i:s O');
2858
 
2859
        if (isset($crl->currentCert) && is_array($crl->currentCert) && isset($crl->currentCert['tbsCertList'])) {
2860
            $this->currentCert = $crl->currentCert;
1042 daniel-mar 2861
            $this->currentCert['tbsCertList']['signature'] = $signatureAlgorithm;
2862
            $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm;
827 daniel-mar 2863
        } else {
2864
            $this->currentCert = [
2865
                'tbsCertList' =>
2866
                    [
2867
                        'version' => 'v2',
1042 daniel-mar 2868
                        'signature' => $signatureAlgorithm,
827 daniel-mar 2869
                        'issuer' => false, // this is going to be overwritten later
2870
                        'thisUpdate' => $this->timeField($thisUpdate) // $this->setStartDate()
2871
                    ],
1042 daniel-mar 2872
                    'signatureAlgorithm' => $signatureAlgorithm,
827 daniel-mar 2873
                    'signature'          => false // this is going to be overwritten later
2874
            ];
2875
        }
2876
 
2877
        $tbsCertList = &$this->currentCert['tbsCertList'];
2878
        $tbsCertList['issuer'] = $issuer->dn;
2879
        $tbsCertList['thisUpdate'] = $this->timeField($thisUpdate);
2880
 
2881
        if (!empty($this->endDate)) {
2882
            $tbsCertList['nextUpdate'] = $this->timeField($this->endDate); // $this->setEndDate()
2883
        } else {
2884
            unset($tbsCertList['nextUpdate']);
2885
        }
2886
 
2887
        if (!empty($this->serialNumber)) {
2888
            $crlNumber = $this->serialNumber;
2889
        } else {
2890
            $crlNumber = $this->getExtension('id-ce-cRLNumber');
2891
            // "The CRL number is a non-critical CRL extension that conveys a
2892
            //  monotonically increasing sequence number for a given CRL scope and
2893
            //  CRL issuer.  This extension allows users to easily determine when a
2894
            //  particular CRL supersedes another CRL."
2895
            // -- https://tools.ietf.org/html/rfc5280#section-5.2.3
2896
            $crlNumber = $crlNumber !== false ? $crlNumber->add(new BigInteger(1)) : null;
2897
        }
2898
 
2899
        $this->removeExtension('id-ce-authorityKeyIdentifier');
2900
        $this->removeExtension('id-ce-issuerAltName');
2901
 
2902
        // Be sure version >= v2 if some extension found.
2903
        $version = isset($tbsCertList['version']) ? $tbsCertList['version'] : 0;
2904
        if (!$version) {
2905
            if (!empty($tbsCertList['crlExtensions'])) {
2906
                $version = 1; // v2.
2907
            } elseif (!empty($tbsCertList['revokedCertificates'])) {
2908
                foreach ($tbsCertList['revokedCertificates'] as $cert) {
2909
                    if (!empty($cert['crlEntryExtensions'])) {
2910
                        $version = 1; // v2.
2911
                    }
2912
                }
2913
            }
2914
 
2915
            if ($version) {
2916
                $tbsCertList['version'] = $version;
2917
            }
2918
        }
2919
 
2920
        // Store additional extensions.
2921
        if (!empty($tbsCertList['version'])) { // At least v2.
2922
            if (!empty($crlNumber)) {
2923
                $this->setExtension('id-ce-cRLNumber', $crlNumber);
2924
            }
2925
 
2926
            if (isset($issuer->currentKeyIdentifier)) {
2927
                $this->setExtension('id-ce-authorityKeyIdentifier', [
2928
                        //'authorityCertIssuer' => array(
2929
                        //    ]
2930
                        //        'directoryName' => $issuer->dn
2931
                        //    ]
2932
                        //),
2933
                        'keyIdentifier' => $issuer->currentKeyIdentifier
2934
                    ]);
2935
                //$extensions = &$tbsCertList['crlExtensions'];
2936
                //if (isset($issuer->serialNumber)) {
2937
                //    $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber;
2938
                //}
2939
                //unset($extensions);
2940
            }
2941
 
2942
            $issuerAltName = $this->getExtension('id-ce-subjectAltName', $issuer->currentCert);
2943
 
2944
            if ($issuerAltName !== false) {
2945
                $this->setExtension('id-ce-issuerAltName', $issuerAltName);
2946
            }
2947
        }
2948
 
2949
        if (empty($tbsCertList['revokedCertificates'])) {
2950
            unset($tbsCertList['revokedCertificates']);
2951
        }
2952
 
2953
        unset($tbsCertList);
2954
 
2955
        // resync $this->signatureSubject
2956
        // save $tbsCertList in case there are any \phpseclib3\File\ASN1\Element objects in it
2957
        $tbsCertList = $this->currentCert['tbsCertList'];
2958
        $this->loadCRL($this->saveCRL($this->currentCert));
2959
 
2960
        $result = $this->currentCert;
2961
        $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject);
2962
        $result['tbsCertList'] = $tbsCertList;
2963
 
2964
        $this->currentCert = $currentCert;
2965
        $this->signatureSubject = $signatureSubject;
2966
 
2967
        return $result;
2968
    }
2969
 
2970
    /**
2971
     * Identify signature algorithm from key settings
2972
     *
2973
     * @param PrivateKey $key
2974
     * @throws \phpseclib3\Exception\UnsupportedAlgorithmException if the algorithm is unsupported
1042 daniel-mar 2975
     * @return array
827 daniel-mar 2976
     */
2977
    private static function identifySignatureAlgorithm(PrivateKey $key)
2978
    {
2979
        if ($key instanceof RSA) {
2980
            if ($key->getPadding() & RSA::SIGNATURE_PSS) {
1042 daniel-mar 2981
                $r = PSS::load($key->withPassword()->toString('PSS'));
2982
                return [
2983
                    'algorithm' => 'id-RSASSA-PSS',
2984
                    'parameters' => PSS::savePSSParams($r)
2985
                ];
827 daniel-mar 2986
            }
2987
            switch ($key->getHash()) {
2988
                case 'md2':
2989
                case 'md5':
2990
                case 'sha1':
2991
                case 'sha224':
2992
                case 'sha256':
2993
                case 'sha384':
2994
                case 'sha512':
1042 daniel-mar 2995
                    return ['algorithm' => $key->getHash() . 'WithRSAEncryption'];
827 daniel-mar 2996
            }
2997
            throw new UnsupportedAlgorithmException('The only supported hash algorithms for RSA are: md2, md5, sha1, sha224, sha256, sha384, sha512');
2998
        }
2999
 
3000
        if ($key instanceof DSA) {
3001
            switch ($key->getHash()) {
3002
                case 'sha1':
3003
                case 'sha224':
3004
                case 'sha256':
1042 daniel-mar 3005
                    return ['algorithm' => 'id-dsa-with-' . $key->getHash()];
827 daniel-mar 3006
            }
3007
            throw new UnsupportedAlgorithmException('The only supported hash algorithms for DSA are: sha1, sha224, sha256');
3008
        }
3009
 
3010
        if ($key instanceof EC) {
3011
            switch ($key->getCurve()) {
3012
                case 'Ed25519':
3013
                case 'Ed448':
1042 daniel-mar 3014
                    return ['algorithm' => 'id-' . $key->getCurve()];
827 daniel-mar 3015
            }
3016
            switch ($key->getHash()) {
3017
                case 'sha1':
3018
                case 'sha224':
3019
                case 'sha256':
3020
                case 'sha384':
3021
                case 'sha512':
1042 daniel-mar 3022
                    return ['algorithm' => 'ecdsa-with-' . strtoupper($key->getHash())];
827 daniel-mar 3023
            }
3024
            throw new UnsupportedAlgorithmException('The only supported hash algorithms for EC are: sha1, sha224, sha256, sha384, sha512');
3025
        }
3026
 
3027
        throw new UnsupportedAlgorithmException('The only supported public key classes are: RSA, DSA, EC');
3028
    }
3029
 
3030
    /**
3031
     * Set certificate start date
3032
     *
3033
     * @param \DateTimeInterface|string $date
3034
     */
3035
    public function setStartDate($date)
3036
    {
3037
        if (!is_object($date) || !($date instanceof \DateTimeInterface)) {
3038
            $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get()));
3039
        }
3040
 
3041
        $this->startDate = $date->format('D, d M Y H:i:s O');
3042
    }
3043
 
3044
    /**
3045
     * Set certificate end date
3046
     *
3047
     * @param \DateTimeInterface|string $date
3048
     */
3049
    public function setEndDate($date)
3050
    {
3051
        /*
3052
          To indicate that a certificate has no well-defined expiration date,
3053
          the notAfter SHOULD be assigned the GeneralizedTime value of
3054
          99991231235959Z.
3055
 
3056
          -- http://tools.ietf.org/html/rfc5280#section-4.1.2.5
3057
        */
3058
        if (is_string($date) && strtolower($date) === 'lifetime') {
3059
            $temp = '99991231235959Z';
3060
            $temp = chr(ASN1::TYPE_GENERALIZED_TIME) . ASN1::encodeLength(strlen($temp)) . $temp;
3061
            $this->endDate = new Element($temp);
3062
        } else {
3063
            if (!is_object($date) || !($date instanceof \DateTimeInterface)) {
3064
                $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get()));
3065
            }
3066
 
3067
            $this->endDate = $date->format('D, d M Y H:i:s O');
3068
        }
3069
    }
3070
 
3071
    /**
3072
     * Set Serial Number
3073
     *
3074
     * @param string $serial
3075
     * @param int $base optional
3076
     */
3077
    public function setSerialNumber($serial, $base = -256)
3078
    {
3079
        $this->serialNumber = new BigInteger($serial, $base);
3080
    }
3081
 
3082
    /**
3083
     * Turns the certificate into a certificate authority
3084
     *
3085
     */
3086
    public function makeCA()
3087
    {
3088
        $this->caFlag = true;
3089
    }
3090
 
3091
    /**
3092
     * Check for validity of subarray
3093
     *
3094
     * This is intended for use in conjunction with _subArrayUnchecked(),
3095
     * implementing the checks included in _subArray() but without copying
3096
     * a potentially large array by passing its reference by-value to is_array().
3097
     *
3098
     * @param array $root
3099
     * @param string $path
3100
     * @return boolean
3101
     */
1042 daniel-mar 3102
    private function isSubArrayValid(array $root, $path)
827 daniel-mar 3103
    {
3104
        if (!is_array($root)) {
3105
            return false;
3106
        }
3107
 
3108
        foreach (explode('/', $path) as $i) {
3109
            if (!is_array($root)) {
3110
                return false;
3111
            }
3112
 
3113
            if (!isset($root[$i])) {
3114
                return true;
3115
            }
3116
 
3117
            $root = $root[$i];
3118
        }
3119
 
3120
        return true;
3121
    }
3122
 
3123
    /**
3124
     * Get a reference to a subarray
3125
     *
3126
     * This variant of _subArray() does no is_array() checking,
3127
     * so $root should be checked with _isSubArrayValid() first.
3128
     *
3129
     * This is here for performance reasons:
3130
     * Passing a reference (i.e. $root) by-value (i.e. to is_array())
3131
     * creates a copy. If $root is an especially large array, this is expensive.
3132
     *
3133
     * @param array $root
3134
     * @param string $path  absolute path with / as component separator
3135
     * @param bool $create optional
3136
     * @return array|false
3137
     */
1042 daniel-mar 3138
    private function &subArrayUnchecked(array &$root, $path, $create = false)
827 daniel-mar 3139
    {
3140
        $false = false;
3141
 
3142
        foreach (explode('/', $path) as $i) {
3143
            if (!isset($root[$i])) {
3144
                if (!$create) {
3145
                    return $false;
3146
                }
3147
 
3148
                $root[$i] = [];
3149
            }
3150
 
3151
            $root = &$root[$i];
3152
        }
3153
 
3154
        return $root;
3155
    }
3156
 
3157
    /**
3158
     * Get a reference to a subarray
3159
     *
3160
     * @param array $root
3161
     * @param string $path  absolute path with / as component separator
3162
     * @param bool $create optional
3163
     * @return array|false
3164
     */
1042 daniel-mar 3165
    private function &subArray(array &$root = null, $path, $create = false)
827 daniel-mar 3166
    {
3167
        $false = false;
3168
 
3169
        if (!is_array($root)) {
3170
            return $false;
3171
        }
3172
 
3173
        foreach (explode('/', $path) as $i) {
3174
            if (!is_array($root)) {
3175
                return $false;
3176
            }
3177
 
3178
            if (!isset($root[$i])) {
3179
                if (!$create) {
3180
                    return $false;
3181
                }
3182
 
3183
                $root[$i] = [];
3184
            }
3185
 
3186
            $root = &$root[$i];
3187
        }
3188
 
3189
        return $root;
3190
    }
3191
 
3192
    /**
3193
     * Get a reference to an extension subarray
3194
     *
3195
     * @param array $root
3196
     * @param string $path optional absolute path with / as component separator
3197
     * @param bool $create optional
3198
     * @return array|false
3199
     */
1042 daniel-mar 3200
    private function &extensions(array &$root = null, $path = null, $create = false)
827 daniel-mar 3201
    {
3202
        if (!isset($root)) {
3203
            $root = $this->currentCert;
3204
        }
3205
 
3206
        switch (true) {
3207
            case !empty($path):
3208
            case !is_array($root):
3209
                break;
3210
            case isset($root['tbsCertificate']):
3211
                $path = 'tbsCertificate/extensions';
3212
                break;
3213
            case isset($root['tbsCertList']):
3214
                $path = 'tbsCertList/crlExtensions';
3215
                break;
3216
            case isset($root['certificationRequestInfo']):
3217
                $pth = 'certificationRequestInfo/attributes';
3218
                $attributes = &$this->subArray($root, $pth, $create);
3219
 
3220
                if (is_array($attributes)) {
3221
                    foreach ($attributes as $key => $value) {
3222
                        if ($value['type'] == 'pkcs-9-at-extensionRequest') {
3223
                            $path = "$pth/$key/value/0";
3224
                            break 2;
3225
                        }
3226
                    }
3227
                    if ($create) {
3228
                        $key = count($attributes);
3229
                        $attributes[] = ['type' => 'pkcs-9-at-extensionRequest', 'value' => []];
3230
                        $path = "$pth/$key/value/0";
3231
                    }
3232
                }
3233
                break;
3234
        }
3235
 
3236
        $extensions = &$this->subArray($root, $path, $create);
3237
 
3238
        if (!is_array($extensions)) {
3239
            $false = false;
3240
            return $false;
3241
        }
3242
 
3243
        return $extensions;
3244
    }
3245
 
3246
    /**
3247
     * Remove an Extension
3248
     *
3249
     * @param string $id
3250
     * @param string $path optional
3251
     * @return bool
3252
     */
3253
    private function removeExtensionHelper($id, $path = null)
3254
    {
3255
        $extensions = &$this->extensions($this->currentCert, $path);
3256
 
3257
        if (!is_array($extensions)) {
3258
            return false;
3259
        }
3260
 
3261
        $result = false;
3262
        foreach ($extensions as $key => $value) {
3263
            if ($value['extnId'] == $id) {
3264
                unset($extensions[$key]);
3265
                $result = true;
3266
            }
3267
        }
3268
 
3269
        $extensions = array_values($extensions);
3270
        // fix for https://bugs.php.net/75433 affecting PHP 7.2
3271
        if (!isset($extensions[0])) {
3272
            $extensions = array_splice($extensions, 0, 0);
3273
        }
3274
        return $result;
3275
    }
3276
 
3277
    /**
3278
     * Get an Extension
3279
     *
3280
     * Returns the extension if it exists and false if not
3281
     *
3282
     * @param string $id
3283
     * @param array $cert optional
3284
     * @param string $path optional
3285
     * @return mixed
3286
     */
1042 daniel-mar 3287
    private function getExtensionHelper($id, array $cert = null, $path = null)
827 daniel-mar 3288
    {
3289
        $extensions = $this->extensions($cert, $path);
3290
 
3291
        if (!is_array($extensions)) {
3292
            return false;
3293
        }
3294
 
3295
        foreach ($extensions as $key => $value) {
3296
            if ($value['extnId'] == $id) {
3297
                return $value['extnValue'];
3298
            }
3299
        }
3300
 
3301
        return false;
3302
    }
3303
 
3304
    /**
3305
     * Returns a list of all extensions in use
3306
     *
3307
     * @param array $cert optional
3308
     * @param string $path optional
3309
     * @return array
3310
     */
1042 daniel-mar 3311
    private function getExtensionsHelper(array $cert = null, $path = null)
827 daniel-mar 3312
    {
3313
        $exts = $this->extensions($cert, $path);
3314
        $extensions = [];
3315
 
3316
        if (is_array($exts)) {
3317
            foreach ($exts as $extension) {
3318
                $extensions[] = $extension['extnId'];
3319
            }
3320
        }
3321
 
3322
        return $extensions;
3323
    }
3324
 
3325
    /**
3326
     * Set an Extension
3327
     *
3328
     * @param string $id
3329
     * @param mixed $value
3330
     * @param bool $critical optional
3331
     * @param bool $replace optional
3332
     * @param string $path optional
3333
     * @return bool
3334
     */
3335
    private function setExtensionHelper($id, $value, $critical = false, $replace = true, $path = null)
3336
    {
3337
        $extensions = &$this->extensions($this->currentCert, $path, true);
3338
 
3339
        if (!is_array($extensions)) {
3340
            return false;
3341
        }
3342
 
3343
        $newext = ['extnId'  => $id, 'critical' => $critical, 'extnValue' => $value];
3344
 
3345
        foreach ($extensions as $key => $value) {
3346
            if ($value['extnId'] == $id) {
3347
                if (!$replace) {
3348
                    return false;
3349
                }
3350
 
3351
                $extensions[$key] = $newext;
3352
                return true;
3353
            }
3354
        }
3355
 
3356
        $extensions[] = $newext;
3357
        return true;
3358
    }
3359
 
3360
    /**
3361
     * Remove a certificate, CSR or CRL Extension
3362
     *
3363
     * @param string $id
3364
     * @return bool
3365
     */
3366
    public function removeExtension($id)
3367
    {
3368
        return $this->removeExtensionHelper($id);
3369
    }
3370
 
3371
    /**
3372
     * Get a certificate, CSR or CRL Extension
3373
     *
3374
     * Returns the extension if it exists and false if not
3375
     *
3376
     * @param string $id
3377
     * @param array $cert optional
3378
     * @param string $path
3379
     * @return mixed
3380
     */
1042 daniel-mar 3381
    public function getExtension($id, array $cert = null, $path = null)
827 daniel-mar 3382
    {
3383
        return $this->getExtensionHelper($id, $cert, $path);
3384
    }
3385
 
3386
    /**
3387
     * Returns a list of all extensions in use in certificate, CSR or CRL
3388
     *
3389
     * @param array $cert optional
3390
     * @param string $path optional
3391
     * @return array
3392
     */
1042 daniel-mar 3393
    public function getExtensions(array $cert = null, $path = null)
827 daniel-mar 3394
    {
3395
        return $this->getExtensionsHelper($cert, $path);
3396
    }
3397
 
3398
    /**
3399
     * Set a certificate, CSR or CRL Extension
3400
     *
3401
     * @param string $id
3402
     * @param mixed $value
3403
     * @param bool $critical optional
3404
     * @param bool $replace optional
3405
     * @return bool
3406
     */
3407
    public function setExtension($id, $value, $critical = false, $replace = true)
3408
    {
3409
        return $this->setExtensionHelper($id, $value, $critical, $replace);
3410
    }
3411
 
3412
    /**
3413
     * Remove a CSR attribute.
3414
     *
3415
     * @param string $id
3416
     * @param int $disposition optional
3417
     * @return bool
3418
     */
3419
    public function removeAttribute($id, $disposition = self::ATTR_ALL)
3420
    {
3421
        $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes');
3422
 
3423
        if (!is_array($attributes)) {
3424
            return false;
3425
        }
3426
 
3427
        $result = false;
3428
        foreach ($attributes as $key => $attribute) {
3429
            if ($attribute['type'] == $id) {
3430
                $n = count($attribute['value']);
3431
                switch (true) {
3432
                    case $disposition == self::ATTR_APPEND:
3433
                    case $disposition == self::ATTR_REPLACE:
3434
                        return false;
3435
                    case $disposition >= $n:
3436
                        $disposition -= $n;
3437
                        break;
3438
                    case $disposition == self::ATTR_ALL:
3439
                    case $n == 1:
3440
                        unset($attributes[$key]);
3441
                        $result = true;
3442
                        break;
3443
                    default:
3444
                        unset($attributes[$key]['value'][$disposition]);
3445
                        $attributes[$key]['value'] = array_values($attributes[$key]['value']);
3446
                        $result = true;
3447
                        break;
3448
                }
3449
                if ($result && $disposition != self::ATTR_ALL) {
3450
                    break;
3451
                }
3452
            }
3453
        }
3454
 
3455
        $attributes = array_values($attributes);
3456
        return $result;
3457
    }
3458
 
3459
    /**
3460
     * Get a CSR attribute
3461
     *
3462
     * Returns the attribute if it exists and false if not
3463
     *
3464
     * @param string $id
3465
     * @param int $disposition optional
3466
     * @param array $csr optional
3467
     * @return mixed
3468
     */
1042 daniel-mar 3469
    public function getAttribute($id, $disposition = self::ATTR_ALL, array $csr = null)
827 daniel-mar 3470
    {
3471
        if (empty($csr)) {
3472
            $csr = $this->currentCert;
3473
        }
3474
 
3475
        $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes');
3476
 
3477
        if (!is_array($attributes)) {
3478
            return false;
3479
        }
3480
 
3481
        foreach ($attributes as $key => $attribute) {
3482
            if ($attribute['type'] == $id) {
3483
                $n = count($attribute['value']);
3484
                switch (true) {
3485
                    case $disposition == self::ATTR_APPEND:
3486
                    case $disposition == self::ATTR_REPLACE:
3487
                        return false;
3488
                    case $disposition == self::ATTR_ALL:
3489
                        return $attribute['value'];
3490
                    case $disposition >= $n:
3491
                        $disposition -= $n;
3492
                        break;
3493
                    default:
3494
                        return $attribute['value'][$disposition];
3495
                }
3496
            }
3497
        }
3498
 
3499
        return false;
3500
    }
3501
 
3502
    /**
3503
     * Returns a list of all CSR attributes in use
3504
     *
3505
     * @param array $csr optional
3506
     * @return array
3507
     */
1042 daniel-mar 3508
    public function getAttributes(array $csr = null)
827 daniel-mar 3509
    {
3510
        if (empty($csr)) {
3511
            $csr = $this->currentCert;
3512
        }
3513
 
3514
        $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes');
3515
        $attrs = [];
3516
 
3517
        if (is_array($attributes)) {
3518
            foreach ($attributes as $attribute) {
3519
                $attrs[] = $attribute['type'];
3520
            }
3521
        }
3522
 
3523
        return $attrs;
3524
    }
3525
 
3526
    /**
3527
     * Set a CSR attribute
3528
     *
3529
     * @param string $id
3530
     * @param mixed $value
3531
     * @param int $disposition optional
3532
     * @return bool
3533
     */
3534
    public function setAttribute($id, $value, $disposition = self::ATTR_ALL)
3535
    {
3536
        $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes', true);
3537
 
3538
        if (!is_array($attributes)) {
3539
            return false;
3540
        }
3541
 
3542
        switch ($disposition) {
3543
            case self::ATTR_REPLACE:
3544
                $disposition = self::ATTR_APPEND;
3545
                // fall-through
3546
            case self::ATTR_ALL:
3547
                $this->removeAttribute($id);
3548
                break;
3549
        }
3550
 
3551
        foreach ($attributes as $key => $attribute) {
3552
            if ($attribute['type'] == $id) {
3553
                $n = count($attribute['value']);
3554
                switch (true) {
3555
                    case $disposition == self::ATTR_APPEND:
3556
                        $last = $key;
3557
                        break;
3558
                    case $disposition >= $n:
3559
                        $disposition -= $n;
3560
                        break;
3561
                    default:
3562
                        $attributes[$key]['value'][$disposition] = $value;
3563
                        return true;
3564
                }
3565
            }
3566
        }
3567
 
3568
        switch (true) {
3569
            case $disposition >= 0:
3570
                return false;
3571
            case isset($last):
3572
                $attributes[$last]['value'][] = $value;
3573
                break;
3574
            default:
3575
                $attributes[] = ['type' => $id, 'value' => $disposition == self::ATTR_ALL ? $value : [$value]];
3576
                break;
3577
        }
3578
 
3579
        return true;
3580
    }
3581
 
3582
    /**
3583
     * Sets the subject key identifier
3584
     *
3585
     * This is used by the id-ce-authorityKeyIdentifier and the id-ce-subjectKeyIdentifier extensions.
3586
     *
3587
     * @param string $value
3588
     */
3589
    public function setKeyIdentifier($value)
3590
    {
3591
        if (empty($value)) {
3592
            unset($this->currentKeyIdentifier);
3593
        } else {
3594
            $this->currentKeyIdentifier = $value;
3595
        }
3596
    }
3597
 
3598
    /**
3599
     * Compute a public key identifier.
3600
     *
3601
     * Although key identifiers may be set to any unique value, this function
3602
     * computes key identifiers from public key according to the two
3603
     * recommended methods (4.2.1.2 RFC 3280).
3604
     * Highly polymorphic: try to accept all possible forms of key:
3605
     * - Key object
3606
     * - \phpseclib3\File\X509 object with public or private key defined
3607
     * - Certificate or CSR array
3608
     * - \phpseclib3\File\ASN1\Element object
3609
     * - PEM or DER string
3610
     *
3611
     * @param mixed $key optional
3612
     * @param int $method optional
3613
     * @return string binary key identifier
3614
     */
3615
    public function computeKeyIdentifier($key = null, $method = 1)
3616
    {
3617
        if (is_null($key)) {
3618
            $key = $this;
3619
        }
3620
 
3621
        switch (true) {
3622
            case is_string($key):
3623
                break;
3624
            case is_array($key) && isset($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']):
3625
                return $this->computeKeyIdentifier($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], $method);
3626
            case is_array($key) && isset($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']):
3627
                return $this->computeKeyIdentifier($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], $method);
3628
            case !is_object($key):
3629
                return false;
3630
            case $key instanceof Element:
3631
                // Assume the element is a bitstring-packed key.
3632
                $decoded = ASN1::decodeBER($key->element);
1042 daniel-mar 3633
                if (!$decoded) {
827 daniel-mar 3634
                    return false;
3635
                }
3636
                $raw = ASN1::asn1map($decoded[0], ['type' => ASN1::TYPE_BIT_STRING]);
3637
                if (empty($raw)) {
3638
                    return false;
3639
                }
3640
                // If the key is private, compute identifier from its corresponding public key.
3641
                $key = PublicKeyLoader::load($raw);
3642
                if ($key instanceof PrivateKey) {  // If private.
3643
                    return $this->computeKeyIdentifier($key, $method);
3644
                }
3645
                $key = $raw; // Is a public key.
3646
                break;
3647
            case $key instanceof X509:
3648
                if (isset($key->publicKey)) {
3649
                    return $this->computeKeyIdentifier($key->publicKey, $method);
3650
                }
3651
                if (isset($key->privateKey)) {
3652
                    return $this->computeKeyIdentifier($key->privateKey, $method);
3653
                }
3654
                if (isset($key->currentCert['tbsCertificate']) || isset($key->currentCert['certificationRequestInfo'])) {
3655
                    return $this->computeKeyIdentifier($key->currentCert, $method);
3656
                }
3657
                return false;
3658
            default: // Should be a key object (i.e.: \phpseclib3\Crypt\RSA).
3659
                $key = $key->getPublicKey();
3660
                break;
3661
        }
3662
 
3663
        // If in PEM format, convert to binary.
3664
        $key = ASN1::extractBER($key);
3665
 
3666
        // Now we have the key string: compute its sha-1 sum.
3667
        $hash = new Hash('sha1');
3668
        $hash = $hash->hash($key);
3669
 
3670
        if ($method == 2) {
3671
            $hash = substr($hash, -8);
3672
            $hash[0] = chr((ord($hash[0]) & 0x0F) | 0x40);
3673
        }
3674
 
3675
        return $hash;
3676
    }
3677
 
3678
    /**
3679
     * Format a public key as appropriate
3680
     *
3681
     * @return array|false
3682
     */
3683
    private function formatSubjectPublicKey()
3684
    {
3685
        $format = $this->publicKey instanceof RSA && ($this->publicKey->getPadding() & RSA::SIGNATURE_PSS) ?
3686
            'PSS' :
3687
            'PKCS8';
3688
 
3689
        $publicKey = base64_decode(preg_replace('#-.+-|[\r\n]#', '', $this->publicKey->toString($format)));
3690
 
3691
        $decoded = ASN1::decodeBER($publicKey);
1042 daniel-mar 3692
        if (!$decoded) {
3693
            return false;
3694
        }
827 daniel-mar 3695
        $mapped = ASN1::asn1map($decoded[0], Maps\SubjectPublicKeyInfo::MAP);
3696
        if (!is_array($mapped)) {
3697
            return false;
3698
        }
3699
 
3700
        $mapped['subjectPublicKey'] = $this->publicKey->toString($format);
3701
 
3702
        return $mapped;
3703
    }
3704
 
3705
    /**
3706
     * Set the domain name's which the cert is to be valid for
3707
     *
3708
     * @param mixed ...$domains
3709
     * @return void
3710
     */
3711
    public function setDomain(...$domains)
3712
    {
3713
        $this->domains = $domains;
3714
        $this->removeDNProp('id-at-commonName');
3715
        $this->setDNProp('id-at-commonName', $this->domains[0]);
3716
    }
3717
 
3718
    /**
3719
     * Set the IP Addresses's which the cert is to be valid for
3720
     *
3721
     * @param mixed[] ...$ipAddresses
3722
     */
3723
    public function setIPAddress(...$ipAddresses)
3724
    {
3725
        $this->ipAddresses = $ipAddresses;
3726
        /*
3727
        if (!isset($this->domains)) {
3728
            $this->removeDNProp('id-at-commonName');
3729
            $this->setDNProp('id-at-commonName', $this->ipAddresses[0]);
3730
        }
3731
        */
3732
    }
3733
 
3734
    /**
3735
     * Helper function to build domain array
3736
     *
3737
     * @param string $domain
3738
     * @return array
3739
     */
1042 daniel-mar 3740
    private static function dnsName($domain)
827 daniel-mar 3741
    {
3742
        return ['dNSName' => $domain];
3743
    }
3744
 
3745
    /**
3746
     * Helper function to build IP Address array
3747
     *
3748
     * (IPv6 is not currently supported)
3749
     *
3750
     * @param string $address
3751
     * @return array
3752
     */
3753
    private function iPAddress($address)
3754
    {
3755
        return ['iPAddress' => $address];
3756
    }
3757
 
3758
    /**
3759
     * Get the index of a revoked certificate.
3760
     *
3761
     * @param array $rclist
3762
     * @param string $serial
3763
     * @param bool $create optional
3764
     * @return int|false
3765
     */
1042 daniel-mar 3766
    private function revokedCertificate(array &$rclist, $serial, $create = false)
827 daniel-mar 3767
    {
3768
        $serial = new BigInteger($serial);
3769
 
3770
        foreach ($rclist as $i => $rc) {
3771
            if (!($serial->compare($rc['userCertificate']))) {
3772
                return $i;
3773
            }
3774
        }
3775
 
3776
        if (!$create) {
3777
            return false;
3778
        }
3779
 
3780
        $i = count($rclist);
3781
        $revocationDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get()));
3782
        $rclist[] = ['userCertificate' => $serial,
3783
                          'revocationDate'  => $this->timeField($revocationDate->format('D, d M Y H:i:s O'))];
3784
        return $i;
3785
    }
3786
 
3787
    /**
3788
     * Revoke a certificate.
3789
     *
3790
     * @param string $serial
3791
     * @param string $date optional
3792
     * @return bool
3793
     */
3794
    public function revoke($serial, $date = null)
3795
    {
3796
        if (isset($this->currentCert['tbsCertList'])) {
3797
            if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) {
3798
                if ($this->revokedCertificate($rclist, $serial) === false) { // If not yet revoked
3799
                    if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) {
3800
                        if (!empty($date)) {
3801
                            $rclist[$i]['revocationDate'] = $this->timeField($date);
3802
                        }
3803
 
3804
                        return true;
3805
                    }
3806
                }
3807
            }
3808
        }
3809
 
3810
        return false;
3811
    }
3812
 
3813
    /**
3814
     * Unrevoke a certificate.
3815
     *
3816
     * @param string $serial
3817
     * @return bool
3818
     */
3819
    public function unrevoke($serial)
3820
    {
3821
        if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) {
3822
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3823
                unset($rclist[$i]);
3824
                $rclist = array_values($rclist);
3825
                return true;
3826
            }
3827
        }
3828
 
3829
        return false;
3830
    }
3831
 
3832
    /**
3833
     * Get a revoked certificate.
3834
     *
3835
     * @param string $serial
3836
     * @return mixed
3837
     */
3838
    public function getRevoked($serial)
3839
    {
3840
        if (is_array($rclist = $this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) {
3841
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3842
                return $rclist[$i];
3843
            }
3844
        }
3845
 
3846
        return false;
3847
    }
3848
 
3849
    /**
3850
     * List revoked certificates
3851
     *
3852
     * @param array $crl optional
3853
     * @return array|bool
3854
     */
1042 daniel-mar 3855
    public function listRevoked(array $crl = null)
827 daniel-mar 3856
    {
3857
        if (!isset($crl)) {
3858
            $crl = $this->currentCert;
3859
        }
3860
 
3861
        if (!isset($crl['tbsCertList'])) {
3862
            return false;
3863
        }
3864
 
3865
        $result = [];
3866
 
3867
        if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) {
3868
            foreach ($rclist as $rc) {
3869
                $result[] = $rc['userCertificate']->toString();
3870
            }
3871
        }
3872
 
3873
        return $result;
3874
    }
3875
 
3876
    /**
3877
     * Remove a Revoked Certificate Extension
3878
     *
3879
     * @param string $serial
3880
     * @param string $id
3881
     * @return bool
3882
     */
3883
    public function removeRevokedCertificateExtension($serial, $id)
3884
    {
3885
        if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) {
3886
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3887
                return $this->removeExtensionHelper($id, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3888
            }
3889
        }
3890
 
3891
        return false;
3892
    }
3893
 
3894
    /**
3895
     * Get a Revoked Certificate Extension
3896
     *
3897
     * Returns the extension if it exists and false if not
3898
     *
3899
     * @param string $serial
3900
     * @param string $id
3901
     * @param array $crl optional
3902
     * @return mixed
3903
     */
1042 daniel-mar 3904
    public function getRevokedCertificateExtension($serial, $id, array $crl = null)
827 daniel-mar 3905
    {
3906
        if (!isset($crl)) {
3907
            $crl = $this->currentCert;
3908
        }
3909
 
3910
        if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) {
3911
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3912
                return $this->getExtension($id, $crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3913
            }
3914
        }
3915
 
3916
        return false;
3917
    }
3918
 
3919
    /**
3920
     * Returns a list of all extensions in use for a given revoked certificate
3921
     *
3922
     * @param string $serial
3923
     * @param array $crl optional
3924
     * @return array|bool
3925
     */
1042 daniel-mar 3926
    public function getRevokedCertificateExtensions($serial, array $crl = null)
827 daniel-mar 3927
    {
3928
        if (!isset($crl)) {
3929
            $crl = $this->currentCert;
3930
        }
3931
 
3932
        if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) {
3933
            if (($i = $this->revokedCertificate($rclist, $serial)) !== false) {
3934
                return $this->getExtensions($crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3935
            }
3936
        }
3937
 
3938
        return false;
3939
    }
3940
 
3941
    /**
3942
     * Set a Revoked Certificate Extension
3943
     *
3944
     * @param string $serial
3945
     * @param string $id
3946
     * @param mixed $value
3947
     * @param bool $critical optional
3948
     * @param bool $replace optional
3949
     * @return bool
3950
     */
3951
    public function setRevokedCertificateExtension($serial, $id, $value, $critical = false, $replace = true)
3952
    {
3953
        if (isset($this->currentCert['tbsCertList'])) {
3954
            if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) {
3955
                if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) {
3956
                    return $this->setExtensionHelper($id, $value, $critical, $replace, "tbsCertList/revokedCertificates/$i/crlEntryExtensions");
3957
                }
3958
            }
3959
        }
3960
 
3961
        return false;
3962
    }
3963
 
3964
    /**
3965
     * Register the mapping for a custom/unsupported extension.
3966
     *
3967
     * @param string $id
3968
     * @param array $mapping
3969
     */
3970
    public static function registerExtension($id, array $mapping)
3971
    {
3972
        if (isset(self::$extensions[$id]) && self::$extensions[$id] !== $mapping) {
3973
            throw new \RuntimeException(
3974
                'Extension ' . $id . ' has already been defined with a different mapping.'
3975
            );
3976
        }
3977
 
3978
        self::$extensions[$id] = $mapping;
3979
    }
3980
 
3981
    /**
3982
     * Register the mapping for a custom/unsupported extension.
3983
     *
3984
     * @param string $id
3985
     *
3986
     * @return array|null
3987
     */
3988
    public static function getRegisteredExtension($id)
3989
    {
3990
        return isset(self::$extensions[$id]) ? self::$extensions[$id] : null;
3991
    }
3992
 
3993
    /**
3994
     * Register the mapping for a custom/unsupported extension.
3995
     *
3996
     * @param string $id
3997
     * @param mixed $value
3998
     * @param bool $critical
3999
     * @param bool $replace
4000
     */
4001
    public function setExtensionValue($id, $value, $critical = false, $replace = false)
4002
    {
4003
        $this->extensionValues[$id] = compact('critical', 'replace', 'value');
4004
    }
4005
}