/** * Sends a signed request to the Let's Encrypt API. * * All requests except the directory request go through this method. * * @since 1.0.0 * @access public * * @param string $endpoint The endpoint to send a request to. * @param array $data Data to send with the request. * @return string|array|WP_Error Either a JSON-decoded array response, a plain text response or an error object. */ public function signed_request($endpoint, $data = null) { $account_keypair = AccountKeyPair::get(); $account_key_resource = $account_keypair->read_private(); if (is_wp_error($account_key_resource)) { return $account_key_resource; } $account_key_details = $account_keypair->get_private_details(); if (is_wp_error($account_key_details)) { return $account_key_details; } $protected = $header = array('alg' => 'RS256', 'jwk' => array('kty' => 'RSA', 'n' => Util::base64_url_encode($account_key_details['rsa']['n']), 'e' => Util::base64_url_encode($account_key_details['rsa']['e']))); if (null !== ($nonce = $this->get_last_nonce())) { $protected['nonce'] = $nonce; } else { $this->directory(); if (null !== ($nonce = $this->get_last_nonce())) { $protected['nonce'] = $nonce; } } if (!isset($protected['nonce'])) { return new WP_Error('signed_request_no_nonce', __('No nonce available for a signed request.', 'wp-encrypt')); } $data64 = Util::base64_url_encode(str_replace('\\/', '/', json_encode($data))); $protected64 = Util::base64_url_encode(json_encode($protected)); $sign_status = openssl_sign($protected64 . '.' . $data64, $signature, $account_key_resource, 'SHA256'); if (false === $sign_status) { return new WP_Error('private_key_cannot_sign', sprintf(__('Could not sign request with private key. Original error message: %s', 'wp-encrypt'), openssl_error_string())); } $signature64 = Util::base64_url_encode($signature); return $this->request($endpoint, 'POST', array('header' => $header, 'protected' => $protected64, 'payload' => $data64, 'signature' => $signature64)); }
/** * Validates a domain with Let's Encrypt through a HTTP challenge. * * @since 1.0.0 * @access public * @static * * @param string $domain The domain to validate. * @param array $account_key_details The account private key details. * @return bool|WP_Error True if the domain was successfully validated, an error object otherwise. */ public static function validate($domain, $account_key_details) { $filesystem = Util::get_filesystem(); $status = Util::maybe_create_letsencrypt_challenges_dir(); if (is_wp_error($status)) { return $status; } $client = Client::get(); $response = $client->auth($domain); if (is_wp_error($response)) { return $response; } $challenge = array_reduce($response['challenges'], function ($v, $w) { if ($v) { return $v; } if ('http-01' === $w['type']) { return $w; } return false; }); if (!$challenge) { return new WP_Error('no_challenge_available', sprintf(__('No HTTP challenge available for domain %1$s. Original response: %2$s', 'wp-encrypt'), $domain, json_encode($response))); } $location = $client->get_last_location(); $directory = Util::get_letsencrypt_challenges_dir_path(); $token_path = $directory . '/' . $challenge['token']; if (!$filesystem->is_dir($directory) && !$filesystem->mkdir($directory, 0755, true)) { return new WP_Error('challenge_cannot_create_dir', sprintf(__('Could not create challenge directory <code>%s</code>. Please check your filesystem permissions.', 'wp-encrypt'), $directory)); } $header = array('e' => Util::base64_url_encode($account_key_details['rsa']['e']), 'kty' => 'RSA', 'n' => Util::base64_url_encode($account_key_details['rsa']['n'])); $data = $challenge['token'] . '.' . Util::base64_url_encode(hash('sha256', json_encode($header), true)); if (false === $filesystem->put_contents($token_path, $data)) { return new WP_Error('challenge_cannot_write_file', sprintf(__('Could not write challenge to file <code>%s</code>. Please check your filesystem permissions.', 'wp-encrypt'), $token_path)); } $filesystem->chmod($token_path, 0644); $response = wp_remote_get(Util::get_letsencrypt_challenges_dir_url() . '/' . $challenge['token']); if (is_wp_error($response)) { $filesystem->delete($token_path); return new WP_Error('challenge_request_failed', sprintf(__('Challenge request failed for domain %s.', 'wp-encrypt'), $domain)); } if ($data !== trim(wp_remote_retrieve_body($response))) { $filesystem->delete($token_path); return new WP_Error('challenge_self_check_failed', sprintf(__('Challenge self check failed for domain %s.', 'wp-encrypt'), $domain)); } $result = $client->challenge($challenge['uri'], $challenge['token'], $data); $done = false; do { if (empty($result['status']) || 'invalid' === $result['status']) { $filesystem->delete($token_path); return new WP_Error('challenge_remote_check_failed', sprintf(__('Challenge remote check failed for domain %s.', 'wp-encrypt'), $domain)); } $done = 'pending' !== $result['status']; if (!$done) { sleep(1); } $result = $client->request($location, 'GET'); if ('invalid' === $result['status']) { $filesystem->delete($token_path); return new WP_Error('challenge_remote_check_failed', sprintf(__('Challenge remote check failed for domain %s.', 'wp-encrypt'), $domain)); } } while (!$done); $filesystem->delete($token_path); return true; }