40 | */
41 | public function getAlternativeLinks() : array {
42 |
43 | $result = [];
44 |
45 | foreach($this->_raw->header as $line) {
46 | $matches = [];
47 | preg_match_all('/^link: <(.*)>;rel="alternate"$/', $line, $matches);
48 |
49 | if(isset($matches[1][0])) {
50 | $result[] = $matches[1][0];
51 | }
52 | }
53 |
54 | return $result;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/LE_ACME2/Response/Order/RevokeCertificate.php:
--------------------------------------------------------------------------------
1 | path = $path;
16 | $this->private = $private;
17 | $this->certificate = $certificate;
18 | $this->intermediate = $intermediate;
19 | $this->expireTime = $expireTime;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/LE_ACME2/Struct/ChallengeAuthorizationKey.php:
--------------------------------------------------------------------------------
1 | _account = $account;
14 | }
15 |
16 | public function get(string $token) : string {
17 | return $token . '.' . $this->_getDigest();
18 | }
19 |
20 | public function getEncoded(string $token) : string {
21 | return Utilities\Base64::UrlSafeEncode(
22 | hash('sha256', $this->get($token), true)
23 | );
24 | }
25 |
26 | private function _getDigest() : string {
27 |
28 | $privateKey = openssl_pkey_get_private(file_get_contents($this->_account->getKeyDirectoryPath() . 'private.pem'));
29 | $details = openssl_pkey_get_details($privateKey);
30 |
31 | $header = array(
32 | "e" => Utilities\Base64::UrlSafeEncode($details["rsa"]["e"]),
33 | "kty" => "RSA",
34 | "n" => Utilities\Base64::UrlSafeEncode($details["rsa"]["n"])
35 |
36 | );
37 | return Utilities\Base64::UrlSafeEncode(hash('sha256', json_encode($header), true));
38 | }
39 | }
--------------------------------------------------------------------------------
/src/LE_ACME2/Utilities/Base64.php:
--------------------------------------------------------------------------------
1 | $order->getSubjects()[0]
17 | ];
18 |
19 | $san = implode(",", array_map(function ($dns) {
20 |
21 | return "DNS:" . $dns;
22 | }, $order->getSubjects())
23 | );
24 |
25 | $configFilePath = $order->getKeyDirectoryPath() . 'csr_config';
26 |
27 | $config = 'HOME = .
28 | RANDFILE = ' . $order->getKeyDirectoryPath() . '.rnd
29 | [ req ]
30 | default_bits = 4096
31 | default_keyfile = privkey.pem
32 | distinguished_name = req_distinguished_name
33 | req_extensions = v3_req
34 | [ req_distinguished_name ]
35 | countryName = Country Name (2 letter code)
36 | [ v3_req ]
37 | basicConstraints = CA:FALSE
38 | subjectAltName = ' . $san . '
39 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment';
40 |
41 | file_put_contents($configFilePath, $config);
42 |
43 | $privateKey = openssl_pkey_get_private(
44 | file_get_contents($order->getKeyDirectoryPath() . 'private.pem')
45 | );
46 |
47 | if($privateKey === false) {
48 | throw new OpenSSLException('openssl_pkey_get_private');
49 | }
50 |
51 | $csr = openssl_csr_new(
52 | $dn,
53 | $privateKey,
54 | [
55 | 'config' => $configFilePath,
56 | 'digest_alg' => 'sha256'
57 | ]
58 | );
59 |
60 | if($csr === false) {
61 | throw new OpenSSLException('openssl_csr_new');
62 | }
63 |
64 | if(!openssl_csr_export($csr, $csr)) {
65 | throw new OpenSSLException('openssl_csr_export');
66 | }
67 |
68 | unlink($configFilePath);
69 |
70 | return $csr;
71 | }
72 | }
--------------------------------------------------------------------------------
/src/LE_ACME2/Utilities/ChallengeHTTP.php:
--------------------------------------------------------------------------------
1 | _subscriber[$event])) {
21 | $this->_subscriber[$event] = [];
22 | }
23 |
24 | $this->_subscriber[$event][] = $callable;
25 | }
26 |
27 | public function trigger(string $event, array $payload = null) : void {
28 |
29 | Logger::getInstance()->add(Logger::LEVEL_DEBUG, 'Event triggered: ' . $event);
30 |
31 | if(!isset($this->_subscriber[$event])) {
32 | return;
33 | }
34 |
35 | foreach($this->_subscriber[$event] as $callable) {
36 | $callable($event, $payload);
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/LE_ACME2/Utilities/KeyGenerator.php:
--------------------------------------------------------------------------------
1 | OPENSSL_KEYTYPE_RSA,
18 | "private_key_bits" => 4096,
19 | ]);
20 |
21 | if(!openssl_pkey_export($res, $privateKey))
22 | throw new \RuntimeException("RSA keypair export failed!");
23 |
24 | $details = openssl_pkey_get_details($res);
25 |
26 | file_put_contents($directory . $privateKeyFile, $privateKey);
27 | file_put_contents($directory . $publicKeyFile, $details['key']);
28 |
29 | if(PHP_MAJOR_VERSION < 8) {
30 | // deprecated after PHP 8.0.0 and not needed anymore
31 | openssl_pkey_free($res);
32 | }
33 | }
34 |
35 | /**
36 | * Generates a new EC prime256v1 keypair and saves both keys to a new file.
37 | *
38 | * @param string $directory The directory in which to store the new keys.
39 | * @param string $privateKeyFile The filename for the private key file.
40 | * @param string $publicKeyFile The filename for the public key file.
41 | */
42 | public static function EC(string $directory, string $privateKeyFile = 'private.pem', string $publicKeyFile = 'public.pem') {
43 |
44 | if (version_compare(PHP_VERSION, '7.1.0') == -1)
45 | throw new \RuntimeException("PHP 7.1+ required for EC keys");
46 |
47 | $res = openssl_pkey_new([
48 | "private_key_type" => OPENSSL_KEYTYPE_EC,
49 | "curve_name" => "prime256v1",
50 | ]);
51 |
52 | if(!openssl_pkey_export($res, $privateKey))
53 | throw new \RuntimeException("EC keypair export failed!");
54 |
55 | $details = openssl_pkey_get_details($res);
56 |
57 | file_put_contents($directory . $privateKeyFile, $privateKey);
58 | file_put_contents($directory . $publicKeyFile, $details['key']);
59 |
60 | if(PHP_MAJOR_VERSION < 8) {
61 | // deprecated after PHP 8.0.0 and not needed anymore
62 | openssl_pkey_free($res);
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/src/LE_ACME2/Utilities/Logger.php:
--------------------------------------------------------------------------------
1 | _desiredLevel = $desiredLevel;
21 | }
22 |
23 | private \Psr\Log\LoggerInterface|null $_psrLogger = null;
24 |
25 | public function setPSRLogger(\Psr\Log\LoggerInterface|null $psrLogger) : void {
26 | $this->_psrLogger = $psrLogger;
27 | }
28 |
29 | public function add(int $level, string $message, array $data = array()) : void {
30 |
31 | if($level > $this->_desiredLevel)
32 | return;
33 |
34 | if($this->_psrLogger) {
35 |
36 | if($level == self::LEVEL_INFO) {
37 | $this->_psrLogger->info($message, $data);
38 | return;
39 | }
40 | if($level == self::LEVEL_DEBUG) {
41 | $this->_psrLogger->debug($message, $data);
42 | return;
43 | }
44 | throw new \RuntimeException('Missing PSR Logger support for level: ' . $level);
45 | }
46 |
47 | $e = new \Exception();
48 | $trace = $e->getTrace();
49 | unset($trace[0]);
50 |
51 | $output = '' . date('d-m-Y H:i:s') . ': ' . $message . '
' . "\n";
52 |
53 | if($this->_desiredLevel == self::LEVEL_DEBUG) {
54 |
55 | $step = 0;
56 | foreach ($trace as $traceItem) {
57 |
58 | if(!isset($traceItem['class']) || !isset($traceItem['function'])) {
59 | continue;
60 | }
61 |
62 | $output .= 'Trace #' . $step . ': ' . $traceItem['class'] . '::' . $traceItem['function'] . '
' . "\n";
63 | $step++;
64 | }
65 |
66 | if ((is_array($data) && count($data) > 0) || !is_array($data))
67 | $output .= "\n" .'
Data:
' . "\n" . '' . var_export($data, true) . '
';
68 |
69 | $output .= '
' . "\n\n";
70 | }
71 |
72 | if(PHP_SAPI == 'cli') {
73 |
74 | $output = strip_tags($output);
75 | }
76 | echo $output;
77 | }
78 | }
--------------------------------------------------------------------------------
/src/LE_ACME2/Utilities/RequestSigner.php:
--------------------------------------------------------------------------------
1 | add(Logger::LEVEL_DEBUG, 'JWK sign request for ' . $url, ['payload' => $payload]);
20 |
21 | $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyDir . $privateKeyFile));
22 | $details = openssl_pkey_get_details($privateKey);
23 |
24 | $protected = [
25 | "alg" => "RS256",
26 | "jwk" => [
27 | "kty" => "RSA",
28 | "n" => Base64::UrlSafeEncode($details["rsa"]["n"]),
29 | "e" => Base64::UrlSafeEncode($details["rsa"]["e"]),
30 | ],
31 | "nonce" => $nonce,
32 | "url" => $url
33 | ];
34 |
35 | $payload64 = Base64::JSONUrlSafeEncode($payload);
36 | $protected64 = Base64::JSONUrlSafeEncode($protected);
37 |
38 | openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
39 | $signed64 = Base64::UrlSafeEncode($signed);
40 |
41 | $data = array(
42 | 'protected' => $protected64,
43 | 'payload' => $payload64,
44 | 'signature' => $signed64
45 | );
46 |
47 | return $data;
48 | }
49 |
50 | /**
51 | * Generates a JSON Web Key signature to attach to the request.
52 | *
53 | * @param array $payload The payload to add to the signature.
54 | * @param string $url The URL to use in the signature.
55 | * @param string $privateKeyDir The directory to get the private key from. Default to the account keys directory given in the constructor. (optional)
56 | * @param string $privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. (optional)
57 | *
58 | * @return string Returns a JSON encoded string containing the signature.
59 | */
60 | public static function JWKString(array $payload, string $url, string $nonce, string $privateKeyDir, string $privateKeyFile = 'private.pem') : string {
61 |
62 | $jwk = self::JWK($payload, $url, $nonce, $privateKeyDir, $privateKeyFile);
63 | return json_encode($jwk);
64 | }
65 |
66 | /**
67 | * Generates a Key ID signature to attach to the request.
68 | *
69 | * @param array|null $payload The payload to add to the signature.
70 | * @param string $kid The Key ID to use in the signature.
71 | * @param string $url The URL to use in the signature.
72 | * @param string $privateKeyDir The directory to get the private key from.
73 | * @param string $privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. (optional)
74 | *
75 | * @return string Returns a JSON encoded string containing the signature.
76 | */
77 | public static function KID(?array $payload, string $kid, string $url, string $nonce, string $privateKeyDir, string $privateKeyFile = 'private.pem') : string {
78 |
79 | Logger::getInstance()->add(Logger::LEVEL_DEBUG, 'KID sign request for ' . $url, ['payload' => $payload]);
80 |
81 | $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyDir . $privateKeyFile));
82 | // TODO: unused - $details = openssl_pkey_get_details($privateKey);
83 |
84 | $protected = [
85 | "alg" => "RS256",
86 | "kid" => $kid,
87 | "nonce" => $nonce,
88 | "url" => $url
89 | ];
90 |
91 | Logger::getInstance()->add(Logger::LEVEL_DEBUG, 'KID: ready to sign request for: ' . $url, ['protected' => $protected]);
92 |
93 | $payload64 = $payload === null ? Base64::UrlSafeEncode('') : Base64::JSONUrlSafeEncode($payload);
94 | $protected64 = Base64::JSONUrlSafeEncode($protected);
95 |
96 | openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
97 | $signed64 = Base64::UrlSafeEncode($signed);
98 |
99 | $data = [
100 | 'protected' => $protected64,
101 | 'payload' => $payload64,
102 | 'signature' => $signed64
103 | ];
104 |
105 | return json_encode($data);
106 | }
107 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/AbstractLeAcme2TestCase.php:
--------------------------------------------------------------------------------
1 | _accountEmail = 'le_acme2_php' . phpversion() . '_client@test.com';
16 |
17 | parent::__construct($name);
18 |
19 | $this->_orderSubjects[] = 'test.de';
20 |
21 | $this->_umlautsOrderSubjects[] = 'xn--test--kra0kxb.de'; // test-üäö.de
22 |
23 | LE_ACME2\Connector\Connector::getInstance()->useStagingServer(true);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/AccountTest.php:
--------------------------------------------------------------------------------
1 | _commonKeyDirectoryPath = TestHelper::getInstance()->getTempPath() . 'le-storage/';
17 | }
18 |
19 | public function testNonExistingCommonKeyDirectoryPath() {
20 |
21 | $this->assertTrue(\LE_ACME2\Account::getCommonKeyDirectoryPath() === null);
22 |
23 | $notExistingPath = TestHelper::getInstance()->getTempPath() . 'should-not-exist/';
24 |
25 | $this->catchExpectedException(
26 | \RuntimeException::class,
27 | function() use($notExistingPath) {
28 | \LE_ACME2\Account::setCommonKeyDirectoryPath($notExistingPath);
29 | }
30 | );
31 | }
32 |
33 | public function testCommonKeyDirectoryPath() {
34 |
35 | if(!file_exists($this->_commonKeyDirectoryPath)) {
36 | mkdir($this->_commonKeyDirectoryPath);
37 | }
38 |
39 | \LE_ACME2\Account::setCommonKeyDirectoryPath($this->_commonKeyDirectoryPath);
40 |
41 | $this->assertTrue(
42 | \LE_ACME2\Account::getCommonKeyDirectoryPath() === $this->_commonKeyDirectoryPath
43 | );
44 | }
45 |
46 | public function testNonExisting() {
47 |
48 | if(\LE_ACME2\Account::exists($this->_accountEmail)) {
49 | $this->markTestSkipped('Skipped: Account does already exist');
50 | }
51 |
52 | $this->assertTrue(!\LE_ACME2\Account::exists($this->_accountEmail));
53 |
54 | $this->catchExpectedException(
55 | \RuntimeException::class,
56 | function() {
57 | \LE_ACME2\Account::get($this->_accountEmail);
58 | }
59 | );
60 |
61 | }
62 |
63 | public function testCreate() {
64 |
65 | if(\LE_ACME2\Account::exists($this->_accountEmail)) {
66 | // Skipping account modification tests, when the account already exists
67 | // to reduce the LE api usage while developing
68 | TestHelper::getInstance()->setSkipAccountModificationTests(true);
69 | $this->markTestSkipped('Account modifications skipped: Account does already exist');
70 | }
71 |
72 | $this->assertTrue(!\LE_ACME2\Account::exists($this->_accountEmail));
73 |
74 | $account = \LE_ACME2\Account::create($this->_accountEmail);
75 | $this->assertTrue(is_object($account));
76 | $this->assertTrue($account->getEmail() === $this->_accountEmail);
77 |
78 | $account = \LE_ACME2\Account::get($this->_accountEmail);
79 | $this->assertTrue(is_object($account));
80 |
81 | $result = $account->getData();
82 | $this->assertTrue($result->getStatus() === \LE_ACME2\Response\Account\AbstractAccount::STATUS_VALID);
83 | }
84 |
85 | public function testInvalidCreate() {
86 |
87 | if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
88 | $this->expectNotToPerformAssertions();
89 | return;
90 | }
91 |
92 | $e = $this->catchExpectedException(
93 | InvalidResponse::class,
94 | function() {
95 | \LE_ACME2\Account::create('test_php' . phpversion() . '@example.org');
96 | }
97 | );
98 | $this->assertEquals(
99 | 'Invalid response received: ' .
100 | 'urn:ietf:params:acme:error:invalidContact' .
101 | ' - ' .
102 | 'Error creating new account :: contact email has forbidden domain "example.org"',
103 | $e->getMessage(),
104 | );
105 | }
106 |
107 | public function testModification() {
108 |
109 | if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
110 | $this->expectNotToPerformAssertions();
111 | return;
112 | }
113 |
114 | $account = \LE_ACME2\Account::get($this->_accountEmail);
115 | $this->assertTrue(is_object($account));
116 |
117 | $keyDirectoryPath = $account->getKeyDirectoryPath();
118 | $newEmail = 'new-' . $this->_accountEmail;
119 |
120 | // An email from example.org is not allowed
121 | $result = $account->update('test@example.org');
122 | $this->assertTrue($result === false);
123 |
124 | $result = $account->update($newEmail);
125 | $this->assertTrue($result === true);
126 |
127 | $this->assertTrue($account->getKeyDirectoryPath() !== $keyDirectoryPath);
128 | $this->assertTrue(file_exists($account->getKeyDirectoryPath()));
129 |
130 | $result = $account->update($this->_accountEmail);
131 | $this->assertTrue($result === true);
132 |
133 | $result = $account->changeKeys();
134 | $this->assertTrue($result === true);
135 |
136 | // 11. August 2022
137 | // Quickfix: The LE server will not recognize fast enough, that the account key has already changed
138 | sleep(5);
139 | }
140 |
141 | public function testDeactivation() {
142 |
143 | if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
144 | $this->expectNotToPerformAssertions();
145 | return;
146 | }
147 |
148 | $account = \LE_ACME2\Account::get($this->_accountEmail);
149 | $this->assertTrue(is_object($account));
150 |
151 | $result = $account->deactivate();
152 | $this->assertTrue($result === true);
153 |
154 | // 11. August 2022
155 | // Quickfix: The LE server will not recognize fast enough, that the account is already deactivated
156 | sleep(5);
157 |
158 | // The account is already deactivated
159 | $result = $account->deactivate();
160 | $this->assertTrue($result === false);
161 |
162 | // The account is already deactivated
163 | $result = $account->changeKeys();
164 | $this->assertTrue($result === false);
165 |
166 | // The account is already deactivated
167 | $this->catchExpectedException(
168 | \LE_ACME2\Exception\InvalidResponse::class,
169 | function() use($account) {
170 | $account->getData();
171 | }
172 | );
173 | }
174 |
175 | public function testCreationAfterDeactivation() {
176 |
177 | if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
178 | $this->expectNotToPerformAssertions();
179 | return;
180 | }
181 |
182 | $account = \LE_ACME2\Account::get($this->_accountEmail);
183 | $this->assertTrue(is_object($account));
184 |
185 | system('rm -R ' . $account->getKeyDirectoryPath());
186 | $this->assertTrue(!\LE_ACME2\Account::exists($this->_accountEmail));
187 |
188 | $account = \LE_ACME2\Account::create($this->_accountEmail);
189 | $this->assertTrue(is_object($account));
190 | }
191 |
192 | public function test() {
193 |
194 | $account = \LE_ACME2\Account::get($this->_accountEmail);
195 | $this->assertTrue(is_object($account));
196 | }
197 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Authorizer/HTTPTest.php:
--------------------------------------------------------------------------------
1 | _directoryPath = TestHelper::getInstance()->getTempPath() . 'acme-challenges/';
18 | }
19 |
20 | public function testNonExistingDirectoryPath() {
21 |
22 | $this->assertTrue(\LE_ACME2\Authorizer\HTTP::getDirectoryPath() === null);
23 |
24 | $this->catchExpectedException(
25 | \RuntimeException::class,
26 | function() {
27 | \LE_ACME2\Authorizer\HTTP::setDirectoryPath(TestHelper::getInstance()->getNonExistingPath());
28 | }
29 | );
30 | }
31 |
32 | public function testDirectoryPath() {
33 |
34 | if(!file_exists($this->_directoryPath)) {
35 | mkdir($this->_directoryPath);
36 | }
37 |
38 | \LE_ACME2\Authorizer\HTTP::setDirectoryPath($this->_directoryPath);
39 |
40 | $this->assertTrue(
41 | \LE_ACME2\Authorizer\HTTP::getDirectoryPath() === $this->_directoryPath
42 | );
43 | }
44 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Connector/RawResponse.php:
--------------------------------------------------------------------------------
1 | assertEquals(
15 | $exception,
16 | get_class($e),
17 | 'Exception message: ' . $e->getMessage(),
18 | );
19 | return $e;
20 | }
21 |
22 | throw new \RuntimeException('Expected exception not thrown: ' . $exception);
23 | }
24 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Exception/RateLimitReachedTest.php:
--------------------------------------------------------------------------------
1 | catchExpectedException(LE_ACME2\Exception\RateLimitReached::class, function() use($raw) {
29 | new LE_ACME2\Response\GetDirectory($raw);
30 | });
31 | $this->assertIsObject($exception);
32 | $this->assertTrue(get_class($exception) == LE_ACME2\Exception\RateLimitReached::class);
33 | }
34 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Exception/ServiceUnavailableTest.php:
--------------------------------------------------------------------------------
1 | catchExpectedException(LE_ACME2\Exception\ServiceUnavailable::class, function() use($raw) {
29 | new LE_ACME2\Response\GetDirectory($raw);
30 | });
31 | $this->assertIsObject($exception);
32 | $this->assertTrue(get_class($exception) == LE_ACME2\Exception\ServiceUnavailable::class);
33 | }
34 |
35 | /**
36 | * @covers LE_ACME2\Exception\ServiceUnavailable
37 | * @covers LE_ACME2\Response\AbstractResponse::_isServiceUnavailable
38 | * @covers LE_ACME2\Response\AbstractResponse::__construct
39 | *
40 | * @return void
41 | */
42 | public function testRetryAfterHeader() {
43 |
44 | $raw = RawResponse::createDummyFrom(
45 | 'HTTP/2 503 Too many requests' . "\r\n" .
46 | 'Retry-After: 120',
47 | '{
48 | "type": "urn:ietf:params:acme:error:rateLimited",
49 | "detail": "Service busy; retry later."
50 | }',
51 | );
52 |
53 | /** @var LE_ACME2\Exception\ServiceUnavailable $exception */
54 | $exception = $this->catchExpectedException(LE_ACME2\Exception\ServiceUnavailable::class, function() use($raw) {
55 | new LE_ACME2\Response\GetDirectory($raw);
56 | });
57 | $this->assertEquals('120', $exception->getRetryAfter());
58 | }
59 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/OrderTest.php:
--------------------------------------------------------------------------------
1 | _accountEmail);
12 |
13 | if(\LE_ACME2\Order::exists($account, $this->_orderSubjects)) {
14 | $this->markTestSkipped('Skipped: Order does already exist');
15 | }
16 |
17 | $this->assertFalse(\LE_ACME2\Order::exists($account, $this->_orderSubjects));
18 | $this->assertFalse(\LE_ACME2\Order::existsCertificateBundle($account, $this->_orderSubjects));
19 |
20 | $this->catchExpectedException(
21 | \RuntimeException::class,
22 | function() use($account) {
23 | \LE_ACME2\Order::get($account, $this->_orderSubjects);
24 | }
25 | );
26 | }
27 |
28 | public function testCreate() {
29 |
30 | $account = \LE_ACME2\Account::get($this->_accountEmail);
31 |
32 | if(\LE_ACME2\Order::exists($account, $this->_orderSubjects)) {
33 | // Skipping order modification tests, when the order already exists
34 | // to reduce the LE api usage while developing
35 | TestHelper::getInstance()->setSkipOrderModificationTests(true);
36 | $this->markTestSkipped('Order modifications skipped: Order does already exist');
37 | }
38 |
39 | $this->assertTrue(!\LE_ACME2\Order::exists($account, $this->_orderSubjects));
40 |
41 | $order = \LE_ACME2\Order::create($account, $this->_orderSubjects);
42 | $this->assertTrue(is_object($order));
43 | $this->assertTrue(count(array_diff($order->getSubjects(), $this->_orderSubjects)) == 0);
44 |
45 | $order = \LE_ACME2\Order::get($account, $this->_orderSubjects);
46 | $this->assertTrue(is_object($order));
47 |
48 | // TODO: Order replacement?
49 | //$result = $order->get();
50 | //$this->assertTrue($result->getStatus() === \LE_ACME2\Response\Account\AbstractAccount::STATUS_VALID);
51 | }
52 |
53 | public function testUmlautsCreate() {
54 |
55 | $account = \LE_ACME2\Account::get($this->_accountEmail);
56 |
57 | if(\LE_ACME2\Order::exists($account, $this->_umlautsOrderSubjects)) {
58 | // Skipping order modification tests, when the order already exists
59 | // to reduce the LE api usage while developing
60 | TestHelper::getInstance()->setSkipOrderModificationTests(true);
61 | $this->markTestSkipped('Order modifications skipped: Order does already exist');
62 | }
63 |
64 | $this->assertTrue(!\LE_ACME2\Order::exists($account, $this->_umlautsOrderSubjects));
65 |
66 | $order = \LE_ACME2\Order::create($account, $this->_umlautsOrderSubjects);
67 | $this->assertTrue(is_object($order));
68 | $this->assertTrue(count(array_diff($order->getSubjects(), $this->_umlautsOrderSubjects)) == 0);
69 |
70 | $order = \LE_ACME2\Order::get($account, $this->_umlautsOrderSubjects);
71 | $this->assertTrue(is_object($order));
72 |
73 | // TODO: Order replacement?
74 | //$result = $order->get();
75 | //$this->assertTrue($result->getStatus() === \LE_ACME2\Response\Account\AbstractAccount::STATUS_VALID);
76 | }
77 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Response/Authorization/GetTest.php:
--------------------------------------------------------------------------------
1 | getChallenge('http-01');
22 |
23 | $error = $challenge->error;
24 | $this->assertTrue(is_object($error) === true);
25 | $this->assertTrue($error->type === 'urn:ietf:params:acme:error:dns');
26 | $this->assertTrue($error->detail === "DNS problem: SERVFAIL looking up CAA for domain1.tld - the domain's nameservers may be malfunctioning");
27 | $this->assertTrue($error->status === 400);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Response/Order/GetTest.php:
--------------------------------------------------------------------------------
1 | catchExpectedException(
23 | LE_ACME2\Exception\OrderStatusInvalid::class,
24 | function() use($rawResponse) {
25 | new LE_ACME2\Response\Order\Get($rawResponse, 'http://dummy.org');
26 | }
27 | );
28 |
29 | try {
30 | new LE_ACME2\Response\Order\Get($rawResponse, 'http://dummy.org');
31 |
32 | throw new \RuntimeException('Exception not thrown');
33 |
34 | } catch (LE_ACME2\Exception\OrderStatusInvalid $e) {
35 | $this->assertNull($e->response->getError());
36 | }
37 | }
38 |
39 | /**
40 | * @covers \LE_ACME2\Response\Order\Struct\OrderError
41 | */
42 | public function testOrderInvalid() {
43 |
44 | $rawResponse = Connector\RawResponse::createDummyFrom(
45 | Connector\RawResponse::HEADER_200,
46 | file_get_contents(dirname(__FILE__, 2) . DIRECTORY_SEPARATOR . '_JSONSamples' . DIRECTORY_SEPARATOR . 'OrderStatusInvalidHavingError.json')
47 | );
48 |
49 | try {
50 | new LE_ACME2\Response\Order\Get($rawResponse, 'http://dummy.org');
51 |
52 | throw new \RuntimeException('Exception not thrown');
53 |
54 | } catch (LE_ACME2\Exception\OrderStatusInvalid $e) {
55 |
56 | $error = $e->response->getError();
57 |
58 | $this->assertNotNull($error);
59 |
60 | $this->assertTrue($e->response->getError()->hasStatusServerError());
61 |
62 | $this->assertEquals(500, $e->response->getError()->status);
63 | $this->assertEquals('urn:ietf:params:acme:error:serverInternal', $e->response->getError()->type);
64 | $this->assertEquals('Error finalizing order :: Unable to meet CA SCT embedding requirements', $e->response->getError()->detail);
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Response/_JSONSamples/ChallengeError.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": {
3 | "type": "dns",
4 | "value": "subdomain.domain1.tld"
5 | },
6 | "status": "invalid",
7 | "expires": "2021-07-24T08:01:41Z",
8 | "challenges": [
9 | {
10 | "type": "http-01",
11 | "status": "invalid",
12 | "error": {
13 | "type": "urn:ietf:params:acme:error:dns",
14 | "detail": "DNS problem: SERVFAIL looking up CAA for domain1.tld - the domain's nameservers may be malfunctioning",
15 | "status": 400
16 | },
17 | "url": "https://acme-v02.api.letsencrypt.org/acme/chall-v3/1234567/beH7Ng",
18 | "token": "RPET-XXXBUHuxDLjO_XXXnG6c34I0U",
19 | "validationRecord": [
20 | {
21 | "url": "http://subdomain.domain1.tld/.well-known/acme-challenge/RPET-XXXBUHuxDLjO_XXXnG6c34I0U",
22 | "hostname": "subdomain.domain1.tld",
23 | "port": "80",
24 | "addressesResolved": [
25 | "111.222.333.444"
26 | ],
27 | "addressUsed": "111.222.333.444"
28 | },
29 | {
30 | "url": "https://subdomain.domain1.tld/.well-known/acme-challenge/RPET-XXXBUHuxDLjO_XXXnG6c34I0U",
31 | "hostname": "subdomain.domain1.tld",
32 | "port": "443",
33 | "addressesResolved": [
34 | "111.222.333.444"
35 | ],
36 | "addressUsed": "111.222.333.444"
37 | }
38 | ],
39 | "validated": "2021-07-17T08:02:10Z"
40 | }
41 | ]
42 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Response/_JSONSamples/OrderStatusInvalid.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "invalid",
3 | "expires": "2021-07-24T08:01:41Z",
4 | "identifiers": [
5 | {
6 | "type": "dns",
7 | "value": "subdomain.domain1.tld"
8 | }
9 | ],
10 | "authorizations": [
11 | "https://acme-v02.api.letsencrypt.org/acme/authz-v3/1234567"
12 | ],
13 | "finalize": "https://acme-v02.api.letsencrypt.org/acme/finalize/1234567/1234567"
14 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Response/_JSONSamples/OrderStatusInvalidHavingError.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "invalid",
3 | "expires": "2021-07-24T08:01:41Z",
4 | "identifiers": [
5 | {
6 | "type": "dns",
7 | "value": "subdomain.domain1.tld"
8 | }
9 | ],
10 | "authorizations": [
11 | "https://acme-v02.api.letsencrypt.org/acme/authz-v3/1234567"
12 | ],
13 | "finalize": "https://acme-v02.api.letsencrypt.org/acme/finalize/1234567/1234567",
14 | "error": {
15 | "type": "urn:ietf:params:acme:error:serverInternal",
16 | "detail": "Error finalizing order :: Unable to meet CA SCT embedding requirements",
17 | "status": 500
18 | }
19 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/TestHelper.php:
--------------------------------------------------------------------------------
1 | _tempPath = $projectPath . 'temp/';
17 | if( !file_exists($this->_tempPath) ) {
18 | mkdir($this->_tempPath);
19 | }
20 |
21 | $this->_nonExistingPath = $this->getTempPath() . 'should-not-exist/';
22 | }
23 |
24 | public function getTempPath() : string {
25 | return $this->_tempPath;
26 | }
27 |
28 | public function getNonExistingPath() : string {
29 | return $this->_nonExistingPath;
30 | }
31 |
32 | private $_skipAccountModificationTests = false;
33 |
34 | public function setSkipAccountModificationTests(bool $value) : void {
35 | $this->_skipAccountModificationTests = $value;
36 | }
37 |
38 | public function shouldSkipAccountModificationTests() : bool {
39 | return $this->_skipAccountModificationTests;
40 | }
41 |
42 | private $_skipOrderModificationTests = false;
43 |
44 | public function setSkipOrderModificationTests(bool $value) : void {
45 | $this->_skipOrderModificationTests = $value;
46 | }
47 |
48 | public function shouldSkipOrderModificationTests() : bool {
49 | return $this->_skipOrderModificationTests;
50 | }
51 | }
--------------------------------------------------------------------------------
/src/LE_ACME2Tests/Utilities/CertificateTest.php:
--------------------------------------------------------------------------------
1 | _accountEmail);
17 |
18 | $this->_testOrderGenerateCSR($account);
19 | $this->_testOrderUmlautsGenerateCSR($account);
20 | }
21 |
22 | private function _testOrderGenerateCSR(\LE_ACME2\Account $account) {
23 |
24 | $order = \LE_ACME2\Order::get($account, $this->_orderSubjects);
25 |
26 | $csr = Utilities\Certificate::generateCSR($order);
27 | $this->assertTrue($csr !== null && is_string($csr));
28 | }
29 |
30 | private function _testOrderUmlautsGenerateCSR(\LE_ACME2\Account $account) {
31 |
32 | $order = \LE_ACME2\Order::get($account, $this->_umlautsOrderSubjects);
33 |
34 | $csr = Utilities\Certificate::generateCSR($order);
35 | $this->assertTrue($csr !== null && is_string($csr));
36 | }
37 | }
--------------------------------------------------------------------------------