From e654975374b3dff99d183acfa9999c53b88c2e68 Mon Sep 17 00:00:00 2001 From: Michael Munger Date: Fri, 24 Apr 2020 16:32:07 -0600 Subject: [PATCH 1/3] Remove unused use statements from helper --- src/Helper.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Helper.php b/src/Helper.php index da5ae14..1890dc5 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -2,8 +2,6 @@ namespace Afosto\Acme; -use Afosto\Acme\Data\Authorization; -use GuzzleHttp\Client as HttpClient; use GuzzleHttp\Exception\ClientException; /** From 713285f8b5548217cb45835787c4d2670931c854 Mon Sep 17 00:00:00 2001 From: Michael Munger Date: Fri, 24 Apr 2020 16:36:49 -0600 Subject: [PATCH 2/3] Add support to get certificate chain as separate certificates - https://github.com/afosto/yaac/issues/10 - The purpose is to support web servers such as apache < 2.4.8 where the intermediate certificate needs to be separate - Add method Helper::splitCertificate to parse cert chain - Add param to Certificate::getCertificate($asChain = true) to maintain bc and get the certificate with or without chain - Add method Certificate::getIntermediateCertificate to get just the intermediate certificate --- src/Data/Certificate.php | 24 ++++++++++++++++++++++-- src/Helper.php | 27 ++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/Data/Certificate.php b/src/Data/Certificate.php index 9fda72e..ee26397 100644 --- a/src/Data/Certificate.php +++ b/src/Data/Certificate.php @@ -17,6 +17,16 @@ class Certificate */ protected $certificate; + /** + * @var string + */ + protected $certificateNoChain; + + /** + * @var string + */ + protected $intermediateCertificate; + /** * @var string */ @@ -39,6 +49,7 @@ public function __construct($privateKey, $csr, $certificate) $this->privateKey = $privateKey; $this->csr = $csr; $this->certificate = $certificate; + list($this->certificateNoChain, $this->intermediateCertificate) = Helper::splitCertificate($certificate); $this->expiryDate = Helper::getCertExpiryDate($certificate); } @@ -64,9 +75,18 @@ public function getExpiryDate(): \DateTime * Return the certificate as a multi line string * @return string */ - public function getCertificate(): string + public function getCertificate($asChain = true): string + { + return $asChain ? $this->certificate : $this->certificateNoChain; + } + + /** + * Return the intermediate certificate as a multi line string + * @return string + */ + public function getIntermediateCertificate(): string { - return $this->certificate; + return $this->intermediateCertificate; } /** diff --git a/src/Helper.php b/src/Helper.php index 1890dc5..ab19ce7 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -65,7 +65,7 @@ public static function getNewKey(): string /** * Get a new CSR * - * @param array $domains + * @param array $domains * @param $key * * @return string @@ -81,8 +81,8 @@ public static function getCsr(array $domains, $key): string '[v3_req]', '[v3_ca]', '[SAN]', - 'subjectAltName=' . implode(',', array_map(function ($domain) { - return 'DNS:' . $domain; + 'subjectAltName='.implode(',', array_map(function ($domain) { + return 'DNS:'.$domain; }, $domains)), ]; @@ -138,4 +138,25 @@ public static function getKeyDetails($key): array return $accountDetails; } + + /** + * Split a two certificate bundle into separate + * multi line string certificates + * @return array + */ + public static function splitCertificate(string $certificate): array + { + preg_match('/^(?-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)\n' + .'(?-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)$/s', + $certificate, $certificates); + + $signed = $certificates['signed'] ?? null; + $intermediate = $certificates['intermediate'] ?? null; + + if (!$signed || !$intermediate) { + throw new \Exception('Could not parse certificate string'); + } + + return [$signed, $intermediate]; + } } From 32b9d432db8ffc1e0f30e1e065cbd1ee89fb7853 Mon Sep 17 00:00:00 2001 From: peterbakker Date: Tue, 28 Apr 2020 20:38:51 +0200 Subject: [PATCH 3/3] naming changes readme update --- README.md | 6 ++++++ src/Client.php | 38 +++++++++++++++++++------------------- src/Data/Certificate.php | 22 ++++++++++++---------- src/Helper.php | 36 ++++++++++++++++++++---------------- 4 files changed, 57 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 938371e..465e738 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,12 @@ file_put_contents('certificate.cert', $certificate->getCertificate()); file_put_contents('private.key', $certificate->getPrivateKey()); ``` +>To get a seperate intermediate certificate and domain certificate: +>```php +>$domainCertificate = $certificate->getCertificate(false); +>$intermediateCertificate = $certificate->getIntermediate(); +>``` + ### Who is using it? Are you using this package, would love to know. Please send a PR to enlist your project or company. diff --git a/src/Client.php b/src/Client.php index 378c5e7..ecc2ec0 100644 --- a/src/Client.php +++ b/src/Client.php @@ -194,7 +194,7 @@ public function createOrder(array $domains): Order foreach ($domains as $domain) { $identifiers[] = [ - 'type' => 'dns', + 'type' => 'dns', 'value' => $domain, ]; } @@ -331,8 +331,8 @@ public function getCertificate(Order $order): Certificate $data['certificate'], $this->signPayloadKid(null, $data['certificate']) ); - $certificate = $str = preg_replace('/^[ \t]*[\r\n]+/m', '', (string)$certificateResponse->getBody()); - return new Certificate($privateKey, $csr, $certificate); + $chain = $str = preg_replace('/^[ \t]*[\r\n]+/m', '', (string)$certificateResponse->getBody()); + return new Certificate($privateKey, $csr, $chain); } @@ -383,8 +383,8 @@ protected function getHttpClient() protected function getSelfTestClient() { return new HttpClient([ - 'verify' => false, - 'timeout' => 10, + 'verify' => false, + 'timeout' => 10, 'connect_timeout' => 3, 'allow_redirects' => true, ]); @@ -459,9 +459,9 @@ protected function selfDNSTest(Authorization $authorization, $maxAttempts) protected function getSelfTestDNSClient() { return new HttpClient([ - 'base_uri' => 'https://cloudflare-dns.com', + 'base_uri' => 'https://cloudflare-dns.com', 'connect_timeout' => 10, - 'headers' => [ + 'headers' => [ 'Accept' => 'application/dns-json', ], ]); @@ -511,7 +511,7 @@ protected function tosAgree() $this->getUrl(self::DIRECTORY_NEW_ACCOUNT), $this->signPayloadJWK( [ - 'contact' => [ + 'contact' => [ 'mailto:' . $this->getOption('username'), ], 'termsOfServiceAgreed' => true, @@ -590,7 +590,7 @@ protected function request($url, $payload = [], $method = 'POST'): ResponseInter { try { $response = $this->getHttpClient()->request($method, $url, [ - 'json' => $payload, + 'json' => $payload, 'headers' => [ 'Content-Type' => 'application/jose+json', ] @@ -650,9 +650,9 @@ protected function getAccountKey() protected function getJWKHeader(): array { return [ - 'e' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['e']), + 'e' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['e']), 'kty' => 'RSA', - 'n' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['n']), + 'n' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['n']), ]; } @@ -671,10 +671,10 @@ protected function getJWK($url): array $this->nonce = $response->getHeaderLine('replay-nonce'); } return [ - 'alg' => 'RS256', - 'jwk' => $this->getJWKHeader(), + 'alg' => 'RS256', + 'jwk' => $this->getJWKHeader(), 'nonce' => $this->nonce, - 'url' => $url + 'url' => $url ]; } @@ -691,10 +691,10 @@ protected function getKID($url): array $nonce = $response->getHeaderLine('replay-nonce'); return [ - "alg" => "RS256", - "kid" => $this->account->getAccountURL(), + "alg" => "RS256", + "kid" => $this->account->getAccountURL(), "nonce" => $nonce, - "url" => $url + "url" => $url ]; } @@ -720,7 +720,7 @@ protected function signPayloadJWK($payload, $url): array return [ 'protected' => $protected, - 'payload' => $payload, + 'payload' => $payload, 'signature' => Helper::toSafeString($signature), ]; } @@ -746,7 +746,7 @@ protected function signPayloadKid($payload, $url): array return [ 'protected' => $protected, - 'payload' => $payload, + 'payload' => $payload, 'signature' => Helper::toSafeString($signature), ]; } diff --git a/src/Data/Certificate.php b/src/Data/Certificate.php index ee26397..b393d5c 100644 --- a/src/Data/Certificate.php +++ b/src/Data/Certificate.php @@ -15,12 +15,12 @@ class Certificate /** * @var string */ - protected $certificate; + protected $chain; /** * @var string */ - protected $certificateNoChain; + protected $certificate; /** * @var string @@ -41,16 +41,16 @@ class Certificate * Certificate constructor. * @param $privateKey * @param $csr - * @param $certificate + * @param $chain * @throws \Exception */ - public function __construct($privateKey, $csr, $certificate) + public function __construct($privateKey, $csr, $chain) { $this->privateKey = $privateKey; $this->csr = $csr; - $this->certificate = $certificate; - list($this->certificateNoChain, $this->intermediateCertificate) = Helper::splitCertificate($certificate); - $this->expiryDate = Helper::getCertExpiryDate($certificate); + $this->chain = $chain; + list($this->certificate, $this->intermediateCertificate) = Helper::splitCertificate($chain); + $this->expiryDate = Helper::getCertExpiryDate($chain); } /** @@ -72,19 +72,21 @@ public function getExpiryDate(): \DateTime } /** - * Return the certificate as a multi line string + * Return the certificate as a multi line string, by default it includes the intermediate certificate as well + * + * @param bool $asChain * @return string */ public function getCertificate($asChain = true): string { - return $asChain ? $this->certificate : $this->certificateNoChain; + return $asChain ? $this->chain : $this->certificate; } /** * Return the intermediate certificate as a multi line string * @return string */ - public function getIntermediateCertificate(): string + public function getIntermediate(): string { return $this->intermediateCertificate; } diff --git a/src/Helper.php b/src/Helper.php index ab19ce7..bad5893 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -65,7 +65,7 @@ public static function getNewKey(): string /** * Get a new CSR * - * @param array $domains + * @param array $domains * @param $key * * @return string @@ -81,8 +81,8 @@ public static function getCsr(array $domains, $key): string '[v3_req]', '[v3_ca]', '[SAN]', - 'subjectAltName='.implode(',', array_map(function ($domain) { - return 'DNS:'.$domain; + 'subjectAltName=' . implode(',', array_map(function ($domain) { + return 'DNS:' . $domain; }, $domains)), ]; @@ -90,11 +90,11 @@ public static function getCsr(array $domains, $key): string file_put_contents($fn, implode("\n", $config)); $csr = openssl_csr_new([ 'countryName' => 'NL', - 'commonName' => $primaryDomain, + 'commonName' => $primaryDomain, ], $key, [ - 'config' => $fn, + 'config' => $fn, 'req_extensions' => 'SAN', - 'digest_alg' => 'sha512', + 'digest_alg' => 'sha512', ]); unlink($fn); @@ -140,23 +140,27 @@ public static function getKeyDetails($key): array } /** - * Split a two certificate bundle into separate - * multi line string certificates + * Split a two certificate bundle into separate multi line string certificates + * @param string $chain * @return array + * @throws \Exception */ - public static function splitCertificate(string $certificate): array + public static function splitCertificate(string $chain): array { - preg_match('/^(?-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)\n' - .'(?-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)$/s', - $certificate, $certificates); - - $signed = $certificates['signed'] ?? null; + preg_match( + '/^(?-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)\n' + . '(?-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)$/s', + $chain, + $certificates + ); + + $domain = $certificates['domain'] ?? null; $intermediate = $certificates['intermediate'] ?? null; - if (!$signed || !$intermediate) { + if (!$domain || !$intermediate) { throw new \Exception('Could not parse certificate string'); } - return [$signed, $intermediate]; + return [$domain, $intermediate]; } }