1 | <?php |
||||||
2 | namespace Zwartpet\PHPCertificateToolbox; |
||||||
3 | |||||||
4 | use Zwartpet\PHPCertificateToolbox\Exception\RuntimeException; |
||||||
5 | use Psr\Log\LoggerInterface; |
||||||
6 | |||||||
7 | /** |
||||||
8 | * LetsEncrypt Account class, containing the functions and data associated with a LetsEncrypt account. |
||||||
9 | * |
||||||
10 | * @author Youri van Weegberg <[email protected]> |
||||||
11 | * @copyright 2018 Youri van Weegberg |
||||||
12 | * @license https://opensource.org/licenses/mit-license.php MIT License |
||||||
13 | */ |
||||||
14 | class LEAccount |
||||||
15 | { |
||||||
16 | private $connector; |
||||||
17 | |||||||
18 | public $id; |
||||||
19 | public $key; |
||||||
20 | public $contact; |
||||||
21 | public $agreement; |
||||||
22 | public $initialIp; |
||||||
23 | public $createdAt; |
||||||
24 | public $status; |
||||||
25 | |||||||
26 | /** @var LoggerInterface */ |
||||||
27 | private $log; |
||||||
28 | |||||||
29 | /** @var CertificateStorageInterface */ |
||||||
30 | private $storage; |
||||||
31 | |||||||
32 | /** |
||||||
33 | * Initiates the LetsEncrypt Account class. |
||||||
34 | * |
||||||
35 | * @param LEConnector $connector The LetsEncrypt Connector instance to use for HTTP requests. |
||||||
36 | * @param LoggerInterface $log PSR-3 compatible logger |
||||||
37 | * @param array $email The array of strings containing e-mail addresses. Only used when creating a |
||||||
38 | * new account. |
||||||
39 | * @param AccountStorageInterface $storage storage for account keys |
||||||
40 | */ |
||||||
41 | 12 | public function __construct($connector, LoggerInterface $log, $email, AccountStorageInterface $storage) |
|||||
42 | { |
||||||
43 | 12 | $this->connector = $connector; |
|||||
44 | 12 | $this->storage = $storage; |
|||||
0 ignored issues
–
show
|
|||||||
45 | 12 | $this->log = $log; |
|||||
46 | |||||||
47 | 12 | if (empty($storage->getAccountPublicKey()) || empty($storage->getAccountPrivateKey())) { |
|||||
48 | 12 | $this->log->notice("No account found for ".implode(',', $email).", attempting to create account"); |
|||||
49 | |||||||
50 | 12 | $accountKey = LEFunctions::RSAgenerateKeys(); |
|||||
51 | 12 | $storage->setAccountPublicKey($accountKey['public']); |
|||||
52 | 12 | $storage->setAccountPrivateKey($accountKey['private']); |
|||||
53 | |||||||
54 | 12 | $this->connector->accountURL = $this->createLEAccount($email); |
|||||
55 | } else { |
||||||
56 | 4 | $this->connector->accountURL = $this->getLEAccount(); |
|||||
57 | } |
||||||
58 | 12 | if ($this->connector->accountURL === false) { |
|||||
59 | 2 | throw new RuntimeException('Account not found or deactivated.'); |
|||||
60 | } |
||||||
61 | 12 | $this->getLEAccountData(); |
|||||
62 | 12 | } |
|||||
63 | |||||||
64 | /** |
||||||
65 | * Creates a new LetsEncrypt account. |
||||||
66 | * |
||||||
67 | * @param array $email The array of strings containing e-mail addresses. |
||||||
68 | * |
||||||
69 | * @return string|bool Returns the new account URL when the account was successfully created, false if not. |
||||||
70 | */ |
||||||
71 | private function createLEAccount($email) |
||||||
72 | { |
||||||
73 | 12 | $contact = array_map(function ($addr) { |
|||||
74 | 12 | return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr); |
|||||
75 | 12 | }, $email); |
|||||
76 | |||||||
77 | 12 | $sign = $this->connector->signRequestJWK( |
|||||
78 | 12 | ['contact' => $contact, 'termsOfServiceAgreed' => true], |
|||||
79 | 12 | $this->connector->newAccount |
|||||
80 | ); |
||||||
81 | 12 | $post = $this->connector->post($this->connector->newAccount, $sign); |
|||||
82 | 12 | if (strpos($post['header'], "201 Created") !== false) { |
|||||
83 | 12 | if (preg_match('~Location: (\S+)~i', $post['header'], $matches)) { |
|||||
84 | 12 | return trim($matches[1]); |
|||||
85 | } |
||||||
86 | } |
||||||
87 | //@codeCoverageIgnoreStart |
||||||
88 | return false; |
||||||
89 | //@codeCoverageIgnoreEnd |
||||||
90 | } |
||||||
91 | |||||||
92 | /** |
||||||
93 | * Gets the LetsEncrypt account URL associated with the stored account keys. |
||||||
94 | * |
||||||
95 | * @return string|bool Returns the account URL if it is found, or false when none is found. |
||||||
96 | */ |
||||||
97 | 4 | private function getLEAccount() |
|||||
98 | { |
||||||
99 | 4 | $sign = $this->connector->signRequestJWK(['onlyReturnExisting' => true], $this->connector->newAccount); |
|||||
100 | 4 | $post = $this->connector->post($this->connector->newAccount, $sign); |
|||||
101 | |||||||
102 | 4 | if (strpos($post['header'], "200 OK") !== false) { |
|||||
103 | 2 | if (preg_match('~Location: (\S+)~i', $post['header'], $matches)) { |
|||||
104 | 2 | return trim($matches[1]); |
|||||
105 | } |
||||||
106 | } |
||||||
107 | 2 | return false; |
|||||
108 | } |
||||||
109 | |||||||
110 | /** |
||||||
111 | * Gets the LetsEncrypt account data from the account URL. |
||||||
112 | */ |
||||||
113 | 12 | private function getLEAccountData() |
|||||
114 | { |
||||||
115 | 12 | $sign = $this->connector->signRequestKid( |
|||||
116 | 12 | ['' => ''], |
|||||
117 | 12 | $this->connector->accountURL, |
|||||
118 | 12 | $this->connector->accountURL |
|||||
119 | ); |
||||||
120 | 12 | $post = $this->connector->post($this->connector->accountURL, $sign); |
|||||
121 | 12 | if (strpos($post['header'], "200 OK") !== false) { |
|||||
122 | 12 | $this->id = isset($post['body']['id']) ? $post['body']['id'] : ''; |
|||||
123 | 12 | $this->key = $post['body']['key']; |
|||||
124 | 12 | $this->contact = $post['body']['contact']; |
|||||
125 | 12 | $this->agreement = isset($post['body']['agreement']) ? $post['body']['agreement'] : null; |
|||||
126 | 12 | $this->initialIp = $post['body']['initialIp']; |
|||||
127 | 12 | $this->createdAt = $post['body']['createdAt']; |
|||||
128 | 12 | $this->status = $post['body']['status']; |
|||||
129 | } else { |
||||||
130 | //@codeCoverageIgnoreStart |
||||||
131 | throw new RuntimeException('Account data cannot be found.'); |
||||||
132 | //@codeCoverageIgnoreEnd |
||||||
133 | } |
||||||
134 | 12 | } |
|||||
135 | |||||||
136 | /** |
||||||
137 | * Updates account data. Now just supporting new contact information. |
||||||
138 | * |
||||||
139 | * @param array $email The array of strings containing e-mail adresses. |
||||||
140 | * |
||||||
141 | * @return boolean Returns true if the update is successful, false if not. |
||||||
142 | */ |
||||||
143 | public function updateAccount($email) |
||||||
144 | { |
||||||
145 | 2 | $contact = array_map(function ($addr) { |
|||||
146 | 2 | return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr); |
|||||
147 | 2 | }, $email); |
|||||
148 | |||||||
149 | 2 | $sign = $this->connector->signRequestKid( |
|||||
150 | 2 | ['contact' => $contact], |
|||||
151 | 2 | $this->connector->accountURL, |
|||||
152 | 2 | $this->connector->accountURL |
|||||
153 | ); |
||||||
154 | 2 | $post = $this->connector->post($this->connector->accountURL, $sign); |
|||||
155 | 2 | if ($post['status'] !== 200) { |
|||||
156 | //@codeCoverageIgnoreStart |
||||||
157 | throw new RuntimeException('Unable to update account'); |
||||||
158 | //@codeCoverageIgnoreEnd |
||||||
159 | } |
||||||
160 | |||||||
161 | 2 | $this->id = isset($post['body']['id']) ? $post['body']['id'] : ''; |
|||||
162 | 2 | $this->key = $post['body']['key']; |
|||||
163 | 2 | $this->contact = $post['body']['contact']; |
|||||
164 | 2 | $this->agreement = isset($post['body']['agreement']) ? $post['body']['agreement'] : ''; |
|||||
165 | 2 | $this->initialIp = $post['body']['initialIp']; |
|||||
166 | 2 | $this->createdAt = $post['body']['createdAt']; |
|||||
167 | 2 | $this->status = $post['body']['status']; |
|||||
168 | |||||||
169 | 2 | $this->log->notice('Account data updated'); |
|||||
170 | 2 | return true; |
|||||
171 | } |
||||||
172 | |||||||
173 | /** |
||||||
174 | * Creates new RSA account keys and updates the keys with LetsEncrypt. |
||||||
175 | * |
||||||
176 | * @return boolean Returns true if the update is successful, false if not. |
||||||
177 | */ |
||||||
178 | 2 | public function changeAccountKeys() |
|||||
179 | { |
||||||
180 | 2 | $new=LEFunctions::RSAgenerateKeys(); |
|||||
181 | |||||||
182 | 2 | $privateKey = openssl_pkey_get_private($new['private']); |
|||||
183 | 2 | if ($privateKey === false) { |
|||||
184 | //@codeCoverageIgnoreStart |
||||||
185 | throw new RuntimeException('Failed to open newly generated private key'); |
||||||
186 | //@codeCoverageIgnoreEnd |
||||||
187 | } |
||||||
188 | |||||||
189 | |||||||
190 | 2 | $details = openssl_pkey_get_details($privateKey); |
|||||
191 | 2 | $innerPayload = ['account' => $this->connector->accountURL, 'newKey' => [ |
|||||
192 | 2 | "kty" => "RSA", |
|||||
193 | 2 | "n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"]), |
|||||
194 | 2 | "e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]) |
|||||
195 | ]]; |
||||||
196 | 2 | $outerPayload = $this->connector->signRequestJWK( |
|||||
197 | 2 | $innerPayload, |
|||||
198 | 2 | $this->connector->keyChange, |
|||||
199 | 2 | $new['private'] |
|||||
200 | ); |
||||||
201 | 2 | $sign = $this->connector->signRequestKid( |
|||||
202 | 2 | $outerPayload, |
|||||
203 | 2 | $this->connector->accountURL, |
|||||
204 | 2 | $this->connector->keyChange |
|||||
205 | ); |
||||||
206 | 2 | $post = $this->connector->post($this->connector->keyChange, $sign); |
|||||
207 | 2 | if ($post['status'] !== 200) { |
|||||
208 | //@codeCoverageIgnoreStart |
||||||
209 | throw new RuntimeException('Unable to post new account keys'); |
||||||
210 | //@codeCoverageIgnoreEnd |
||||||
211 | } |
||||||
212 | |||||||
213 | 2 | $this->getLEAccountData(); |
|||||
214 | |||||||
215 | 2 | $this->storage->setAccountPublicKey($new['public']); |
|||||
0 ignored issues
–
show
The method
setAccountPublicKey() does not exist on Zwartpet\PHPCertificateT...ificateStorageInterface .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
216 | 2 | $this->storage->setAccountPrivateKey($new['private']); |
|||||
0 ignored issues
–
show
The method
setAccountPrivateKey() does not exist on Zwartpet\PHPCertificateT...ificateStorageInterface .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
217 | |||||||
218 | 2 | $this->log->notice('Account keys changed'); |
|||||
219 | 2 | return true; |
|||||
220 | } |
||||||
221 | |||||||
222 | /** |
||||||
223 | * Deactivates the LetsEncrypt account. |
||||||
224 | * |
||||||
225 | * @return boolean Returns true if the deactivation is successful, false if not. |
||||||
226 | */ |
||||||
227 | 2 | public function deactivateAccount() |
|||||
228 | { |
||||||
229 | 2 | $sign = $this->connector->signRequestKid( |
|||||
230 | 2 | ['status' => 'deactivated'], |
|||||
231 | 2 | $this->connector->accountURL, |
|||||
232 | 2 | $this->connector->accountURL |
|||||
233 | ); |
||||||
234 | 2 | $post = $this->connector->post($this->connector->accountURL, $sign); |
|||||
235 | 2 | if ($post['status'] !== 200) { |
|||||
236 | //@codeCoverageIgnoreStart |
||||||
237 | $this->log->error('Account deactivation failed'); |
||||||
238 | return false; |
||||||
239 | //@codeCoverageIgnoreEnd |
||||||
240 | } |
||||||
241 | |||||||
242 | 2 | $this->connector->accountDeactivated = true; |
|||||
243 | 2 | $this->log->info('Account deactivated'); |
|||||
244 | 2 | return true; |
|||||
245 | } |
||||||
246 | } |
||||||
247 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..