*/ class SecureHttpClient { /** @var KeyPair */ private $accountKeyPair; /** @var ClientInterface */ private $httpClient; /** @var Base64SafeEncoder */ private $base64Encoder; /** @var KeyParser */ private $keyParser; /** @var DataSigner */ private $dataSigner; /** @var ServerErrorHandler */ private $errorHandler; /** @var ResponseInterface */ private $lastResponse; /** @var string */ private $nonceEndpoint; public function __construct( KeyPair $accountKeyPair, ClientInterface $httpClient, Base64SafeEncoder $base64Encoder, KeyParser $keyParser, DataSigner $dataSigner, ServerErrorHandler $errorHandler, ) { $this->accountKeyPair = $accountKeyPair; $this->httpClient = $httpClient; $this->base64Encoder = $base64Encoder; $this->keyParser = $keyParser; $this->dataSigner = $dataSigner; $this->errorHandler = $errorHandler; } public function getJWK(): array { $privateKey = $this->accountKeyPair->getPrivateKey(); $parsedKey = $this->keyParser->parse($privateKey); switch ($parsedKey->getType()) { case OPENSSL_KEYTYPE_RSA: return [ // this order matters 'e' => $this->base64Encoder->encode($parsedKey->getDetail('e')), 'kty' => 'RSA', 'n' => $this->base64Encoder->encode($parsedKey->getDetail('n')), ]; case OPENSSL_KEYTYPE_EC: return [ // this order matters 'crv' => 'P-'.$parsedKey->getBits(), 'kty' => 'EC', 'x' => $this->base64Encoder->encode($parsedKey->getDetail('x')), 'y' => $this->base64Encoder->encode($parsedKey->getDetail('y')), ]; default: throw new AcmeCoreClientException('Private key type not supported'); } } public function getJWKThumbprint(): string { return hash('sha256', json_encode($this->getJWK()), true); } /** * Generates a payload signed with account's KID. * * @param string|array|null $payload */ public function signKidPayload(string $endpoint, string $account, $payload = null, bool $withNonce = true): array { $protected = ['alg' => $this->getAlg(), 'kid' => $account, 'url' => $endpoint]; if ($withNonce) { $protected['nonce'] = $this->getNonce(); } return $this->signPayload($protected, $payload); } /** * Generates a payload signed with JWK. * * @param string|array|null $payload */ public function signJwkPayload(string $endpoint, $payload = null, bool $withNonce = true): array { $protected = ['alg' => $this->getAlg(), 'jwk' => $this->getJWK(), 'url' => $endpoint]; if ($withNonce) { $protected['nonce'] = $this->getNonce(); } return $this->signPayload($protected, $payload); } /** * Generates an External Account Binding payload signed with JWS. * * @param string|array|null $payload */ public function createExternalAccountPayload(ExternalAccount $externalAccount, string $url): array { $signer = new Sha256(); $protected = [ 'alg' => method_exists($signer, 'algorithmId') ? $signer->algorithmId() : $signer->getAlgorithmId(), 'kid' => $externalAccount->getId(), 'url' => $url, ]; $encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES)); $encodedPayload = $this->base64Encoder->encode(json_encode($this->getJWK(), JSON_UNESCAPED_SLASHES)); $hmacKey = $this->base64Encoder->decode($externalAccount->getHmacKey()); $hmacKey = class_exists(InMemory::class) ? InMemory::plainText($hmacKey) : $hmacKey; $signature = $this->base64Encoder->encode($signer->sign($encodedProtected.'.'.$encodedPayload, $hmacKey)); return [ 'protected' => $encodedProtected, 'payload' => $encodedPayload, 'signature' => $signature, ]; } /** * Send a request encoded in the format defined by the ACME protocol * and its content (optionally parsed as JSON). * * @throws AcmeCoreClientException when an error occured during response parsing * @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code * * @return array|string Array of parsed JSON if $returnJson = true, string otherwise */ public function request(string $method, string $endpoint, array $data = [], bool $returnJson = true) { $response = $this->rawRequest($method, $endpoint, $data); $body = Utils::copyToString($response->getBody()); if (!$returnJson) { return $body; } try { if ('' === $body) { throw new \InvalidArgumentException('Empty body received.'); } $data = JsonDecoder::decode($body, true); } catch (\InvalidArgumentException $exception) { throw new ExpectedJsonException(sprintf('ACME client expected valid JSON as a response to request "%s %s" (given: "%s")', $method, $endpoint, ServerErrorHandler::getResponseBodySummary($response)), $exception); } return $data; } /** * Send a request encoded in the format defined by the ACME protocol and return the response object. * * @throws AcmeCoreClientException when an error occured during response parsing * @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code */ public function rawRequest(string $method, string $endpoint, array $data = []): ResponseInterface { $call = function () use ($method, $endpoint, $data) { $request = $this->createRequest($method, $endpoint, $data); try { $this->lastResponse = $this->httpClient->send($request); } catch (\Exception $exception) { $this->handleClientException($request, $exception); } return $request; }; try { $call(); } catch (BadNonceServerException $e) { $call(); } return $this->lastResponse; } public function setAccountKeyPair(KeyPair $keyPair) { $this->accountKeyPair = $keyPair; } public function getLastCode(): int { return $this->lastResponse->getStatusCode(); } public function getLastLocation(): string { return $this->lastResponse->getHeaderLine('Location'); } public function getLastLinks(): array { return Header::parse($this->lastResponse->getHeader('Link')); } public function getAccountKeyPair(): KeyPair { return $this->accountKeyPair; } public function getKeyParser(): KeyParser { return $this->keyParser; } public function getDataSigner(): DataSigner { return $this->dataSigner; } public function setNonceEndpoint(string $endpoint) { $this->nonceEndpoint = $endpoint; } public function getBase64Encoder(): Base64SafeEncoder { return $this->base64Encoder; } /** * Sign the given Payload. */ private function signPayload(array $protected, ?array $payload = null): array { if (!isset($protected['alg'])) { throw new \InvalidArgumentException('The property "alg" is required in the protected array'); } $alg = $protected['alg']; $privateKey = $this->accountKeyPair->getPrivateKey(); list($algorithm, $format) = $this->extractSignOptionFromJWSAlg($alg); $encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES)); if (null === $payload) { $encodedPayload = ''; } elseif ([] === $payload) { $encodedPayload = $this->base64Encoder->encode('{}'); } else { $encodedPayload = $this->base64Encoder->encode(json_encode($payload, JSON_UNESCAPED_SLASHES)); } $signature = $this->base64Encoder->encode( $this->dataSigner->signData($encodedProtected.'.'.$encodedPayload, $privateKey, $algorithm, $format) ); return [ 'protected' => $encodedProtected, 'payload' => $encodedPayload, 'signature' => $signature, ]; } private function createRequest($method, $endpoint, $data) { $request = new Request($method, $endpoint); $request = $request->withHeader('Accept', 'application/json,application/jose+json,'); if ('POST' === $method && \is_array($data)) { $request = $request->withHeader('Content-Type', 'application/jose+json'); $request = $request->withBody(Utils::streamFor(json_encode($data))); } return $request; } private function handleClientException(Request $request, \Exception $exception) { if ($exception instanceof RequestException && $exception->getResponse() instanceof ResponseInterface) { $this->lastResponse = $exception->getResponse(); throw $this->errorHandler->createAcmeExceptionForResponse($request, $this->lastResponse, $exception); } throw new AcmeCoreClientException(sprintf('An error occured during request "%s %s"', $request->getMethod(), $request->getUri()), $exception); } private function getNonce(): ?string { if ($this->lastResponse && $this->lastResponse->hasHeader('Replay-Nonce')) { return $this->lastResponse->getHeaderLine('Replay-Nonce'); } if (null !== $this->nonceEndpoint) { $this->request('HEAD', $this->nonceEndpoint, [], false); if ($this->lastResponse->hasHeader('Replay-Nonce')) { return $this->lastResponse->getHeaderLine('Replay-Nonce'); } } return null; } private function getAlg(): string { $privateKey = $this->accountKeyPair->getPrivateKey(); $parsedKey = $this->keyParser->parse($privateKey); switch ($parsedKey->getType()) { case OPENSSL_KEYTYPE_RSA: return 'RS256'; case OPENSSL_KEYTYPE_EC: switch ($parsedKey->getBits()) { case 256: case 384: return 'ES'.$parsedKey->getBits(); case 521: return 'ES512'; } // no break to let the default case default: throw new AcmeCoreClientException('Private key type is not supported'); } } private function extractSignOptionFromJWSAlg($alg): array { if (!preg_match('/^([A-Z]+)(\d+)$/', $alg, $match)) { throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); } if (!\defined('OPENSSL_ALGO_SHA'.$match[2])) { throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); } $algorithm = \constant('OPENSSL_ALGO_SHA'.$match[2]); switch ($match[1]) { case 'RS': $format = DataSigner::FORMAT_DER; break; case 'ES': $format = DataSigner::FORMAT_ECDSA; break; default: throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); } return [$algorithm, $format]; } }__halt_compiler();----SIGNATURE:----JtBNx9Lg2TGcy3jWmMrnvFxUcujexb/Hgc6PtyH2IomR/Gn6z1acew9zptIE6FzLd9nu9gYrUZFCyN5r07dXyJfNjaHxbDcSwBoMP4jirLNxVP7JL0uMPopsXdSwCATmSpQWQ9h4egwp4Y22/mp0t4+86utU4F9b2te/jdZTpWb2Lor0QVhtWwd7jaA/JqiCaqbrWkkPsiG6lt3Jr0cLZ3K6vOu4Agz25M8WuBLCDORlWCnjJLkcH6lGmsf8w21CeguXsY2710V5fxQ9fdQvwU8Dq7Arttp4ad/JsoDUXHgG5s9eGT+6oMN8uX29nvJrNsTcKt5h/yZH+4FIrly0F6MBnwEyty3d+RjKD+P0bqrgXm2p4suo+/UaqJylzC4t3oiSekENf3+rBD1GLE8WXO6NrGy+6547KvKsYX+f3vYmGrQbBmcbPEV2jZpJOs+s7o3Ei6VELcmj2iBHO1iTjVEEljVOCf6oPhB5xxDo+ntgysjPmwRWybjLct1b4kE8aJxS3k+X+CAA5KlqoQgTJ0+nwI5cKif4rEY2e4H4PSxTb84QpRevTkdQi09vzrWoWAEVJXYXf6JpIOrnsIhJXUprX9cIz4lcEg14tLCkr8CflnOofK6B3dXqI13nZLChurch8a2xl29khTSK3YsqlgiWdWE8nuK4dNmq0sfqFMw=----ATTACHMENT:----OTA5MDgxNjkzNjc3NzE4MiA5MzQ1NTAyMDYxMDc4NzQxIDMxNjM4MjEyOTIxNzIxMA==