From ca164317d1cda88f067008689913df37afc9a8d1 Mon Sep 17 00:00:00 2001 From: yourivw Date: Thu, 29 Mar 2018 12:02:54 +0200 Subject: [PATCH] Release version 1.1.1 - Fixed 415 Unsupported Media Type error caused by ACME protocol divergence. - Fixed certificate revocation problem addressed in issue #25. - Set default acmeURL to the production environment. - Implemented option to bypass local DNS/HTTP verification verifyPendingOrderAuthorization($identifier, $type, $localcheck = true). It is still making a local verification by default. Documentation is to be updated. --- LEClient/LEClient.php | 35 ++++++++++++++------------------ LEClient/src/LEAccount.php | 4 ++-- LEClient/src/LEAuthorization.php | 2 +- LEClient/src/LEConnector.php | 4 ++-- LEClient/src/LEFunctions.php | 11 +++++----- LEClient/src/LEOrder.php | 19 +++++++++-------- composer.json | 2 +- 7 files changed, 36 insertions(+), 41 deletions(-) diff --git a/LEClient/LEClient.php b/LEClient/LEClient.php index 3ea7e56..4e14aa1 100644 --- a/LEClient/LEClient.php +++ b/LEClient/LEClient.php @@ -39,7 +39,7 @@ * @author Youri van Weegberg * @copyright 2018 Youri van Weegberg * @license https://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.0 + * @version 1.1.1 * @link https://github.com/yourivw/LEClient * @since Class available since Release 1.0.0 */ @@ -67,11 +67,11 @@ class LEClient * @param boolean $acmeURL ACME URL, can be string or one of predefined values: LE_STAGING or LE_PRODUCTION. Defaults to LE_STAGING. * @param int $log The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted. Defaults to LOG_OFF. (optional) * @param string $certificateKeys The main directory in which all keys (and certificates), including account keys, are stored. Defaults to 'keys/'. (optional) - * @param array $certificateKeys Optional array containing location of all certificate files. Required paths are public_key, private_key, order and certificate/fullchain_certificate (you can use both or only one of them) + * @param array $certificateKeys Optional array containing location of all certificate files. Required paths are public_key, private_key, order and certificate/fullchain_certificate (you can use both or only one of them) * @param string $accountKeys The directory in which the account keys are stored. Is a subdir inside $certificateKeys. Defaults to '__account/'.(optional) - * @param array $accountKeys Optional array containing location of account private and public keys. Required paths are private_key, public_key. + * @param array $accountKeys Optional array containing location of account private and public keys. Required paths are private_key, public_key. */ - public function __construct($email, $acmeURL = LEClient::LE_STAGING, $log = LEClient::LOG_OFF, $certificateKeys = 'keys/', $accountKeys = '__account/') + public function __construct($email, $acmeURL = LEClient::LE_PRODUCTION, $log = LEClient::LOG_OFF, $certificateKeys = 'keys/', $accountKeys = '__account/') { $this->log = $log; @@ -85,14 +85,13 @@ public function __construct($email, $acmeURL = LEClient::LE_STAGING, $log = LECl { $this->baseURL = $acmeURL; } - else throw new \RuntimeException('acmeURL must be set to string or bool (legacy)'); + else throw new \RuntimeException('acmeURL must be set to string or bool (legacy).'); - if (is_array($certificateKeys) && is_string($accountKeys)) throw new \RuntimeException('when certificateKeys is array, accountKeys must be array also'); - elseif (is_array($accountKeys) && is_string($certificateKeys)) throw new \RuntimeException('when accountKeys is array, certificateKeys must be array also'); + if (is_array($certificateKeys) && is_string($accountKeys)) throw new \RuntimeException('When certificateKeys is array, accountKeys must be array too.'); + elseif (is_array($accountKeys) && is_string($certificateKeys)) throw new \RuntimeException('When accountKeys is array, certificateKeys must be array too.'); if (is_string($certificateKeys)) { - $certificateKeysDir = $certificateKeys; if(!file_exists($certificateKeys)) @@ -108,32 +107,28 @@ public function __construct($email, $acmeURL = LEClient::LE_STAGING, $log = LECl "fullchain_certificate" => $certificateKeys.'/fullchain.crt', "order" => $certificateKeys.'/order' ); - } elseif (is_array($certificateKeys)) { - - if (!isset($certificateKeys['certificate']) && !isset($certificateKeys['fullchain_certificate'])) throw new \RuntimeException('certificateKeys[certificate] or certificateKeys[fullchain_certificate] file path must be set'); - if (!isset($certificateKeys['private_key'])) throw new \RuntimeException('certificateKeys[private_key] file path must be set'); + if (!isset($certificateKeys['certificate']) && !isset($certificateKeys['fullchain_certificate'])) throw new \RuntimeException('certificateKeys[certificate] or certificateKeys[fullchain_certificate] file path must be set.'); + if (!isset($certificateKeys['private_key'])) throw new \RuntimeException('certificateKeys[private_key] file path must be set.'); if (!isset($certificateKeys['order'])) $certificateKeys['order'] = dirname($certificateKeys['private_key']).'/order'; if (!isset($certificateKeys['public_key'])) $certificateKeys['public_key'] = dirname($certificateKeys['private_key']).'/public.pem'; foreach ($certificateKeys as $param => $file) { $parentDir = dirname($file); - if (!is_dir($parentDir)) throw new \RuntimeException($parentDir.' directory not found'); + if (!is_dir($parentDir)) throw new \RuntimeException($parentDir.' directory not found.'); } $this->certificateKeys = $certificateKeys; - } else { - throw new \RuntimeException('certificateKeys must be string or array'); + throw new \RuntimeException('certificateKeys must be string or array.'); } if (is_string($accountKeys)) { - $accountKeys = $certificateKeysDir.'/'.$accountKeys; if(!file_exists($accountKeys)) @@ -149,12 +144,12 @@ public function __construct($email, $acmeURL = LEClient::LE_STAGING, $log = LECl } elseif (is_array($accountKeys)) { - if (!isset($accountKeys['private_key'])) throw new \RuntimeException('accountKeys[private_key] file path must be set'); - if (!isset($accountKeys['public_key'])) throw new \RuntimeException('accountKeys[public_key] file path must be set'); + if (!isset($accountKeys['private_key'])) throw new \RuntimeException('accountKeys[private_key] file path must be set.'); + if (!isset($accountKeys['public_key'])) throw new \RuntimeException('accountKeys[public_key] file path must be set.'); foreach ($accountKeys as $param => $file) { $parentDir = dirname($file); - if (!is_dir($parentDir)) throw new \RuntimeException($parentDir.' directory not found'); + if (!is_dir($parentDir)) throw new \RuntimeException($parentDir.' directory not found.'); } $this->accountKeys = $accountKeys; @@ -167,7 +162,7 @@ public function __construct($email, $acmeURL = LEClient::LE_STAGING, $log = LECl $this->connector = new LEConnector($this->log, $this->baseURL, $this->accountKeys); $this->account = new LEAccount($this->connector, $this->log, $email, $this->accountKeys); - if($this->log) LEFunctions::log('LEClient finished constructing', 'function LEClient __construct'); + if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('LEClient finished constructing', 'function LEClient __construct'); } diff --git a/LEClient/src/LEAccount.php b/LEClient/src/LEAccount.php index bf1fe01..31e0b31 100644 --- a/LEClient/src/LEAccount.php +++ b/LEClient/src/LEAccount.php @@ -30,7 +30,7 @@ * @author Youri van Weegberg * @copyright 2018 Youri van Weegberg * @license https://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.0 + * @version 1.1.1 * @link https://github.com/yourivw/LEClient * @since Class available since Release 1.0.0 */ @@ -178,7 +178,7 @@ public function changeAccountKeys() LEFunctions::RSAgenerateKeys(null, $this->accountKeys['private_key'].'.new', $this->accountKeys['public_key'].'.new'); $privateKey = openssl_pkey_get_private(file_get_contents($this->accountKeys['private_key'].'.new')); $details = openssl_pkey_get_details($privateKey); - $innerPayload = array('account' => $this->connector->accountURL, 'newKey' => array( + $innerPayload = array('account' => $this->connector->accountURL, 'newKey' => array( "kty" => "RSA", "n" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["n"]), "e" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["e"]) diff --git a/LEClient/src/LEAuthorization.php b/LEClient/src/LEAuthorization.php index 7ea82a0..a7ffcbb 100644 --- a/LEClient/src/LEAuthorization.php +++ b/LEClient/src/LEAuthorization.php @@ -30,7 +30,7 @@ * @author Youri van Weegberg * @copyright 2018 Youri van Weegberg * @license https://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.0 + * @version 1.1.1 * @link https://github.com/yourivw/LEClient * @since Class available since Release 1.0.0 */ diff --git a/LEClient/src/LEConnector.php b/LEClient/src/LEConnector.php index 1bdc5fd..651062b 100644 --- a/LEClient/src/LEConnector.php +++ b/LEClient/src/LEConnector.php @@ -30,7 +30,7 @@ * @author Youri van Weegberg * @copyright 2018 Youri van Weegberg * @license https://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.0 + * @version 1.1.1 * @link https://github.com/yourivw/LEClient * @since Class available since Release 1.0.0 */ @@ -102,7 +102,7 @@ private function request($method, $URL, $data = null) { if($this->accountDeactivated) throw new \RuntimeException('The account was deactivated. No further requests can be made.'); - $headers = array('Accept: application/json', 'Content-Type: application/json'); + $headers = array('Accept: application/json', 'Content-Type: application/jose+json'); $requestURL = preg_match('~^http~', $URL) ? $URL : $this->baseURL . $URL; $handle = curl_init(); curl_setopt($handle, CURLOPT_URL, $requestURL); diff --git a/LEClient/src/LEFunctions.php b/LEClient/src/LEFunctions.php index 7e59dda..ea8371c 100644 --- a/LEClient/src/LEFunctions.php +++ b/LEClient/src/LEFunctions.php @@ -30,7 +30,7 @@ * @author Youri van Weegberg * @copyright 2018 Youri van Weegberg * @license https://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.0 + * @version 1.1.1 * @link https://github.com/yourivw/LEClient * @since Class available since Release 1.0.0 */ @@ -47,7 +47,7 @@ class LEFunctions public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pem', $publicKeyFile = 'public.pem', $keySize = 4096) { - if ($keySize < 2048 || $keySize > 4096) throw new \RuntimeException("RSA key size must be between 2048 and 4096"); + if ($keySize < 2048 || $keySize > 4096) throw new \RuntimeException("RSA key size must be between 2048 and 4096."); $res = openssl_pkey_new(array( "private_key_type" => OPENSSL_KEYTYPE_RSA, @@ -82,9 +82,8 @@ public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pe */ public static function ECGenerateKeys($directory, $privateKeyFile = 'private.pem', $publicKeyFile = 'public.pem', $keySize = 256) { - if (version_compare(PHP_VERSION, '7.1.0') == -1) throw new \RuntimeException("PHP 7.1+ required for EC keys"); - - + if (version_compare(PHP_VERSION, '7.1.0') == -1) throw new \RuntimeException("PHP 7.1+ required for EC keys."); + if ($keySize == 256) { $res = openssl_pkey_new(array( @@ -99,7 +98,7 @@ public static function ECGenerateKeys($directory, $privateKeyFile = 'private.pem "curve_name" => "secp384r1", )); } - else throw new \RuntimeException("EC key size must be 256 or 384"); + else throw new \RuntimeException("EC key size must be 256 or 384."); if(!openssl_pkey_export($res, $privateKey)) throw new \RuntimeException("EC keypair export failed!"); diff --git a/LEClient/src/LEOrder.php b/LEClient/src/LEOrder.php index bc235f0..8bff15c 100644 --- a/LEClient/src/LEOrder.php +++ b/LEClient/src/LEOrder.php @@ -30,7 +30,7 @@ * @author Youri van Weegberg * @copyright 2018 Youri van Weegberg * @license https://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.0 + * @version 1.1.1 * @link https://github.com/yourivw/LEClient * @since Class available since Release 1.0.0 */ @@ -339,10 +339,11 @@ public function getPendingAuthorizations($type) * * @param string $identifier The domain name to verify. * @param int $type The type of verification. Supporting LEOrder::CHALLENGE_TYPE_HTTP and LEOrder::CHALLENGE_TYPE_DNS. + * @param boolean $localcheck Whether to verify the authorization locally before making the authorization request to LE. Optional, default to true. * * @return boolean Returns true when the verification request was successful, false if not. */ - public function verifyPendingOrderAuthorization($identifier, $type) + public function verifyPendingOrderAuthorization($identifier, $type, $localcheck = true) { $privateKey = openssl_pkey_get_private(file_get_contents($this->connector->accountKeys['private_key'])); $details = openssl_pkey_get_details($privateKey); @@ -368,13 +369,13 @@ public function verifyPendingOrderAuthorization($identifier, $type) switch($type) { case LEOrder::CHALLENGE_TYPE_HTTP: - if(LEFunctions::checkHTTPChallenge($identifier, $challenge['token'], $keyAuthorization)) + if($localcheck == false OR LEFunctions::checkHTTPChallenge($identifier, $challenge['token'], $keyAuthorization)) { $sign = $this->connector->signRequestKid(array('keyAuthorization' => $keyAuthorization), $this->connector->accountURL, $challenge['url']); $post = $this->connector->post($challenge['url'], $sign); if(strpos($post['header'], "200 OK") !== false) { - if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('HTTP challenge for \'' . $identifier . '\' valid.', 'function verifyPendingOrderAuthorization'); + if($localcheck && $this->log >= LECLient::LOG_STATUS) LEFunctions::log('HTTP challenge for \'' . $identifier . '\' valid.', 'function verifyPendingOrderAuthorization'); while($auth->status == 'pending') { sleep(1); @@ -385,18 +386,18 @@ public function verifyPendingOrderAuthorization($identifier, $type) } else { - if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('HTTP challenge for \'' . $identifier . '\' tested, found invalid.', 'function verifyPendingOrderAuthorization'); + if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('HTTP challenge for \'' . $identifier . '\' tested locally, found invalid.', 'function verifyPendingOrderAuthorization'); } break; case LEOrder::CHALLENGE_TYPE_DNS: $DNSDigest = LEFunctions::Base64UrlSafeEncode(hash('sha256', $keyAuthorization, true)); - if(LEFunctions::checkDNSChallenge($identifier, $DNSDigest)) + if($localcheck == false OR LEFunctions::checkDNSChallenge($identifier, $DNSDigest)) { $sign = $this->connector->signRequestKid(array('keyAuthorization' => $keyAuthorization), $this->connector->accountURL, $challenge['url']); $post = $this->connector->post($challenge['url'], $sign); if(strpos($post['header'], "200 OK") !== false) { - if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('DNS challenge for \'' . $identifier . '\' valid.', 'function verifyPendingOrderAuthorization'); + if($localcheck && $this->log >= LECLient::LOG_STATUS) LEFunctions::log('DNS challenge for \'' . $identifier . '\' valid.', 'function verifyPendingOrderAuthorization'); while($auth->status == 'pending') { sleep(1); @@ -407,7 +408,7 @@ public function verifyPendingOrderAuthorization($identifier, $type) } else { - if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('DNS challenge for \'' . $identifier . '\' tested, found invalid.', 'function verifyPendingOrderAuthorization'); + if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('DNS challenge for \'' . $identifier . '\' tested locally, found invalid.', 'function verifyPendingOrderAuthorization'); } break; } @@ -629,7 +630,7 @@ public function revokeCertificate($reason = 0) preg_match('~-----BEGIN\sCERTIFICATE-----(.*)-----END\sCERTIFICATE-----~s', $certificate, $matches); $certificate = trim(LEFunctions::Base64UrlSafeEncode(base64_decode(trim($matches[1])))); - $sign = $this->connector->signRequestJWK(array('certificate' => $certificate, 'reason' => $reason), $this->connector->revokeCert); + $sign = $this->connector->signRequestJWK(array('certificate' => $certificate, 'reason' => $reason), $this->connector->revokeCert, $this->certificateKeys['private_key']); $post = $this->connector->post($this->connector->revokeCert, $sign); if(strpos($post['header'], "200 OK") !== false) { diff --git a/composer.json b/composer.json index d5cb090..7028170 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,6 @@ "type": "library", "description": "PHP LetsEncrypt client library for ACME v2", "require": { - "php": "^7.1.0" + "php": "^5.2" } }