Completed
Pull Request — master (#9)
by John
02:06
created
src/LEOrder.php 1 patch
Indentation   +772 added lines, -772 removed lines patch added patch discarded remove patch
@@ -16,559 +16,559 @@  discard block
 block discarded – undo
16 16
  */
17 17
 class LEOrder
18 18
 {
19
-    const CHALLENGE_TYPE_HTTP = 'http-01';
20
-    const CHALLENGE_TYPE_DNS = 'dns-01';
21
-
22
-    /** @var string order status (pending, processing, valid) */
23
-    private $status;
24
-
25
-    /** @var string expiration date for order */
26
-    private $expires;
27
-
28
-    /** @var array containing all the domain identifiers for the order */
29
-    private $identifiers;
30
-
31
-    /** @var string[] URLs to all the authorization objects for this order */
32
-    private $authorizationURLs;
33
-
34
-    /** @var LEAuthorization[] array of authorization objects for the order */
35
-    private $authorizations;
36
-
37
-    /** @var string URL for order finalization */
38
-    private $finalizeURL;
39
-
40
-    /** @var string URL for obtaining certificate */
41
-    private $certificateURL;
42
-
43
-    /** @var string base domain name for certificate */
44
-    private $basename;
45
-
46
-    /** @var string URL referencing order */
47
-    private $orderURL;
48
-
49
-    /** @var string type of key (rsa or ec) */
50
-    private $keyType;
51
-
52
-    /** @var int size of key (typically 2048 or 4096 for rsa, 256 or 384 for ec */
53
-    private $keySize;
54
-
55
-    /** @var LEConnector ACME API connection provided to constructor */
56
-    private $connector;
57
-
58
-    /** @var LoggerInterface logger provided to constructor */
59
-    private $log;
60
-
61
-    /** @var DNSValidatorInterface dns resolution provider to constructor*/
62
-    private $dns;
63
-
64
-    /** @var Sleep sleep service provided to constructor */
65
-    private $sleep;
66
-
67
-    /** @var CertificateStorageInterface storage interface provided to constructor */
68
-    private $storage;
69
-
70
-    /** @var AccountStorageInterface */
71
-    private $accountStorage;
72
-
73
-    /**
74
-     * Initiates the LetsEncrypt Order class. If the base name is found in the $keysDir directory, the order data is
75
-     * requested. If no order was found locally, if the request is invalid or when there is a change in domain names, a
76
-     * new order is created.
77
-     *
78
-     * @param LEConnector $connector The LetsEncrypt Connector instance to use for HTTP requests.
79
-     * @param CertificateStorageInterface $storage
80
-     * @param AccountStorageInterface $accountStorage
81
-     * @param LoggerInterface $log PSR-3 compatible logger
82
-     * @param DNSValidatorInterface $dns DNS challenge checking service
83
-     * @param Sleep $sleep Sleep service for polling
84
-     */
85
-    public function __construct(
86
-        LEConnector $connector,
87
-        CertificateStorageInterface $storage,
88
-        AccountStorageInterface $accountStorage,
89
-        LoggerInterface $log,
90
-        DNSValidatorInterface $dns,
91
-        Sleep $sleep
92
-    ) {
93
-
94
-        $this->connector = $connector;
95
-        $this->log = $log;
96
-        $this->dns = $dns;
97
-        $this->sleep = $sleep;
98
-        $this->storage = $storage;
99
-        $this->accountStorage = $accountStorage;
100
-    }
101
-
102
-    /**
103
-     * Loads or updates an order. If the base name is found in the $keysDir directory, the order data is
104
-     * requested. If no order was found locally, if the request is invalid or when there is a change in domain names, a
105
-     * new order is created.
106
-     *
107
-     * @param string $basename The base name for the order. Preferable the top domain (example.org).
108
-     *                                         Will be the directory in which the keys are stored. Used for the
109
-     *                                         CommonName in the certificate as well.
110
-     * @param array $domains The array of strings containing the domain names on the certificate.
111
-     * @param string $keyType Type of the key we want to use for certificate. Can be provided in
112
-     *                                         ALGO-SIZE format (ex. rsa-4096 or ec-256) or simply "rsa" and "ec"
113
-     *                                         (using default sizes)
114
-     * @param string $notBefore A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
115
-     *                                         at which the certificate becomes valid.
116
-     * @param string $notAfter A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
117
-     *                                         until which the certificate is valid.
118
-     */
119
-    public function loadOrder($basename, array $domains, $keyType, $notBefore, $notAfter)
120
-    {
121
-        $this->basename = $basename;
122
-
123
-        $this->initialiseKeyTypeAndSize($keyType ?? 'rsa-4096');
124
-
125
-        if ($this->loadExistingOrder($domains)) {
126
-            $this->updateAuthorizations();
127
-        } else {
128
-            $this->createOrder($domains, $notBefore, $notAfter);
129
-        }
130
-    }
131
-
132
-    private function loadExistingOrder($domains)
133
-    {
134
-        $orderUrl = $this->storage->getMetadata($this->basename.'.order.url');
135
-        $publicKey = $this->storage->getPublicKey($this->basename);
136
-        $privateKey = $this->storage->getPrivateKey($this->basename);
137
-
138
-        //anything to load?
139
-        if (empty($orderUrl) || empty($publicKey) || empty($privateKey)) {
140
-            $this->log->info("No order found for {$this->basename}. Creating new order.");
141
-            return false;
142
-        }
143
-
144
-        //valid URL?
145
-        $this->orderURL = $orderUrl;
146
-        if (!filter_var($this->orderURL, FILTER_VALIDATE_URL)) {
147
-            //@codeCoverageIgnoreStart
148
-            $this->log->warning("Order for {$this->basename} has invalid URL. Creating new order.");
149
-            $this->deleteOrderFiles();
150
-            return false;
151
-            //@codeCoverageIgnoreEnd
152
-        }
153
-
154
-        //retrieve the order
155
-        $sign = $this->connector->signRequestKid(
156
-            null,
157
-            $this->connector->accountURL,
158
-            $this->orderURL
159
-        );
160
-
161
-        $post = $this->connector->post($this->orderURL, $sign);
162
-        if ($post['status'] !== 200) {
163
-            //@codeCoverageIgnoreStart
164
-            $this->log->warning("Order for {$this->basename} could not be loaded. Creating new order.");
165
-            $this->deleteOrderFiles();
166
-            return false;
167
-            //@codeCoverageIgnoreEnd
168
-        }
169
-
170
-        //ensure the order is still valid
171
-        if ($post['body']['status'] === 'invalid') {
172
-            $this->log->warning("Order for {$this->basename} is 'invalid', unable to authorize. Creating new order.");
173
-            $this->deleteOrderFiles();
174
-            return false;
175
-        }
176
-
177
-        //ensure retrieved order matches our domains
178
-        $orderdomains = array_map(function ($ident) {
179
-            return $ident['value'];
180
-        }, $post['body']['identifiers']);
181
-        $diff = array_merge(array_diff($orderdomains, $domains), array_diff($domains, $orderdomains));
182
-        if (!empty($diff)) {
183
-            $this->log->warning('Domains do not match order data. Deleting and creating new order.');
184
-            $this->deleteOrderFiles();
185
-            return false;
186
-        }
187
-
188
-        //the order is good
189
-        $this->status = $post['body']['status'];
190
-        $this->expires = $post['body']['expires'];
191
-        $this->identifiers = $post['body']['identifiers'];
192
-        $this->authorizationURLs = $post['body']['authorizations'];
193
-        $this->finalizeURL = $post['body']['finalize'];
194
-        if (array_key_exists('certificate', $post['body'])) {
195
-            $this->certificateURL = $post['body']['certificate'];
196
-        }
197
-
198
-        return true;
199
-    }
200
-
201
-    private function deleteOrderFiles()
202
-    {
203
-        $this->storage->setPrivateKey($this->basename, null);
204
-        $this->storage->setPublicKey($this->basename, null);
205
-        $this->storage->setCertificate($this->basename, null);
206
-        $this->storage->setFullChainCertificate($this->basename, null);
207
-        $this->storage->setMetadata($this->basename.'.order.url', null);
208
-    }
209
-
210
-    private function initialiseKeyTypeAndSize($keyType)
211
-    {
212
-        if ($keyType == 'rsa') {
213
-            $this->keyType = 'rsa';
214
-            $this->keySize = 4096;
215
-        } elseif ($keyType == 'ec') {
216
-            $this->keyType = 'ec';
217
-            $this->keySize = 256;
218
-        } else {
219
-            preg_match_all('/^(rsa|ec)\-([0-9]{3,4})$/', $keyType, $keyTypeParts, PREG_SET_ORDER, 0);
220
-
221
-            if (!empty($keyTypeParts)) {
222
-                $this->keyType = $keyTypeParts[0][1];
223
-                $this->keySize = intval($keyTypeParts[0][2]);
224
-            } else {
225
-                throw new LogicException('Key type \'' . $keyType . '\' not supported.');
226
-            }
227
-        }
228
-    }
229
-
230
-    /**
231
-     * Creates a new LetsEncrypt order and fills this instance with its data. Subsequently creates a new RSA keypair
232
-     * for the certificate.
233
-     *
234
-     * @param array $domains The array of strings containing the domain names on the certificate.
235
-     * @param string $notBefore A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
236
-     *                          at which the certificate becomes valid.
237
-     * @param string $notAfter A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
238
-     *                          until which the certificate is valid.
239
-     */
240
-    private function createOrder($domains, $notBefore, $notAfter)
241
-    {
242
-        if (!preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notBefore) ||
243
-            !preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notAfter)
244
-        ) {
245
-            throw new LogicException("notBefore and notAfter must be blank or iso-8601 datestamp");
246
-        }
247
-
248
-        $dns = [];
249
-        foreach ($domains as $domain) {
250
-            if (preg_match_all('~(\*\.)~', $domain) > 1) {
251
-                throw new LogicException('Cannot create orders with multiple wildcards in one domain.');
252
-            }
253
-            $dns[] = ['type' => 'dns', 'value' => $domain];
254
-        }
255
-        $payload = ["identifiers" => $dns, 'notBefore' => $notBefore, 'notAfter' => $notAfter];
256
-        $sign = $this->connector->signRequestKid(
257
-            $payload,
258
-            $this->connector->accountURL,
259
-            $this->connector->newOrder
260
-        );
261
-        $post = $this->connector->post($this->connector->newOrder, $sign);
262
-        if ($post['status'] !== 201) {
263
-            //@codeCoverageIgnoreStart
264
-            throw new RuntimeException('Creating new order failed.');
265
-            //@codeCoverageIgnoreEnd
266
-        }
267
-
268
-        if (!preg_match('~Location: (\S+)~i', $post['header'], $matches)) {
269
-            //@codeCoverageIgnoreStart
270
-            throw new RuntimeException('New-order returned invalid response.');
271
-            //@codeCoverageIgnoreEnd
272
-        }
273
-
274
-        $this->orderURL = trim($matches[1]);
275
-        $this->storage->setMetadata($this->basename.'.order.url', $this->orderURL);
276
-
277
-        $this->generateKeys();
278
-
279
-        $this->status = $post['body']['status'];
280
-        $this->expires = $post['body']['expires'];
281
-        $this->identifiers = $post['body']['identifiers'];
282
-        $this->authorizationURLs = $post['body']['authorizations'];
283
-        $this->finalizeURL = $post['body']['finalize'];
284
-        if (array_key_exists('certificate', $post['body'])) {
285
-            $this->certificateURL = $post['body']['certificate'];
286
-        }
287
-        $this->updateAuthorizations();
288
-
289
-        $this->log->info('Created order for ' . $this->basename);
290
-    }
291
-
292
-    private function generateKeys()
293
-    {
294
-        if ($this->keyType == "rsa") {
295
-            $key = LEFunctions::RSAgenerateKeys($this->keySize);
296
-        } else {
297
-            $key = LEFunctions::ECgenerateKeys($this->keySize);
298
-        }
299
-
300
-        $this->storage->setPublicKey($this->basename, $key['public']);
301
-        $this->storage->setPrivateKey($this->basename, $key['private']);
302
-    }
303
-
304
-    /**
305
-     * Fetches the latest data concerning this LetsEncrypt Order instance and fills this instance with the new data.
306
-     */
307
-    private function updateOrderData()
308
-    {
309
-        $sign = $this->connector->signRequestKid(
310
-            null,
311
-            $this->connector->accountURL,
312
-            $this->orderURL
313
-        );
314
-
315
-        $post = $this->connector->post($this->orderURL, $sign);
316
-        if (strpos($post['header'], "200 OK") !== false) {
317
-            $this->status = $post['body']['status'];
318
-            $this->expires = $post['body']['expires'];
319
-            $this->identifiers = $post['body']['identifiers'];
320
-            $this->authorizationURLs = $post['body']['authorizations'];
321
-            $this->finalizeURL = $post['body']['finalize'];
322
-            if (array_key_exists('certificate', $post['body'])) {
323
-                $this->certificateURL = $post['body']['certificate'];
324
-            }
325
-            $this->updateAuthorizations();
326
-        } else {
327
-            //@codeCoverageIgnoreStart
328
-            $this->log->error("Failed to fetch order for {$this->basename}");
329
-            //@codeCoverageIgnoreEnd
330
-        }
331
-    }
332
-
333
-    /**
334
-     * Fetches the latest data concerning all authorizations connected to this LetsEncrypt Order instance and
335
-     * creates and stores a new LetsEncrypt Authorization instance for each one.
336
-     */
337
-    private function updateAuthorizations()
338
-    {
339
-        $this->authorizations = [];
340
-        foreach ($this->authorizationURLs as $authURL) {
341
-            if (filter_var($authURL, FILTER_VALIDATE_URL)) {
342
-                $auth = new LEAuthorization($this->connector, $this->log, $authURL);
343
-                if ($auth != false) {
344
-                    $this->authorizations[] = $auth;
345
-                }
346
-            }
347
-        }
348
-    }
349
-
350
-    /**
351
-     * Walks all LetsEncrypt Authorization instances and returns whether they are all valid (verified).
352
-     *
353
-     * @return boolean  Returns true if all authorizations are valid (verified), returns false if not.
354
-     */
355
-    public function allAuthorizationsValid()
356
-    {
357
-        if (count($this->authorizations) > 0) {
358
-            foreach ($this->authorizations as $auth) {
359
-                if ($auth->status != 'valid') {
360
-                    return false;
361
-                }
362
-            }
363
-            return true;
364
-        }
365
-        return false;
366
-    }
367
-
368
-    private function loadAccountKey()
369
-    {
370
-        $keydata = $this->accountStorage->getAccountPrivateKey();
371
-        $privateKey = openssl_pkey_get_private($keydata);
372
-        if ($privateKey === false) {
373
-            //@codeCoverageIgnoreStart
374
-            throw new RuntimeException("Failed load account key");
375
-            //@codeCoverageIgnoreEnd
376
-        }
377
-        return $privateKey;
378
-    }
379
-
380
-
381
-    private function loadCertificateKey()
382
-    {
383
-        $keydata = $this->storage->getPrivateKey($this->basename);
384
-        $privateKey = openssl_pkey_get_private($keydata);
385
-        if ($privateKey === false) {
386
-            //@codeCoverageIgnoreStart
387
-            throw new RuntimeException("Failed load certificate key");
388
-            //@codeCoverageIgnoreEnd
389
-        }
390
-        return $privateKey;
391
-    }
392
-
393
-    /**
394
-     * Get all pending LetsEncrypt Authorization instances and return the necessary data for verification.
395
-     * The data in the return object depends on the $type.
396
-     *
397
-     * @param string $type The type of verification to get. Supporting http-01 and dns-01.
398
-     *                     Supporting LEOrder::CHALLENGE_TYPE_HTTP and LEOrder::CHALLENGE_TYPE_DNS. Throws a Runtime
399
-     *                     Exception when requesting an unknown $type. Keep in mind a wildcard domain authorization only
400
-     *                     accepts LEOrder::CHALLENGE_TYPE_DNS.
401
-     *
402
-     * @return array|bool Returns an array with verification data if successful, false if not pending LetsEncrypt
403
-     *                  Authorization instances were found. The return array always
404
-     *                  contains 'type' and 'identifier'. For LEOrder::CHALLENGE_TYPE_HTTP, the array contains
405
-     *                  'filename' and 'content' for necessary the authorization file.
406
-     *                  For LEOrder::CHALLENGE_TYPE_DNS, the array contains 'DNSDigest', which is the content for the
407
-     *                  necessary DNS TXT entry.
408
-     */
409
-
410
-    public function getPendingAuthorizations($type)
411
-    {
412
-        $authorizations = [];
413
-
414
-        $privateKey = $this->loadAccountKey();
415
-        $details = openssl_pkey_get_details($privateKey);
416
-
417
-        $header = [
418
-            "e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]),
419
-            "kty" => "RSA",
420
-            "n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"])
421
-
422
-        ];
423
-        $digest = LEFunctions::base64UrlSafeEncode(hash('sha256', json_encode($header), true));
424
-
425
-        foreach ($this->authorizations as $auth) {
426
-            if ($auth->status == 'pending') {
427
-                $challenge = $auth->getChallenge($type);
428
-                if ($challenge['status'] == 'pending') {
429
-                    $keyAuthorization = $challenge['token'] . '.' . $digest;
430
-                    switch (strtolower($type)) {
431
-                        case LEOrder::CHALLENGE_TYPE_HTTP:
432
-                            $authorizations[] = [
433
-                                'type' => LEOrder::CHALLENGE_TYPE_HTTP,
434
-                                'identifier' => $auth->identifier['value'],
435
-                                'filename' => $challenge['token'],
436
-                                'content' => $keyAuthorization
437
-                            ];
438
-                            break;
439
-                        case LEOrder::CHALLENGE_TYPE_DNS:
440
-                            $DNSDigest = LEFunctions::base64UrlSafeEncode(
441
-                                hash('sha256', $keyAuthorization, true)
442
-                            );
443
-                            $authorizations[] = [
444
-                                'type' => LEOrder::CHALLENGE_TYPE_DNS,
445
-                                'identifier' => $auth->identifier['value'],
446
-                                'DNSDigest' => $DNSDigest
447
-                            ];
448
-                            break;
449
-                    }
450
-                }
451
-            }
452
-        }
453
-
454
-        return count($authorizations) > 0 ? $authorizations : false;
455
-    }
456
-
457
-    /**
458
-     * Sends a verification request for a given $identifier and $type. The function itself checks whether the
459
-     * verification is valid before making the request.
460
-     * Updates the LetsEncrypt Authorization instances after a successful verification.
461
-     *
462
-     * @param string $identifier The domain name to verify.
463
-     * @param int $type The type of verification. Supporting LEOrder::CHALLENGE_TYPE_HTTP and
464
-     *                           LEOrder::CHALLENGE_TYPE_DNS.
465
-     *
466
-     * @return boolean  Returns true when the verification request was successful, false if not.
467
-     */
468
-    public function verifyPendingOrderAuthorization($identifier, $type)
469
-    {
470
-        $privateKey = $this->loadAccountKey();
471
-        $details = openssl_pkey_get_details($privateKey);
472
-
473
-        $header = [
474
-            "e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]),
475
-            "kty" => "RSA",
476
-            "n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"])
477
-        ];
478
-        $digest = LEFunctions::base64UrlSafeEncode(hash('sha256', json_encode($header), true));
479
-
480
-        foreach ($this->authorizations as $auth) {
481
-            if ($auth->identifier['value'] == $identifier) {
482
-                if ($auth->status == 'pending') {
483
-                    $challenge = $auth->getChallenge($type);
484
-                    if ($challenge['status'] == 'pending') {
485
-                        $keyAuthorization = $challenge['token'] . '.' . $digest;
486
-                        switch ($type) {
487
-                            case LEOrder::CHALLENGE_TYPE_HTTP:
488
-                                return $this->verifyHTTPChallenge($identifier, $challenge, $keyAuthorization, $auth);
489
-                            case LEOrder::CHALLENGE_TYPE_DNS:
490
-                                return $this->verifyDNSChallenge($identifier, $challenge, $keyAuthorization, $auth);
491
-                        }
492
-                    }
493
-                }
494
-            }
495
-        }
496
-
497
-        //f we reach here, the domain identifier given did not match any authorization object
498
-        //@codeCoverageIgnoreStart
499
-        throw new LogicException("Attempt to verify authorization for identifier $identifier not in order");
500
-        //@codeCoverageIgnoreEnd
501
-    }
502
-
503
-    private function verifyDNSChallenge($identifier, array $challenge, $keyAuthorization, LEAuthorization $auth)
504
-    {
505
-        //check it ourselves
506
-        $DNSDigest = LEFunctions::base64UrlSafeEncode(hash('sha256', $keyAuthorization, true));
507
-        if (!$this->dns->checkChallenge($identifier, $DNSDigest)) {
508
-            $this->log->warning("DNS challenge for $identifier tested, found invalid.");
509
-            return false;
510
-        }
511
-
512
-        //ask LE to check
513
-        $sign = $this->connector->signRequestKid(
514
-            ['keyAuthorization' => $keyAuthorization],
515
-            $this->connector->accountURL,
516
-            $challenge['url']
517
-        );
518
-        $post = $this->connector->post($challenge['url'], $sign);
519
-        if ($post['status'] !== 200) {
520
-            $this->log->warning("DNS challenge for $identifier valid, but failed to post to ACME service");
521
-            return false;
522
-        }
523
-
524
-        while ($auth->status == 'pending') {
525
-            $this->log->notice("DNS challenge for $identifier valid - waiting for confirmation");
526
-            $this->sleep->for(1);
527
-            $auth->updateData();
528
-        }
529
-        $this->log->notice("DNS challenge for $identifier validated");
530
-
531
-        return true;
532
-    }
533
-
534
-    private function verifyHTTPChallenge($identifier, array $challenge, $keyAuthorization, LEAuthorization $auth)
535
-    {
536
-        if (!$this->connector->checkHTTPChallenge($identifier, $challenge['token'], $keyAuthorization)) {
537
-            $this->log->warning("HTTP challenge for $identifier tested, found invalid.");
538
-            return false;
539
-        }
540
-
541
-        $sign = $this->connector->signRequestKid(
542
-            ['keyAuthorization' => $keyAuthorization],
543
-            $this->connector->accountURL,
544
-            $challenge['url']
545
-        );
546
-
547
-        $post = $this->connector->post($challenge['url'], $sign);
548
-        if ($post['status'] !== 200) {
549
-            //@codeCoverageIgnoreStart
550
-            $this->log->warning("HTTP challenge for $identifier valid, but failed to post to ACME service");
551
-            return false;
552
-            //@codeCoverageIgnoreEnd
553
-        }
554
-
555
-        while ($auth->status == 'pending') {
556
-            $this->log->notice("HTTP challenge for $identifier valid - waiting for confirmation");
557
-            $this->sleep->for(1);
558
-            $auth->updateData();
559
-        }
560
-        $this->log->notice("HTTP challenge for $identifier validated");
561
-        return true;
562
-    }
563
-
564
-    /*
19
+	const CHALLENGE_TYPE_HTTP = 'http-01';
20
+	const CHALLENGE_TYPE_DNS = 'dns-01';
21
+
22
+	/** @var string order status (pending, processing, valid) */
23
+	private $status;
24
+
25
+	/** @var string expiration date for order */
26
+	private $expires;
27
+
28
+	/** @var array containing all the domain identifiers for the order */
29
+	private $identifiers;
30
+
31
+	/** @var string[] URLs to all the authorization objects for this order */
32
+	private $authorizationURLs;
33
+
34
+	/** @var LEAuthorization[] array of authorization objects for the order */
35
+	private $authorizations;
36
+
37
+	/** @var string URL for order finalization */
38
+	private $finalizeURL;
39
+
40
+	/** @var string URL for obtaining certificate */
41
+	private $certificateURL;
42
+
43
+	/** @var string base domain name for certificate */
44
+	private $basename;
45
+
46
+	/** @var string URL referencing order */
47
+	private $orderURL;
48
+
49
+	/** @var string type of key (rsa or ec) */
50
+	private $keyType;
51
+
52
+	/** @var int size of key (typically 2048 or 4096 for rsa, 256 or 384 for ec */
53
+	private $keySize;
54
+
55
+	/** @var LEConnector ACME API connection provided to constructor */
56
+	private $connector;
57
+
58
+	/** @var LoggerInterface logger provided to constructor */
59
+	private $log;
60
+
61
+	/** @var DNSValidatorInterface dns resolution provider to constructor*/
62
+	private $dns;
63
+
64
+	/** @var Sleep sleep service provided to constructor */
65
+	private $sleep;
66
+
67
+	/** @var CertificateStorageInterface storage interface provided to constructor */
68
+	private $storage;
69
+
70
+	/** @var AccountStorageInterface */
71
+	private $accountStorage;
72
+
73
+	/**
74
+	 * Initiates the LetsEncrypt Order class. If the base name is found in the $keysDir directory, the order data is
75
+	 * requested. If no order was found locally, if the request is invalid or when there is a change in domain names, a
76
+	 * new order is created.
77
+	 *
78
+	 * @param LEConnector $connector The LetsEncrypt Connector instance to use for HTTP requests.
79
+	 * @param CertificateStorageInterface $storage
80
+	 * @param AccountStorageInterface $accountStorage
81
+	 * @param LoggerInterface $log PSR-3 compatible logger
82
+	 * @param DNSValidatorInterface $dns DNS challenge checking service
83
+	 * @param Sleep $sleep Sleep service for polling
84
+	 */
85
+	public function __construct(
86
+		LEConnector $connector,
87
+		CertificateStorageInterface $storage,
88
+		AccountStorageInterface $accountStorage,
89
+		LoggerInterface $log,
90
+		DNSValidatorInterface $dns,
91
+		Sleep $sleep
92
+	) {
93
+
94
+		$this->connector = $connector;
95
+		$this->log = $log;
96
+		$this->dns = $dns;
97
+		$this->sleep = $sleep;
98
+		$this->storage = $storage;
99
+		$this->accountStorage = $accountStorage;
100
+	}
101
+
102
+	/**
103
+	 * Loads or updates an order. If the base name is found in the $keysDir directory, the order data is
104
+	 * requested. If no order was found locally, if the request is invalid or when there is a change in domain names, a
105
+	 * new order is created.
106
+	 *
107
+	 * @param string $basename The base name for the order. Preferable the top domain (example.org).
108
+	 *                                         Will be the directory in which the keys are stored. Used for the
109
+	 *                                         CommonName in the certificate as well.
110
+	 * @param array $domains The array of strings containing the domain names on the certificate.
111
+	 * @param string $keyType Type of the key we want to use for certificate. Can be provided in
112
+	 *                                         ALGO-SIZE format (ex. rsa-4096 or ec-256) or simply "rsa" and "ec"
113
+	 *                                         (using default sizes)
114
+	 * @param string $notBefore A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
115
+	 *                                         at which the certificate becomes valid.
116
+	 * @param string $notAfter A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
117
+	 *                                         until which the certificate is valid.
118
+	 */
119
+	public function loadOrder($basename, array $domains, $keyType, $notBefore, $notAfter)
120
+	{
121
+		$this->basename = $basename;
122
+
123
+		$this->initialiseKeyTypeAndSize($keyType ?? 'rsa-4096');
124
+
125
+		if ($this->loadExistingOrder($domains)) {
126
+			$this->updateAuthorizations();
127
+		} else {
128
+			$this->createOrder($domains, $notBefore, $notAfter);
129
+		}
130
+	}
131
+
132
+	private function loadExistingOrder($domains)
133
+	{
134
+		$orderUrl = $this->storage->getMetadata($this->basename.'.order.url');
135
+		$publicKey = $this->storage->getPublicKey($this->basename);
136
+		$privateKey = $this->storage->getPrivateKey($this->basename);
137
+
138
+		//anything to load?
139
+		if (empty($orderUrl) || empty($publicKey) || empty($privateKey)) {
140
+			$this->log->info("No order found for {$this->basename}. Creating new order.");
141
+			return false;
142
+		}
143
+
144
+		//valid URL?
145
+		$this->orderURL = $orderUrl;
146
+		if (!filter_var($this->orderURL, FILTER_VALIDATE_URL)) {
147
+			//@codeCoverageIgnoreStart
148
+			$this->log->warning("Order for {$this->basename} has invalid URL. Creating new order.");
149
+			$this->deleteOrderFiles();
150
+			return false;
151
+			//@codeCoverageIgnoreEnd
152
+		}
153
+
154
+		//retrieve the order
155
+		$sign = $this->connector->signRequestKid(
156
+			null,
157
+			$this->connector->accountURL,
158
+			$this->orderURL
159
+		);
160
+
161
+		$post = $this->connector->post($this->orderURL, $sign);
162
+		if ($post['status'] !== 200) {
163
+			//@codeCoverageIgnoreStart
164
+			$this->log->warning("Order for {$this->basename} could not be loaded. Creating new order.");
165
+			$this->deleteOrderFiles();
166
+			return false;
167
+			//@codeCoverageIgnoreEnd
168
+		}
169
+
170
+		//ensure the order is still valid
171
+		if ($post['body']['status'] === 'invalid') {
172
+			$this->log->warning("Order for {$this->basename} is 'invalid', unable to authorize. Creating new order.");
173
+			$this->deleteOrderFiles();
174
+			return false;
175
+		}
176
+
177
+		//ensure retrieved order matches our domains
178
+		$orderdomains = array_map(function ($ident) {
179
+			return $ident['value'];
180
+		}, $post['body']['identifiers']);
181
+		$diff = array_merge(array_diff($orderdomains, $domains), array_diff($domains, $orderdomains));
182
+		if (!empty($diff)) {
183
+			$this->log->warning('Domains do not match order data. Deleting and creating new order.');
184
+			$this->deleteOrderFiles();
185
+			return false;
186
+		}
187
+
188
+		//the order is good
189
+		$this->status = $post['body']['status'];
190
+		$this->expires = $post['body']['expires'];
191
+		$this->identifiers = $post['body']['identifiers'];
192
+		$this->authorizationURLs = $post['body']['authorizations'];
193
+		$this->finalizeURL = $post['body']['finalize'];
194
+		if (array_key_exists('certificate', $post['body'])) {
195
+			$this->certificateURL = $post['body']['certificate'];
196
+		}
197
+
198
+		return true;
199
+	}
200
+
201
+	private function deleteOrderFiles()
202
+	{
203
+		$this->storage->setPrivateKey($this->basename, null);
204
+		$this->storage->setPublicKey($this->basename, null);
205
+		$this->storage->setCertificate($this->basename, null);
206
+		$this->storage->setFullChainCertificate($this->basename, null);
207
+		$this->storage->setMetadata($this->basename.'.order.url', null);
208
+	}
209
+
210
+	private function initialiseKeyTypeAndSize($keyType)
211
+	{
212
+		if ($keyType == 'rsa') {
213
+			$this->keyType = 'rsa';
214
+			$this->keySize = 4096;
215
+		} elseif ($keyType == 'ec') {
216
+			$this->keyType = 'ec';
217
+			$this->keySize = 256;
218
+		} else {
219
+			preg_match_all('/^(rsa|ec)\-([0-9]{3,4})$/', $keyType, $keyTypeParts, PREG_SET_ORDER, 0);
220
+
221
+			if (!empty($keyTypeParts)) {
222
+				$this->keyType = $keyTypeParts[0][1];
223
+				$this->keySize = intval($keyTypeParts[0][2]);
224
+			} else {
225
+				throw new LogicException('Key type \'' . $keyType . '\' not supported.');
226
+			}
227
+		}
228
+	}
229
+
230
+	/**
231
+	 * Creates a new LetsEncrypt order and fills this instance with its data. Subsequently creates a new RSA keypair
232
+	 * for the certificate.
233
+	 *
234
+	 * @param array $domains The array of strings containing the domain names on the certificate.
235
+	 * @param string $notBefore A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
236
+	 *                          at which the certificate becomes valid.
237
+	 * @param string $notAfter A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss)
238
+	 *                          until which the certificate is valid.
239
+	 */
240
+	private function createOrder($domains, $notBefore, $notAfter)
241
+	{
242
+		if (!preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notBefore) ||
243
+			!preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notAfter)
244
+		) {
245
+			throw new LogicException("notBefore and notAfter must be blank or iso-8601 datestamp");
246
+		}
247
+
248
+		$dns = [];
249
+		foreach ($domains as $domain) {
250
+			if (preg_match_all('~(\*\.)~', $domain) > 1) {
251
+				throw new LogicException('Cannot create orders with multiple wildcards in one domain.');
252
+			}
253
+			$dns[] = ['type' => 'dns', 'value' => $domain];
254
+		}
255
+		$payload = ["identifiers" => $dns, 'notBefore' => $notBefore, 'notAfter' => $notAfter];
256
+		$sign = $this->connector->signRequestKid(
257
+			$payload,
258
+			$this->connector->accountURL,
259
+			$this->connector->newOrder
260
+		);
261
+		$post = $this->connector->post($this->connector->newOrder, $sign);
262
+		if ($post['status'] !== 201) {
263
+			//@codeCoverageIgnoreStart
264
+			throw new RuntimeException('Creating new order failed.');
265
+			//@codeCoverageIgnoreEnd
266
+		}
267
+
268
+		if (!preg_match('~Location: (\S+)~i', $post['header'], $matches)) {
269
+			//@codeCoverageIgnoreStart
270
+			throw new RuntimeException('New-order returned invalid response.');
271
+			//@codeCoverageIgnoreEnd
272
+		}
273
+
274
+		$this->orderURL = trim($matches[1]);
275
+		$this->storage->setMetadata($this->basename.'.order.url', $this->orderURL);
276
+
277
+		$this->generateKeys();
278
+
279
+		$this->status = $post['body']['status'];
280
+		$this->expires = $post['body']['expires'];
281
+		$this->identifiers = $post['body']['identifiers'];
282
+		$this->authorizationURLs = $post['body']['authorizations'];
283
+		$this->finalizeURL = $post['body']['finalize'];
284
+		if (array_key_exists('certificate', $post['body'])) {
285
+			$this->certificateURL = $post['body']['certificate'];
286
+		}
287
+		$this->updateAuthorizations();
288
+
289
+		$this->log->info('Created order for ' . $this->basename);
290
+	}
291
+
292
+	private function generateKeys()
293
+	{
294
+		if ($this->keyType == "rsa") {
295
+			$key = LEFunctions::RSAgenerateKeys($this->keySize);
296
+		} else {
297
+			$key = LEFunctions::ECgenerateKeys($this->keySize);
298
+		}
299
+
300
+		$this->storage->setPublicKey($this->basename, $key['public']);
301
+		$this->storage->setPrivateKey($this->basename, $key['private']);
302
+	}
303
+
304
+	/**
305
+	 * Fetches the latest data concerning this LetsEncrypt Order instance and fills this instance with the new data.
306
+	 */
307
+	private function updateOrderData()
308
+	{
309
+		$sign = $this->connector->signRequestKid(
310
+			null,
311
+			$this->connector->accountURL,
312
+			$this->orderURL
313
+		);
314
+
315
+		$post = $this->connector->post($this->orderURL, $sign);
316
+		if (strpos($post['header'], "200 OK") !== false) {
317
+			$this->status = $post['body']['status'];
318
+			$this->expires = $post['body']['expires'];
319
+			$this->identifiers = $post['body']['identifiers'];
320
+			$this->authorizationURLs = $post['body']['authorizations'];
321
+			$this->finalizeURL = $post['body']['finalize'];
322
+			if (array_key_exists('certificate', $post['body'])) {
323
+				$this->certificateURL = $post['body']['certificate'];
324
+			}
325
+			$this->updateAuthorizations();
326
+		} else {
327
+			//@codeCoverageIgnoreStart
328
+			$this->log->error("Failed to fetch order for {$this->basename}");
329
+			//@codeCoverageIgnoreEnd
330
+		}
331
+	}
332
+
333
+	/**
334
+	 * Fetches the latest data concerning all authorizations connected to this LetsEncrypt Order instance and
335
+	 * creates and stores a new LetsEncrypt Authorization instance for each one.
336
+	 */
337
+	private function updateAuthorizations()
338
+	{
339
+		$this->authorizations = [];
340
+		foreach ($this->authorizationURLs as $authURL) {
341
+			if (filter_var($authURL, FILTER_VALIDATE_URL)) {
342
+				$auth = new LEAuthorization($this->connector, $this->log, $authURL);
343
+				if ($auth != false) {
344
+					$this->authorizations[] = $auth;
345
+				}
346
+			}
347
+		}
348
+	}
349
+
350
+	/**
351
+	 * Walks all LetsEncrypt Authorization instances and returns whether they are all valid (verified).
352
+	 *
353
+	 * @return boolean  Returns true if all authorizations are valid (verified), returns false if not.
354
+	 */
355
+	public function allAuthorizationsValid()
356
+	{
357
+		if (count($this->authorizations) > 0) {
358
+			foreach ($this->authorizations as $auth) {
359
+				if ($auth->status != 'valid') {
360
+					return false;
361
+				}
362
+			}
363
+			return true;
364
+		}
365
+		return false;
366
+	}
367
+
368
+	private function loadAccountKey()
369
+	{
370
+		$keydata = $this->accountStorage->getAccountPrivateKey();
371
+		$privateKey = openssl_pkey_get_private($keydata);
372
+		if ($privateKey === false) {
373
+			//@codeCoverageIgnoreStart
374
+			throw new RuntimeException("Failed load account key");
375
+			//@codeCoverageIgnoreEnd
376
+		}
377
+		return $privateKey;
378
+	}
379
+
380
+
381
+	private function loadCertificateKey()
382
+	{
383
+		$keydata = $this->storage->getPrivateKey($this->basename);
384
+		$privateKey = openssl_pkey_get_private($keydata);
385
+		if ($privateKey === false) {
386
+			//@codeCoverageIgnoreStart
387
+			throw new RuntimeException("Failed load certificate key");
388
+			//@codeCoverageIgnoreEnd
389
+		}
390
+		return $privateKey;
391
+	}
392
+
393
+	/**
394
+	 * Get all pending LetsEncrypt Authorization instances and return the necessary data for verification.
395
+	 * The data in the return object depends on the $type.
396
+	 *
397
+	 * @param string $type The type of verification to get. Supporting http-01 and dns-01.
398
+	 *                     Supporting LEOrder::CHALLENGE_TYPE_HTTP and LEOrder::CHALLENGE_TYPE_DNS. Throws a Runtime
399
+	 *                     Exception when requesting an unknown $type. Keep in mind a wildcard domain authorization only
400
+	 *                     accepts LEOrder::CHALLENGE_TYPE_DNS.
401
+	 *
402
+	 * @return array|bool Returns an array with verification data if successful, false if not pending LetsEncrypt
403
+	 *                  Authorization instances were found. The return array always
404
+	 *                  contains 'type' and 'identifier'. For LEOrder::CHALLENGE_TYPE_HTTP, the array contains
405
+	 *                  'filename' and 'content' for necessary the authorization file.
406
+	 *                  For LEOrder::CHALLENGE_TYPE_DNS, the array contains 'DNSDigest', which is the content for the
407
+	 *                  necessary DNS TXT entry.
408
+	 */
409
+
410
+	public function getPendingAuthorizations($type)
411
+	{
412
+		$authorizations = [];
413
+
414
+		$privateKey = $this->loadAccountKey();
415
+		$details = openssl_pkey_get_details($privateKey);
416
+
417
+		$header = [
418
+			"e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]),
419
+			"kty" => "RSA",
420
+			"n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"])
421
+
422
+		];
423
+		$digest = LEFunctions::base64UrlSafeEncode(hash('sha256', json_encode($header), true));
424
+
425
+		foreach ($this->authorizations as $auth) {
426
+			if ($auth->status == 'pending') {
427
+				$challenge = $auth->getChallenge($type);
428
+				if ($challenge['status'] == 'pending') {
429
+					$keyAuthorization = $challenge['token'] . '.' . $digest;
430
+					switch (strtolower($type)) {
431
+						case LEOrder::CHALLENGE_TYPE_HTTP:
432
+							$authorizations[] = [
433
+								'type' => LEOrder::CHALLENGE_TYPE_HTTP,
434
+								'identifier' => $auth->identifier['value'],
435
+								'filename' => $challenge['token'],
436
+								'content' => $keyAuthorization
437
+							];
438
+							break;
439
+						case LEOrder::CHALLENGE_TYPE_DNS:
440
+							$DNSDigest = LEFunctions::base64UrlSafeEncode(
441
+								hash('sha256', $keyAuthorization, true)
442
+							);
443
+							$authorizations[] = [
444
+								'type' => LEOrder::CHALLENGE_TYPE_DNS,
445
+								'identifier' => $auth->identifier['value'],
446
+								'DNSDigest' => $DNSDigest
447
+							];
448
+							break;
449
+					}
450
+				}
451
+			}
452
+		}
453
+
454
+		return count($authorizations) > 0 ? $authorizations : false;
455
+	}
456
+
457
+	/**
458
+	 * Sends a verification request for a given $identifier and $type. The function itself checks whether the
459
+	 * verification is valid before making the request.
460
+	 * Updates the LetsEncrypt Authorization instances after a successful verification.
461
+	 *
462
+	 * @param string $identifier The domain name to verify.
463
+	 * @param int $type The type of verification. Supporting LEOrder::CHALLENGE_TYPE_HTTP and
464
+	 *                           LEOrder::CHALLENGE_TYPE_DNS.
465
+	 *
466
+	 * @return boolean  Returns true when the verification request was successful, false if not.
467
+	 */
468
+	public function verifyPendingOrderAuthorization($identifier, $type)
469
+	{
470
+		$privateKey = $this->loadAccountKey();
471
+		$details = openssl_pkey_get_details($privateKey);
472
+
473
+		$header = [
474
+			"e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]),
475
+			"kty" => "RSA",
476
+			"n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"])
477
+		];
478
+		$digest = LEFunctions::base64UrlSafeEncode(hash('sha256', json_encode($header), true));
479
+
480
+		foreach ($this->authorizations as $auth) {
481
+			if ($auth->identifier['value'] == $identifier) {
482
+				if ($auth->status == 'pending') {
483
+					$challenge = $auth->getChallenge($type);
484
+					if ($challenge['status'] == 'pending') {
485
+						$keyAuthorization = $challenge['token'] . '.' . $digest;
486
+						switch ($type) {
487
+							case LEOrder::CHALLENGE_TYPE_HTTP:
488
+								return $this->verifyHTTPChallenge($identifier, $challenge, $keyAuthorization, $auth);
489
+							case LEOrder::CHALLENGE_TYPE_DNS:
490
+								return $this->verifyDNSChallenge($identifier, $challenge, $keyAuthorization, $auth);
491
+						}
492
+					}
493
+				}
494
+			}
495
+		}
496
+
497
+		//f we reach here, the domain identifier given did not match any authorization object
498
+		//@codeCoverageIgnoreStart
499
+		throw new LogicException("Attempt to verify authorization for identifier $identifier not in order");
500
+		//@codeCoverageIgnoreEnd
501
+	}
502
+
503
+	private function verifyDNSChallenge($identifier, array $challenge, $keyAuthorization, LEAuthorization $auth)
504
+	{
505
+		//check it ourselves
506
+		$DNSDigest = LEFunctions::base64UrlSafeEncode(hash('sha256', $keyAuthorization, true));
507
+		if (!$this->dns->checkChallenge($identifier, $DNSDigest)) {
508
+			$this->log->warning("DNS challenge for $identifier tested, found invalid.");
509
+			return false;
510
+		}
511
+
512
+		//ask LE to check
513
+		$sign = $this->connector->signRequestKid(
514
+			['keyAuthorization' => $keyAuthorization],
515
+			$this->connector->accountURL,
516
+			$challenge['url']
517
+		);
518
+		$post = $this->connector->post($challenge['url'], $sign);
519
+		if ($post['status'] !== 200) {
520
+			$this->log->warning("DNS challenge for $identifier valid, but failed to post to ACME service");
521
+			return false;
522
+		}
523
+
524
+		while ($auth->status == 'pending') {
525
+			$this->log->notice("DNS challenge for $identifier valid - waiting for confirmation");
526
+			$this->sleep->for(1);
527
+			$auth->updateData();
528
+		}
529
+		$this->log->notice("DNS challenge for $identifier validated");
530
+
531
+		return true;
532
+	}
533
+
534
+	private function verifyHTTPChallenge($identifier, array $challenge, $keyAuthorization, LEAuthorization $auth)
535
+	{
536
+		if (!$this->connector->checkHTTPChallenge($identifier, $challenge['token'], $keyAuthorization)) {
537
+			$this->log->warning("HTTP challenge for $identifier tested, found invalid.");
538
+			return false;
539
+		}
540
+
541
+		$sign = $this->connector->signRequestKid(
542
+			['keyAuthorization' => $keyAuthorization],
543
+			$this->connector->accountURL,
544
+			$challenge['url']
545
+		);
546
+
547
+		$post = $this->connector->post($challenge['url'], $sign);
548
+		if ($post['status'] !== 200) {
549
+			//@codeCoverageIgnoreStart
550
+			$this->log->warning("HTTP challenge for $identifier valid, but failed to post to ACME service");
551
+			return false;
552
+			//@codeCoverageIgnoreEnd
553
+		}
554
+
555
+		while ($auth->status == 'pending') {
556
+			$this->log->notice("HTTP challenge for $identifier valid - waiting for confirmation");
557
+			$this->sleep->for(1);
558
+			$auth->updateData();
559
+		}
560
+		$this->log->notice("HTTP challenge for $identifier validated");
561
+		return true;
562
+	}
563
+
564
+	/*
565 565
      * Deactivate an LetsEncrypt Authorization instance.
566 566
      *
567 567
      * @param string $identifier The domain name for which the verification should be deactivated.
568 568
      *
569 569
      * @return boolean  Returns true is the deactivation request was successful, false if not.
570 570
      */
