Completed
Pull Request — master (#11)
by John
02:24
created
src/LEConnector.php 1 patch
Indentation   +331 added lines, -331 removed lines patch added patch discarded remove patch
@@ -21,335 +21,335 @@
 block discarded – undo
21 21
  */
22 22
 class LEConnector
23 23
 {
24
-    public $baseURL;
25
-
26
-    private $nonce;
27
-
28
-    public $keyChange;
29
-    public $newAccount;
30
-    public $newNonce;
31
-    public $newOrder;
32
-    public $revokeCert;
33
-
34
-    public $accountURL;
35
-    public $accountDeactivated = false;
36
-
37
-    /** @var LoggerInterface */
38
-    private $log;
39
-
40
-    /** @var ClientInterface */
41
-    private $httpClient;
42
-
43
-    /** @var AccountStorageInterface */
44
-    private $storage;
45
-
46
-    /**
47
-     * Initiates the LetsEncrypt Connector class.
48
-     *
49
-     * @param LoggerInterface $log
50
-     * @param ClientInterface $httpClient
51
-     * @param string $baseURL The LetsEncrypt server URL to make requests to.
52
-     * @param AccountStorageInterface $storage
53
-     */
54
-    public function __construct(
55
-        LoggerInterface $log,
56
-        ClientInterface $httpClient,
57
-        $baseURL,
58
-        AccountStorageInterface $storage
59
-    ) {
60
-
61
-        $this->baseURL = $baseURL;
62
-        $this->storage = $storage;
63
-        $this->log = $log;
64
-        $this->httpClient = $httpClient;
65
-
66
-        $this->getLEDirectory();
67
-        $this->getNewNonce();
68
-    }
69
-
70
-    /**
71
-     * Requests the LetsEncrypt Directory and stores the necessary URLs in this LetsEncrypt Connector instance.
72
-     */
73
-    private function getLEDirectory()
74
-    {
75
-        $req = $this->get('/directory');
76
-        $this->keyChange = $req['body']['keyChange'];
77
-        $this->newAccount = $req['body']['newAccount'];
78
-        $this->newNonce = $req['body']['newNonce'];
79
-        $this->newOrder = $req['body']['newOrder'];
80
-        $this->revokeCert = $req['body']['revokeCert'];
81
-    }
82
-
83
-    /**
84
-     * Requests a new nonce from the LetsEncrypt server and stores it in this LetsEncrypt Connector instance.
85
-     */
86
-    private function getNewNonce()
87
-    {
88
-        $result = $this->head($this->newNonce);
89
-
90
-        if ($result['status'] !== 200) {
91
-            //@codeCoverageIgnoreStart
92
-            throw new RuntimeException("No new nonce - fetched {$this->newNonce} got " . $result['header']);
93
-            //@codeCoverageIgnoreEnd
94
-        }
95
-    }
96
-
97
-    /**
98
-     * Makes a request to the HTTP challenge URL and checks whether the authorization is valid for the given $domain.
99
-     *
100
-     * @param string $domain The domain to check the authorization for.
101
-     * @param string $token The token (filename) to request.
102
-     * @param string $keyAuthorization the keyAuthorization (file content) to compare.
103
-     *
104
-     * @return boolean  Returns true if the challenge is valid, false if not.
105
-     */
106
-    public function checkHTTPChallenge($domain, $token, $keyAuthorization)
107
-    {
108
-        $requestURL = 'http://' . $domain . '/.well-known/acme-challenge/' . $token;
109
-
110
-        $request = new Request('GET', $requestURL);
111
-
112
-        try {
113
-            $response = $this->httpClient->send($request);
114
-        } catch (\Exception $e) {
115
-            $this->log->warning(
116
-                "HTTP check on $requestURL failed ({msg})",
117
-                ['msg' => $e->getMessage()]
118
-            );
119
-            return false;
120
-        }
121
-
122
-        $content = $response->getBody()->getContents();
123
-        return $content == $keyAuthorization;
124
-    }
125
-
126
-    /**
127
-     * Makes a Curl request.
128
-     *
129
-     * @param string $method The HTTP method to use. Accepting GET, POST and HEAD requests.
130
-     * @param string $URL The URL or partial URL to make the request to.
131
-     *                       If it is partial, the baseURL will be prepended.
132
-     * @param string $data The body to attach to a POST request. Expected as a JSON encoded string.
133
-     *
134
-     * @return array Returns an array with the keys 'request', 'header' and 'body'.
135
-     */
136
-    private function request($method, $URL, $data = null)
137
-    {
138
-        if ($this->accountDeactivated) {
139
-            throw new LogicException('The account was deactivated. No further requests can be made.');
140
-        }
141
-
142
-        $requestURL = preg_match('~^http~', $URL) ? $URL : $this->baseURL . $URL;
143
-
144
-        $hdrs = ['Accept' => 'application/json'];
145
-        if (!empty($data)) {
146
-            $hdrs['Content-Type'] = 'application/jose+json';
147
-        }
148
-
149
-        $request = new Request($method, $requestURL, $hdrs, $data);
150
-
151
-        try {
152
-            $response = $this->httpClient->send($request);
153
-        } catch (BadResponseException $e) {
154
-            //4xx/5xx failures are not expected and we throw exceptions for them
155
-            $msg = "$method $URL failed";
156
-            if ($e->hasResponse()) {
157
-                $body = (string)$e->getResponse()->getBody();
158
-                $json = json_decode($body, true);
159
-                if (!empty($json) && isset($json['detail'])) {
160
-                    $msg .= " ({$json['detail']})";
161
-                }
162
-            }
163
-            throw new RuntimeException($msg, 0, $e);
164
-        } catch (GuzzleException $e) {
165
-            //@codeCoverageIgnoreStart
166
-            throw new RuntimeException("$method $URL failed", 0, $e);
167
-            //@codeCoverageIgnoreEnd
168
-        }
169
-
170
-        //uncomment this to generate a test simulation of this request
171
-        //TestResponseGenerator::dumpTestSimulation($method, $requestURL, $response);
172
-
173
-        $this->maintainNonce($method, $response);
174
-
175
-        return $this->formatResponse($method, $requestURL, $response);
176
-    }
177
-
178
-    private function formatResponse($method, $requestURL, ResponseInterface $response)
179
-    {
180
-        $body = $response->getBody();
181
-
182
-        $header = $response->getStatusCode() . ' ' . $response->getReasonPhrase() . "\n";
183
-        $allHeaders = $response->getHeaders();
184
-        foreach ($allHeaders as $name => $values) {
185
-            foreach ($values as $value) {
186
-                $header .= "$name: $value\n";
187
-            }
188
-        }
189
-
190
-        $decoded = $body;
191
-        if ($response->getHeaderLine('Content-Type') === 'application/json') {
192
-            $decoded = json_decode($body, true);
193
-            if (!$decoded) {
194
-                //@codeCoverageIgnoreStart
195
-                throw new RuntimeException('Bad JSON received ' . $body);
196
-                //@codeCoverageIgnoreEnd
197
-            }
198
-        }
199
-
200
-        $jsonresponse = [
201
-            'request' => $method . ' ' . $requestURL,
202
-            'header' => $header,
203
-            'body' => $decoded,
204
-            'raw' => $body,
205
-            'status' => $response->getStatusCode()
206
-        ];
207
-
208
-        //$this->log->debug('{request} got {status} header = {header} body = {raw}', $jsonresponse);
209
-
210
-        return $jsonresponse;
211
-    }
212
-
213
-    private function maintainNonce($requestMethod, ResponseInterface $response)
214
-    {
215
-        if ($response->hasHeader('Replay-Nonce')) {
216
-            $this->nonce = $response->getHeader('Replay-Nonce')[0];
217
-            $this->log->debug("got new nonce " . $this->nonce);
218
-        } elseif ($requestMethod == 'POST') {
219
-            $this->getNewNonce(); // Not expecting a new nonce with GET and HEAD requests.
220
-        }
221
-    }
222
-
223
-    /**
224
-     * Makes a GET request.
225
-     *
226
-     * @param string $url The URL or partial URL to make the request to.
227
-     *                    If it is partial, the baseURL will be prepended.
228
-     *
229
-     * @return array Returns an array with the keys 'request', 'header' and 'body'.
230
-     */
231
-    public function get($url)
232
-    {
233
-        return $this->request('GET', $url);
234
-    }
235
-
236
-    /**
237
-     * Makes a POST request.
238
-     *
239
-     * @param string $url The URL or partial URL for the request to. If it is partial, the baseURL will be prepended.
240
-     * @param string $data The body to attach to a POST request. Expected as a json string.
241
-     *
242
-     * @return array Returns an array with the keys 'request', 'header' and 'body'.
243
-     */
244
-    public function post($url, $data = null)
245
-    {
246
-        return $this->request('POST', $url, $data);
247
-    }
248
-
249
-    /**
250
-     * Makes a HEAD request.
251
-     *
252
-     * @param string $url The URL or partial URL to make the request to.
253
-     *                    If it is partial, the baseURL will be prepended.
254
-     *
255
-     * @return array Returns an array with the keys 'request', 'header' and 'body'.
256
-     */
257
-    public function head($url)
258
-    {
259
-        return $this->request('HEAD', $url);
260
-    }
261
-
262
-    /**
263
-     * Generates a JSON Web Key signature to attach to the request.
264
-     *
265
-     * @param array|string $payload The payload to add to the signature.
266
-     * @param string $url The URL to use in the signature.
267
-     * @param string $privateKey The private key to sign the request with.
268
-     *
269
-     * @return string   Returns a JSON encoded string containing the signature.
270
-     */
271
-    public function signRequestJWK($payload, $url, $privateKey = '')
272
-    {
273
-        if ($privateKey == '') {
274
-            $privateKey = $this->storage->getAccountPrivateKey();
275
-        }
276
-        $privateKey = openssl_pkey_get_private($privateKey);
277
-        if ($privateKey === false) {
278
-            //@codeCoverageIgnoreStart
279
-            throw new RuntimeException('LEConnector::signRequestJWK failed to get private key');
280
-            //@codeCoverageIgnoreEnd
281
-        }
282
-
283
-        $details = openssl_pkey_get_details($privateKey);
284
-
285
-        $protected = [
286
-            "alg" => "RS256",
287
-            "jwk" => [
288
-                "kty" => "RSA",
289
-                "n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"]),
290
-                "e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]),
291
-            ],
292
-            "nonce" => $this->nonce,
293
-            "url" => $url
294
-        ];
295
-
296
-        $payload64 = LEFunctions::base64UrlSafeEncode(
297
-            str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload)
298
-        );
299
-        $protected64 = LEFunctions::base64UrlSafeEncode(json_encode($protected));
300
-
301
-        openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, OPENSSL_ALGO_SHA256);
302
-        $signed64 = LEFunctions::base64UrlSafeEncode($signed);
303
-
304
-        $data = [
305
-            'protected' => $protected64,
306
-            'payload' => $payload64,
307
-            'signature' => $signed64
308
-        ];
309
-
310
-        return json_encode($data);
311
-    }
312
-
313
-    /**
314
-     * Generates a Key ID signature to attach to the request.
315
-     *
316
-     * @param array|string $payload The payload to add to the signature.
317
-     * @param string $kid The Key ID to use in the signature.
318
-     * @param string $url The URL to use in the signature.
319
-     * @param string $privateKey The private key to sign the request with. Defaults to account key
320
-     *
321
-     * @return string   Returns a JSON encoded string containing the signature.
322
-     */
323
-    public function signRequestKid($payload, $kid, $url, $privateKey = '')
324
-    {
325
-        if ($privateKey == '') {
326
-            $privateKey = $this->storage->getAccountPrivateKey();
327
-        }
328
-        $privateKey = openssl_pkey_get_private($privateKey);
329
-
330
-        //$details = openssl_pkey_get_details($privateKey);
331
-
332
-        $protected = [
333
-            "alg" => "RS256",
334
-            "kid" => $kid,
335
-            "nonce" => $this->nonce,
336
-            "url" => $url
337
-        ];
338
-
339
-        $payload64 = LEFunctions::base64UrlSafeEncode(
340
-            str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload)
341
-        );
342
-        $protected64 = LEFunctions::base64UrlSafeEncode(json_encode($protected));
343
-
344
-        openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, OPENSSL_ALGO_SHA256);
345
-        $signed64 = LEFunctions::base64UrlSafeEncode($signed);
346
-
347
-        $data = [
348
-            'protected' => $protected64,
349
-            'payload' => $payload64,
350
-            'signature' => $signed64
351
-        ];
352
-
353
-        return json_encode($data);
354
-    }
24
+	public $baseURL;
25
+
26
+	private $nonce;
27
+
28
+	public $keyChange;
29
+	public $newAccount;
30
+	public $newNonce;
31
+	public $newOrder;
32
+	public $revokeCert;
33
+
34
+	public $accountURL;
35
+	public $accountDeactivated = false;
36
+
37
+	/** @var LoggerInterface */
38
+	private $log;
39
+
40
+	/** @var ClientInterface */
41
+	private $httpClient;
42
+
43
+	/** @var AccountStorageInterface */
44
+	private $storage;
45
+
46
+	/**
47
+	 * Initiates the LetsEncrypt Connector class.
48
+	 *
49
+	 * @param LoggerInterface $log
50
+	 * @param ClientInterface $httpClient
51
+	 * @param string $baseURL The LetsEncrypt server URL to make requests to.
52
+	 * @param AccountStorageInterface $storage
53
+	 */
54
+	public function __construct(
55
+		LoggerInterface $log,
56
+		ClientInterface $httpClient,
57
+		$baseURL,
58
+		AccountStorageInterface $storage
59
+	) {
60
+
61
+		$this->baseURL = $baseURL;
62
+		$this->storage = $storage;
63
+		$this->log = $log;
64
+		$this->httpClient = $httpClient;
65
+
66
+		$this->getLEDirectory();
67
+		$this->getNewNonce();
68
+	}
69
+
70
+	/**
71
+	 * Requests the LetsEncrypt Directory and stores the necessary URLs in this LetsEncrypt Connector instance.
72
+	 */
73
+	private function getLEDirectory()
74
+	{
75
+		$req = $this->get('/directory');
76
+		$this->keyChange = $req['body']['keyChange'];
77
+		$this->newAccount = $req['body']['newAccount'];
78
+		$this->newNonce = $req['body']['newNonce'];
79
+		$this->newOrder = $req['body']['newOrder'];
80
+		$this->revokeCert = $req['body']['revokeCert'];
81
+	}
82
+
83
+	/**
84
+	 * Requests a new nonce from the LetsEncrypt server and stores it in this LetsEncrypt Connector instance.
85
+	 */
86
+	private function getNewNonce()
87
+	{
88
+		$result = $this->head($this->newNonce);
89
+
90
+		if ($result['status'] !== 200) {
91
+			//@codeCoverageIgnoreStart
92
+			throw new RuntimeException("No new nonce - fetched {$this->newNonce} got " . $result['header']);
93
+			//@codeCoverageIgnoreEnd
94
+		}
95
+	}
96
+
97
+	/**
98
+	 * Makes a request to the HTTP challenge URL and checks whether the authorization is valid for the given $domain.
99
+	 *
100
+	 * @param string $domain The domain to check the authorization for.
101
+	 * @param string $token The token (filename) to request.
102
+	 * @param string $keyAuthorization the keyAuthorization (file content) to compare.
103
+	 *
104
+	 * @return boolean  Returns true if the challenge is valid, false if not.
105
+	 */
106
+	public function checkHTTPChallenge($domain, $token, $keyAuthorization)
107
+	{
108
+		$requestURL = 'http://' . $domain . '/.well-known/acme-challenge/' . $token;
109
+
110
+		$request = new Request('GET', $requestURL);
111
+
112
+		try {
113
+			$response = $this->httpClient->send($request);
114
+		} catch (\Exception $e) {
115
+			$this->log->warning(
116
+				"HTTP check on $requestURL failed ({msg})",
117
+				['msg' => $e->getMessage()]
118
+			);
119
+			return false;
120
+		}
121
+
122
+		$content = $response->getBody()->getContents();
123
+		return $content == $keyAuthorization;
124
+	}
125
+
126
+	/**
127
+	 * Makes a Curl request.
128
+	 *
129
+	 * @param string $method The HTTP method to use. Accepting GET, POST and HEAD requests.
130
+	 * @param string $URL The URL or partial URL to make the request to.
131
+	 *                       If it is partial, the baseURL will be prepended.
132
+	 * @param string $data The body to attach to a POST request. Expected as a JSON encoded string.
133
+	 *
134
+	 * @return array Returns an array with the keys 'request', 'header' and 'body'.
135
+	 */
136
+	private function request($method, $URL, $data = null)
137
+	{
138
+		if ($this->accountDeactivated) {
139
+			throw new LogicException('The account was deactivated. No further requests can be made.');
140
+		}
141
+
142
+		$requestURL = preg_match('~^http~', $URL) ? $URL : $this->baseURL . $URL;
143
+
144
+		$hdrs = ['Accept' => 'application/json'];
145
+		if (!empty($data)) {
146
+			$hdrs['Content-Type'] = 'application/jose+json';
147
+		}
148
+
149
+		$request = new Request($method, $requestURL, $hdrs, $data);
150
+
151
+		try {
152
+			$response = $this->httpClient->send($request);
153
+		} catch (BadResponseException $e) {
154
+			//4xx/5xx failures are not expected and we throw exceptions for them
155
+			$msg = "$method $URL failed";
156
+			if ($e->hasResponse()) {
157
+				$body = (string)$e->getResponse()->getBody();
158
+				$json = json_decode($body, true);
159
+				if (!empty($json) && isset($json['detail'])) {
160
+					$msg .= " ({$json['detail']})";
161
+				}
162
+			}
163
+			throw new RuntimeException($msg, 0, $e);
164
+		} catch (GuzzleException $e) {
165
+			//@codeCoverageIgnoreStart
166
+			throw new RuntimeException("$method $URL failed", 0, $e);
167
+			//@codeCoverageIgnoreEnd
168
+		}
169
+
170
+		//uncomment this to generate a test simulation of this request
171
+		//TestResponseGenerator::dumpTestSimulation($method, $requestURL, $response);
172
+
173
+		$this->maintainNonce($method, $response);
174
+
175
+		return $this->formatResponse($method, $requestURL, $response);
176
+	}
177
+
178
+	private function formatResponse($method, $requestURL, ResponseInterface $response)
179
+	{
180
+		$body = $response->getBody();
181
+
182
+		$header = $response->getStatusCode() . ' ' . $response->getReasonPhrase() . "\n";
183
+		$allHeaders = $response->getHeaders();
184
+		foreach ($allHeaders as $name => $values) {
185
+			foreach ($values as $value) {
186
+				$header .= "$name: $value\n";
187
+			}
188
+		}
189
+
190
+		$decoded = $body;
191
+		if ($response->getHeaderLine('Content-Type') === 'application/json') {
192
+			$decoded = json_decode($body, true);
193
+			if (!$decoded) {
194
+				//@codeCoverageIgnoreStart
195
+				throw new RuntimeException('Bad JSON received ' . $body);
196
+				//@codeCoverageIgnoreEnd
197
+			}
198
+		}
199
+
200
+		$jsonresponse = [
201
+			'request' => $method . ' ' . $requestURL,
202
+			'header' => $header,
203
+			'body' => $decoded,
204
+			'raw' => $body,
205
+			'status' => $response->getStatusCode()
206
+		];
207
+
208
+		//$this->log->debug('{request} got {status} header = {header} body = {raw}', $jsonresponse);
209
+
210
+		return $jsonresponse;
211
+	}
212
+
213
+	private function maintainNonce($requestMethod, ResponseInterface $response)
214
+	{
215
+		if ($response->hasHeader('Replay-Nonce')) {
216
+			$this->nonce = $response->getHeader('Replay-Nonce')[0];
217
+			$this->log->debug("got new nonce " . $this->nonce);
218
+		} elseif ($requestMethod == 'POST') {
219
+			$this->getNewNonce(); // Not expecting a new nonce with GET and HEAD requests.
220
+		}
221
+	}
222
+
223
+	/**
224
+	 * Makes a GET request.
225
+	 *
226
+	 * @param string $url The URL or partial URL to make the request to.
227
+	 *                    If it is partial, the baseURL will be prepended.
228
+	 *
229
+	 * @return array Returns an array with the keys 'request', 'header' and 'body'.
230
+	 */
231
+	public function get($url)
232
+	{
233
+		return $this->request('GET', $url);
234
+	}
235
+
236
+	/**
237
+	 * Makes a POST request.
238
+	 *
239
+	 * @param string $url The URL or partial URL for the request to. If it is partial, the baseURL will be prepended.
240
+	 * @param string $data The body to attach to a POST request. Expected as a json string.
241
+	 *
242
+	 * @return array Returns an array with the keys 'request', 'header' and 'body'.
243
+	 */
244
+	public function post($url, $data = null)
245
+	{
246
+		return $this->request('POST', $url, $data);
247
+	}
248
+
249
+	/**
250
+	 * Makes a HEAD request.
251
+	 *
252
+	 * @param string $url The URL or partial URL to make the request to.
253
+	 *                    If it is partial, the baseURL will be prepended.
254
+	 *
255
+	 * @return array Returns an array with the keys 'request', 'header' and 'body'.
256
+	 */
257
+	public function head($url)
258
+	{
259
+		return $this->request('HEAD', $url);
260
+	}
261
+
262
+	/**
263
+	 * Generates a JSON Web Key signature to attach to the request.
264
+	 *
265
+	 * @param array|string $payload The payload to add to the signature.
266
+	 * @param string $url The URL to use in the signature.
267
+	 * @param string $privateKey The private key to sign the request with.
268
+	 *
269
+	 * @return string   Returns a JSON encoded string containing the signature.
270
+	 */
271
+	public function signRequestJWK($payload, $url, $privateKey = '')
272
+	{
273
+		if ($privateKey == '') {
274
+			$privateKey = $this->storage->getAccountPrivateKey();
275
+		}
276
+		$privateKey = openssl_pkey_get_private($privateKey);
277
+		if ($privateKey === false) {
278
+			//@codeCoverageIgnoreStart
279
+			throw new RuntimeException('LEConnector::signRequestJWK failed to get private key');
280
+			//@codeCoverageIgnoreEnd
281
+		}
282
+
283
+		$details = openssl_pkey_get_details($privateKey);
284
+
285
+		$protected = [
286
+			"alg" => "RS256",
287
+			"jwk" => [
288
+				"kty" => "RSA",
289
+				"n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"]),
290
+				"e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]),
291
+			],
292
+			"nonce" => $this->nonce,
293
+			"url" => $url
294
+		];
295
+
296
+		$payload64 = LEFunctions::base64UrlSafeEncode(
297
+			str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload)
298
+		);
299
+		$protected64 = LEFunctions::base64UrlSafeEncode(json_encode($protected));
300
+
301
+		openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, OPENSSL_ALGO_SHA256);
302
+		$signed64 = LEFunctions::base64UrlSafeEncode($signed);
303
+
304
+		$data = [
305
+			'protected' => $protected64,
306
+			'payload' => $payload64,
307
+			'signature' => $signed64
308
+		];
309
+
310
+		return json_encode($data);
311
+	}
312
+
313
+	/**
314
+	 * Generates a Key ID signature to attach to the request.
315
+	 *
316
+	 * @param array|string $payload The payload to add to the signature.
317
+	 * @param string $kid The Key ID to use in the signature.
318
+	 * @param string $url The URL to use in the signature.
319
+	 * @param string $privateKey The private key to sign the request with. Defaults to account key
320
+	 *
321
+	 * @return string   Returns a JSON encoded string containing the signature.
322
+	 */
323
+	public function signRequestKid($payload, $kid, $url, $privateKey = '')
324
+	{
325
+		if ($privateKey == '') {
326
+			$privateKey = $this->storage->getAccountPrivateKey();
327
+		}
328
+		$privateKey = openssl_pkey_get_private($privateKey);
329
+
330
+		//$details = openssl_pkey_get_details($privateKey);
331
+
332
+		$protected = [
333
+			"alg" => "RS256",
334
+			"kid" => $kid,
335
+			"nonce" => $this->nonce,
336
+			"url" => $url
337
+		];
338
+
339
+		$payload64 = LEFunctions::base64UrlSafeEncode(
340
+			str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload)
341
+		);
342
+		$protected64 = LEFunctions::base64UrlSafeEncode(json_encode($protected));
343
+
344
+		openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, OPENSSL_ALGO_SHA256);
345
+		$signed64 = LEFunctions::base64UrlSafeEncode($signed);
346
+
347
+		$data = [
348
+			'protected' => $protected64,
349
+			'payload' => $payload64,
350
+			'signature' => $signed64
351
+		];
352
+
353
+		return json_encode($data);
354
+	}
355 355
 }
Please login to merge, or discard this patch.