571
-    /*
571
+	/*
572 572
     public function deactivateOrderAuthorization($identifier)
573 573
     {
574 574
         foreach ($this->authorizations as $auth) {
@@ -593,37 +593,37 @@  discard block
 block discarded – undo
593 593
     }
594 594
     */
595 595
 
596
-    /**
597
-     * Generates a Certificate Signing Request for the identifiers in the current LetsEncrypt Order instance.
598
-     * If possible, the base name will be the certificate common name and all domain names in this LetsEncrypt Order
599
-     * instance will be added to the Subject Alternative Names entry.
600
-     *
601
-     * @return string   Returns the generated CSR as string, unprepared for LetsEncrypt. Preparation for the request
602
-     *                  happens in finalizeOrder()
603
-     */
604
-    private function generateCSR()
605
-    {
606
-        $domains = array_map(function ($dns) {
607
-            return $dns['value'];
608
-        }, $this->identifiers);
609
-
610
-        $dn = ["commonName" => $this->calcCommonName($domains)];
611
-
612
-        $san = implode(",", array_map(function ($dns) {
613
-            return "DNS:" . $dns;
614
-        }, $domains));
615
-        $tmpConf = tmpfile();
616
-        if ($tmpConf === false) {
617
-            //@codeCoverageIgnoreStart
618
-            throw new RuntimeException('LEOrder::generateCSR failed to create tmp file');
619
-            //@codeCoverageIgnoreEnd
620
-        }
621
-        $tmpConfMeta = stream_get_meta_data($tmpConf);
622
-        $tmpConfPath = $tmpConfMeta["uri"];
623
-
624
-        fwrite(
625
-            $tmpConf,
626
-            'HOME = .
596
+	/**
597
+	 * Generates a Certificate Signing Request for the identifiers in the current LetsEncrypt Order instance.
598
+	 * If possible, the base name will be the certificate common name and all domain names in this LetsEncrypt Order
599
+	 * instance will be added to the Subject Alternative Names entry.
600
+	 *
601
+	 * @return string   Returns the generated CSR as string, unprepared for LetsEncrypt. Preparation for the request
602
+	 *                  happens in finalizeOrder()
603
+	 */
604
+	private function generateCSR()
605
+	{
606
+		$domains = array_map(function ($dns) {
607
+			return $dns['value'];
608
+		}, $this->identifiers);
609
+
610
+		$dn = ["commonName" => $this->calcCommonName($domains)];
611
+
612
+		$san = implode(",", array_map(function ($dns) {
613
+			return "DNS:" . $dns;
614
+		}, $domains));
615
+		$tmpConf = tmpfile();
616
+		if ($tmpConf === false) {
617
+			//@codeCoverageIgnoreStart
618
+			throw new RuntimeException('LEOrder::generateCSR failed to create tmp file');
619
+			//@codeCoverageIgnoreEnd
620
+		}
621
+		$tmpConfMeta = stream_get_meta_data($tmpConf);
622
+		$tmpConfPath = $tmpConfMeta["uri"];
623
+
624
+		fwrite(
625
+			$tmpConf,
626
+			'HOME = .
627 627
 			RANDFILE = $ENV::HOME/.rnd
628 628
 			[ req ]
629 629
 			default_bits = ' . $this->keySize . '
@@ -636,198 +636,198 @@  discard block
 block discarded – undo
636 636
 			basicConstraints = CA:FALSE
637 637
 			subjectAltName = ' . $san . '
638 638
 			keyUsage = nonRepudiation, digitalSignature, keyEncipherment'
639
-        );
640
-
641
-        $privateKey = $this->loadCertificateKey();
642
-        $csr = openssl_csr_new($dn, $privateKey, ['config' => $tmpConfPath, 'digest_alg' => 'sha256']);
643
-        openssl_csr_export($csr, $csr);
644
-        return $csr;
645
-    }
646
-
647
-    private function calcCommonName($domains)
648
-    {
649
-        if (in_array($this->basename, $domains)) {
650
-            $CN = $this->basename;
651
-        } elseif (in_array('*.' . $this->basename, $domains)) {
652
-            $CN = '*.' . $this->basename;
653
-        } else {
654
-            $CN = $domains[0];
655
-        }
656
-        return $CN;
657
-    }
658
-
659
-    /**
660
-     * Checks, for redundancy, whether all authorizations are valid, and finalizes the order. Updates this LetsEncrypt
661
-     * Order instance with the new data.
662
-     *
663
-     * @param string $csr The Certificate Signing Request as a string. Can be a custom CSR. If empty, a CSR will
664
-     *                    be generated with the generateCSR() function.
665
-     *
666
-     * @return boolean  Returns true if the finalize request was successful, false if not.
667
-     */
668
-    public function finalizeOrder($csr = '')
669
-    {
670
-        if ($this->status == 'pending' || $this->status == 'ready') {
671
-            if ($this->allAuthorizationsValid()) {
672
-                if (empty($csr)) {
673
-                    $csr = $this->generateCSR();
674
-                }
675
-                if (preg_match(
676
-                    '~-----BEGIN\sCERTIFICATE\sREQUEST-----(.*)-----END\sCERTIFICATE\sREQUEST-----~s',
677
-                    $csr,
678
-                    $matches
679
-                )
680
-                ) {
681
-                    $csr = $matches[1];
682
-                }
683
-                $csr = trim(LEFunctions::base64UrlSafeEncode(base64_decode($csr)));
684
-                $sign = $this->connector->signRequestKid(
685
-                    ['csr' => $csr],
686
-                    $this->connector->accountURL,
687
-                    $this->finalizeURL
688
-                );
689
-                $post = $this->connector->post($this->finalizeURL, $sign);
690
-                if (strpos($post['header'], "200 OK") !== false) {
691
-                    $this->status = $post['body']['status'];
692
-                    $this->expires = $post['body']['expires'];
693
-                    $this->identifiers = $post['body']['identifiers'];
694
-                    $this->authorizationURLs = $post['body']['authorizations'];
695
-                    $this->finalizeURL = $post['body']['finalize'];
696
-                    if (array_key_exists('certificate', $post['body'])) {
697
-                        $this->certificateURL = $post['body']['certificate'];
698
-                    }
699
-                    $this->updateAuthorizations();
700
-                    $this->log->info('Order for \'' . $this->basename . '\' finalized.');
701
-
702
-                    return true;
703
-                }
704
-            } else {
705
-                $this->log->warning(
706
-                    'Not all authorizations are valid for \'' .
707
-                    $this->basename . '\'. Cannot finalize order.'
708
-                );
709
-            }
710
-        } else {
711
-            $this->log->warning(
712
-                'Order status for \'' . $this->basename .
713
-                '\' is \'' . $this->status . '\'. Cannot finalize order.'
714
-            );
715
-        }
716
-        return false;
717
-    }
718
-
719
-    /**
720
-     * Gets whether the LetsEncrypt Order is finalized by checking whether the status is processing or valid. Keep in
721
-     * mind, a certificate is not yet available when the status still is processing.
722
-     *
723
-     * @return boolean  Returns true if finalized, false if not.
724
-     */
725
-    public function isFinalized()
726
-    {
727
-        return ($this->status == 'processing' || $this->status == 'valid');
728
-    }
729
-
730
-    /**
731
-     * Requests the certificate for this LetsEncrypt Order instance, after finalization. When the order status is still
732
-     * 'processing', the order will be polled max four times with five seconds in between. If the status becomes 'valid'
733
-     * in the meantime, the certificate will be requested. Else, the function returns false.
734
-     *
735
-     * @return boolean  Returns true if the certificate is stored successfully, false if the certificate could not be
736
-     *                  retrieved or the status remained 'processing'.
737
-     */
738
-    public function getCertificate()
739
-    {
740
-        $polling = 0;
741
-        while ($this->status == 'processing' && $polling < 4) {
742
-            $this->log->info('Certificate for ' . $this->basename . ' being processed. Retrying in 5 seconds...');
743
-
744
-            $this->sleep->for(5);
745
-            $this->updateOrderData();
746
-            $polling++;
747
-        }
748
-
749
-        if ($this->status != 'valid' || empty($this->certificateURL)) {
750
-            $this->log->warning(
751
-                'Order for ' . $this->basename . ' not valid. Cannot retrieve certificate.'
752
-            );
753
-            return false;
754
-        }
755
-
756
-        $sign = $this->connector->signRequestKid(
757
-            null,
758
-            $this->connector->accountURL,
759
-            $this->certificateURL
760
-        );
761
-
762
-        $post = $this->connector->post($this->certificateURL, $sign);
763
-        if (strpos($post['header'], "200 OK") === false) {
764
-            $this->log->warning(
765
-                'Invalid response for certificate request for \'' . $this->basename .
766
-                '\'. Cannot save certificate.'
767
-            );
768
-            return false;
769
-        }
770
-
771
-        return $this->writeCertificates($post['body']);
772
-    }
773
-
774
-    private function writeCertificates($body)
775
-    {
776
-        if (preg_match_all('~(-----BEGIN\sCERTIFICATE-----[\s\S]+?-----END\sCERTIFICATE-----)~i', $body, $matches)) {
777
-            $this->storage->setCertificate($this->basename, $matches[0][0]);
778
-
779
-            $matchCount = count($matches[0]);
780
-            if ($matchCount > 1) {
781
-                $fullchain = $matches[0][0] . "\n";
782
-
783
-                for ($i = 1; $i < $matchCount; $i++) {
784
-                    $fullchain .= $matches[0][$i] . "\n";
785
-                }
786
-                $this->storage->setFullChainCertificate($this->basename, $fullchain);
787
-            }
788
-            $this->log->info("Certificate for {$this->basename} stored");
789
-            return true;
790
-        }
791
-
792
-        $this->log->error("Received invalid certificate for {$this->basename}, cannot save");
793
-        return false;
794
-    }
795
-
796
-    /**
797
-     * Revokes the certificate in the current LetsEncrypt Order instance, if existent. Unlike stated in the ACME draft,
798
-     * the certificate revoke request cannot be signed with the account private key, and will be signed with the
799
-     * certificate private key.
800
-     *
801
-     * @param int $reason The reason to revoke the LetsEncrypt Order instance certificate. Possible reasons can be
802
-     *                        found in section 5.3.1 of RFC5280.
803
-     *
804
-     * @return boolean  Returns true if the certificate was successfully revoked, false if not.
805
-     */
806
-    public function revokeCertificate($reason = 0)
807
-    {
808
-        if ($this->status != 'valid') {
809
-            $this->log->warning("Order for {$this->basename} not valid, cannot revoke");
810
-            return false;
811
-        }
812
-
813
-        $certificate = $this->storage->getCertificate($this->basename);
814
-        if (empty($certificate)) {
815
-            $this->log->warning("Certificate for {$this->basename} not found, cannot revoke");
816
-            return false;
817
-        }
818
-
819
-        preg_match('~-----BEGIN\sCERTIFICATE-----(.*)-----END\sCERTIFICATE-----~s', $certificate, $matches);
820
-        $certificate = trim(LEFunctions::base64UrlSafeEncode(base64_decode(trim($matches[1]))));
821
-
822
-        $certificateKey = $this->storage->getPrivateKey($this->basename);
823
-        $sign = $this->connector->signRequestJWK(
824
-            ['certificate' => $certificate, 'reason' => $reason],
825
-            $this->connector->revokeCert,
826
-            $certificateKey
827
-        );
828
-        //4**/5** responses will throw an exception...
829
-        $this->connector->post($this->connector->revokeCert, $sign);
830
-        $this->log->info("Certificate for {$this->basename} successfully revoked");
831
-        return true;
832
-    }
639
+		);
640
+
641
+		$privateKey = $this->loadCertificateKey();
642
+		$csr = openssl_csr_new($dn, $privateKey, ['config' => $tmpConfPath, 'digest_alg' => 'sha256']);
643
+		openssl_csr_export($csr, $csr);
644
+		return $csr;
645
+	}
646
+
647
+	private function calcCommonName($domains)
648
+	{
649
+		if (in_array($this->basename, $domains)) {
650
+			$CN = $this->basename;
651
+		} elseif (in_array('*.' . $this->basename, $domains)) {
652
+			$CN = '*.' . $this->basename;
653
+		} else {
654
+			$CN = $domains[0];
655
+		}
656
+		return $CN;
657
+	}
658
+
659
+	/**
660
+	 * Checks, for redundancy, whether all authorizations are valid, and finalizes the order. Updates this LetsEncrypt
661
+	 * Order instance with the new data.
662
+	 *
663
+	 * @param string $csr The Certificate Signing Request as a string. Can be a custom CSR. If empty, a CSR will
664
+	 *                    be generated with the generateCSR() function.
665
+	 *
666
+	 * @return boolean  Returns true if the finalize request was successful, false if not.
667
+	 */
668
+	public function finalizeOrder($csr = '')
669
+	{
670
+		if ($this->status == 'pending' || $this->status == 'ready') {
671
+			if ($this->allAuthorizationsValid()) {
672
+				if (empty($csr)) {
673
+					$csr = $this->generateCSR();
674
+				}
675
+				if (preg_match(
676
+					'~-----BEGIN\sCERTIFICATE\sREQUEST-----(.*)-----END\sCERTIFICATE\sREQUEST-----~s',
677
+					$csr,
678
+					$matches
679
+				)
680
+				) {
681
+					$csr = $matches[1];
682
+				}
683
+				$csr = trim(LEFunctions::base64UrlSafeEncode(base64_decode($csr)));
684
+				$sign = $this->connector->signRequestKid(
685
+					['csr' => $csr],
686
+					$this->connector->accountURL,
687
+					$this->finalizeURL
688
+				);
689
+				$post = $this->connector->post($this->finalizeURL, $sign);
690
+				if (strpos($post['header'], "200 OK") !== false) {
691
+					$this->status = $post['body']['status'];
692
+					$this->expires = $post['body']['expires'];
693
+					$this->identifiers = $post['body']['identifiers'];
694
+					$this->authorizationURLs = $post['body']['authorizations'];
695
+					$this->finalizeURL = $post['body']['finalize'];
696
+					if (array_key_exists('certificate', $post['body'])) {
697
+						$this->certificateURL = $post['body']['certificate'];
698
+					}
699
+					$this->updateAuthorizations();
700
+					$this->log->info('Order for \'' . $this->basename . '\' finalized.');
701
+
702
+					return true;
703
+				}
704
+			} else {
705
+				$this->log->warning(
706
+					'Not all authorizations are valid for \'' .
707
+					$this->basename . '\'. Cannot finalize order.'
708
+				);
709
+			}
710
+		} else {
711
+			$this->log->warning(
712
+				'Order status for \'' . $this->basename .
713
+				'\' is \'' . $this->status . '\'. Cannot finalize order.'
714
+			);
715
+		}
716
+		return false;
717
+	}
718
+
719
+	/**
720
+	 * Gets whether the LetsEncrypt Order is finalized by checking whether the status is processing or valid. Keep in
721
+	 * mind, a certificate is not yet available when the status still is processing.
722
+	 *
723
+	 * @return boolean  Returns true if finalized, false if not.
724
+	 */
725
+	public function isFinalized()
726
+	{
727
+		return ($this->status == 'processing' || $this->status == 'valid');
728
+	}
729
+
730
+	/**
731
+	 * Requests the certificate for this LetsEncrypt Order instance, after finalization. When the order status is still
732
+	 * 'processing', the order will be polled max four times with five seconds in between. If the status becomes 'valid'
733
+	 * in the meantime, the certificate will be requested. Else, the function returns false.
734
+	 *
735
+	 * @return boolean  Returns true if the certificate is stored successfully, false if the certificate could not be
736
+	 *                  retrieved or the status remained 'processing'.
737
+	 */
738
+	public function getCertificate()
739
+	{
740
+		$polling = 0;
741
+		while ($this->status == 'processing' && $polling < 4) {
742
+			$this->log->info('Certificate for ' . $this->basename . ' being processed. Retrying in 5 seconds...');
743
+
744
+			$this->sleep->for(5);
745
+			$this->updateOrderData();
746
+			$polling++;
747
+		}
748
+
749
+		if ($this->status != 'valid' || empty($this->certificateURL)) {
750
+			$this->log->warning(
751
+				'Order for ' . $this->basename . ' not valid. Cannot retrieve certificate.'
752
+			);
753
+			return false;
754
+		}
755
+
756
+		$sign = $this->connector->signRequestKid(
757
+			null,
758
+			$this->connector->accountURL,
759
+			$this->certificateURL
760
+		);
761
+
762
+		$post = $this->connector->post($this->certificateURL, $sign);
763
+		if (strpos($post['header'], "200 OK") === false) {
764
+			$this->log->warning(
765
+				'Invalid response for certificate request for \'' . $this->basename .
766
+				'\'. Cannot save certificate.'
767
+			);
768
+			return false;
769
+		}
770
+
771
+		return $this->writeCertificates($post['body']);
772
+	}
773
+
774
+	private function writeCertificates($body)
775
+	{
776
+		if (preg_match_all('~(-----BEGIN\sCERTIFICATE-----[\s\S]+?-----END\sCERTIFICATE-----)~i', $body, $matches)) {
777
+			$this->storage->setCertificate($this->basename, $matches[0][0]);
778
+
779
+			$matchCount = count($matches[0]);
780
+			if ($matchCount > 1) {
781
+				$fullchain = $matches[0][0] . "\n";
782
+
783
+				for ($i = 1; $i < $matchCount; $i++) {
784
+					$fullchain .= $matches[0][$i] . "\n";
785
+				}
786
+				$this->storage->setFullChainCertificate($this->basename, $fullchain);
787
+			}
788
+			$this->log->info("Certificate for {$this->basename} stored");
789
+			return true;
790
+		}
791
+
792
+		$this->log->error("Received invalid certificate for {$this->basename}, cannot save");
793
+		return false;
794
+	}
795
+
796
+	/**
797
+	 * Revokes the certificate in the current LetsEncrypt Order instance, if existent. Unlike stated in the ACME draft,
798
+	 * the certificate revoke request cannot be signed with the account private key, and will be signed with the
799
+	 * certificate private key.
800
+	 *
801
+	 * @param int $reason The reason to revoke the LetsEncrypt Order instance certificate. Possible reasons can be
802
+	 *                        found in section 5.3.1 of RFC5280.
803
+	 *
804
+	 * @return boolean  Returns true if the certificate was successfully revoked, false if not.
805
+	 */
806
+	public function revokeCertificate($reason = 0)
807
+	{
808
+		if ($this->status != 'valid') {
809
+			$this->log->warning("Order for {$this->basename} not valid, cannot revoke");
810
+			return false;
811
+		}
812
+
813
+		$certificate = $this->storage->getCertificate($this->basename);
814
+		if (empty($certificate)) {
815
+			$this->log->warning("Certificate for {$this->basename} not found, cannot revoke");
816
+			return false;
817
+		}
818
+
819
+		preg_match('~-----BEGIN\sCERTIFICATE-----(.*)-----END\sCERTIFICATE-----~s', $certificate, $matches);
820
+		$certificate = trim(LEFunctions::base64UrlSafeEncode(base64_decode(trim($matches[1]))));
821
+
822
+		$certificateKey = $this->storage->getPrivateKey($this->basename);
823
+		$sign = $this->connector->signRequestJWK(
824
+			['certificate' => $certificate, 'reason' => $reason],
825
+			$this->connector->revokeCert,
826
+			$certificateKey
827
+		);
828
+		//4**/5** responses will throw an exception...
829
+		$this->connector->post($this->connector->revokeCert, $sign);
830
+		$this->log->info("Certificate for {$this->basename} successfully revoked");
831
+		return true;
832
+	}
833 833
 }
Please login to merge, or discard this patch.