version . " (support@griffin.software)")) throw new Exception("cURL set user agent option failed"); if (!curl_setopt($ch, CURLOPT_RETURNTRANSFER, true)) throw new Exception("cURL set return transfer option failed"); if (isset($payload)) { if ($payload === "") $payload = null; if (!isset($nonce)) { $response = $this->sendRequest($this->acmeDirectory["newNonce"], 204); if (!isset($nonce)) throw new Exception("get new nonce failed"); } $protected = [ "url" => $url, "alg" => "RS256", "nonce" => $nonce ]; if (isset($jwk)) $protected["jwk"] = $jwk; else $protected["kid"] = $this->account["url"]; $protected = $this->encodeBase64($this->encodeJSON($protected)); $payload = $this->encodeBase64($this->encodeJSON($payload)); if (openssl_sign("$protected.$payload", $signature, $this->account["privateKey"], "sha256WithRSAEncryption") === false) throw new Exception("openssl signing failed"); $signature = $this->encodeBase64($signature); $josejson = $this->encodeJSON([ "protected" => $protected, "payload" => $payload, "signature" => $signature ]); if (!curl_setopt($ch, CURLOPT_POSTFIELDS, $josejson)) throw new Exception("cURL set post fields option failed"); if (!curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/jose+json", "Content-Length: " . strlen($josejson)])) throw new Exception("cURL set http header option failed"); } if (!curl_setopt($ch, CURLOPT_HEADER, true)) throw new Exception("cURL set header option failed"); if (!curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$headers, &$headerSize) { $headers[] = $header; $length = strlen($header); $headerSize += $length; return $length; })) throw new Exception("cURL set header function option failed"); $body = curl_exec($ch); if ($body === false) throw new Exception("cURL execution failed: $url"); $this->responses[] = $body; $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($responseCode === false) throw new Exception("cURL get response code info failed"); /* $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); if ($headerSize === false) throw new Exception("cURL get header size info failed"); */ $length = strlen($body); if ($headerSize > $length) throw new Exception("improper response header size"); $body = $headerSize == $length ? "" : substr($body, $headerSize); if ($body === false) throw new Exception("could not truncate headers from response"); $response = [ "headers" => $headers, "body" => $body ]; if ($responseCode !== $expectedResponseCode) { if ($this->findHeader($response, "content-type", false) === "application/problem+json") { $problem = $this->decodeJSON($response["body"]); if (isset($problem["type"], $problem["detail"])) throw new Exception($problem["type"] . ": " . $problem["detail"]); } throw new Exception("unexpected response code"); } $nonce = $this->findHeader($response, "replay-nonce", false); return $response; } public function dumpResponses($fileName) { // *** DUMP BOULDER RESPONSES *** $this->writeFile($this->dataDirectory . "/$fileName", implode("\n\n-----\n\n", array_reverse($this->responses)), 0600); } public function __construct($dataDirectory) { // *** VERSION *** $this->version = "1.0.0"; // *** ESTABLISH DATA DIRECTORY *** $this->createDirectory($dataDirectory, 0700); $this->dataDirectory = $dataDirectory; $filePath = $dataDirectory . "/" . "code.txt"; // *** READ CODE *** $this->code = $this->readFile($filePath); // *** WRITE CODE *** $this->writeFile($filePath, $this->encodeBase64(random_bytes(12)), 0600); } public function initialize($environment) { // *** ESTABLISH ENVIRONMENT *** switch ($environment) { case "production": $url = "https://acme-v02.api.letsencrypt.org/directory"; $filePath = $this->dataDirectory . "/" . "account.json"; break; case "staging": $url = "https://acme-staging-v02.api.letsencrypt.org/directory"; $filePath = $this->dataDirectory . "/" . "account-staging.json"; break; default: throw new Exception("unknown environment"); } // *** GET ACME DIRECTORY *** $response = $this->sendRequest($url, 200); $this->acmeDirectory = $this->decodeJSON($response["body"]); // *** READ ACME ACCOUNT DATA *** $this->account = $this->decodeJSON($this->readFile($filePath)); if (isset($this->account["privateKey"], $this->account["thumbprint"], $this->account["url"])) return; $this->account = []; // *** GENERATE ACME ACCOUNT PRIVATE KEY *** $configargs = [ "digest_alg" => "sha256", "private_key_bits" => 2048, "private_key_type" => OPENSSL_KEYTYPE_RSA ]; $privateKeyObject = openssl_pkey_new($configargs); if ($privateKeyObject === false) throw new Exception("account private key generate failed"); if (!openssl_pkey_export($privateKeyObject, $privateKey)) throw new Exception("account private key export failed"); $this->account["privateKey"] = $privateKey; // *** CALCULATE JWK *** $privateKeyDetails = openssl_pkey_get_details($privateKeyObject); if ($privateKeyDetails === false) throw new Exception("account private key get details failed"); $jwk = [ "e" => $this->encodeBase64($privateKeyDetails["rsa"]["e"]), // public exponent "kty" => "RSA", "n" => $this->encodeBase64($privateKeyDetails["rsa"]["n"]) // modulus ]; // *** CALCULATE THUMBPRINT *** $digest = openssl_digest($this->encodeJSON($jwk), "sha256", true); if ($digest === false) throw new Exception("JWK digest failed"); $thumbprint = $this->encodeBase64($digest); $this->account["thumbprint"] = $thumbprint; // *** REGISTER NEW ACME ACCOUNT *** $url = $this->acmeDirectory["newAccount"]; $payload = [ "termsOfServiceAgreed" => true ]; // 201 on create, 200 on find $response = $this->sendRequest($url, 201, $payload, $jwk); $url = $this->findHeader($response, "location"); $this->account["url"] = $url; // *** WRITE ACME ACCOUNT DATA *** $this->writeFile($filePath, $this->encodeJSON($this->account), 0600); } public function updateEmailAddresses($emailAddresses) { if (!isset($this->acmeDirectory, $this->account)) throw new Exception("environment not established"); // *** UPDATE EMAIL ADDRESSES *** $url = $this->account["url"]; $contact = []; foreach ($emailAddresses as $emailAddress) $contact[] = "mailto:$emailAddress"; $payload = [ "contact" => $contact ]; $response = $this->sendRequest($url, 200, $payload); } public function acquireCertificate($domainNames) { if (!isset($this->acmeDirectory, $this->account)) throw new Exception("environment not established"); // *** CREATE NEW ORDER *** $url = $this->acmeDirectory["newOrder"]; $identifiers = []; foreach ($domainNames as $domainName) $identifiers[] = [ "type" => "dns", "value" => $domainName ]; $payload = [ "identifiers" => $identifiers ]; $response = $this->sendRequest($url, 201, $payload); $orderurl = $this->findHeader($response, "location"); $order = $this->decodeJSON($response["body"]); // *** GET CHALLENGES *** $authorizationurls = []; $challengeurls = []; $challengetokens = []; $payload = ""; // empty foreach ($order["authorizations"] as $url) { $response = $this->sendRequest($url, 200, $payload); $authorization = $this->decodeJSON($response["body"]); if ($authorization["status"] === "valid") continue; $authorizationurls[] = $url; foreach ($authorization["challenges"] as $challenge) { if ($challenge["type"] !== "http-01") continue; $challengeurls[] = $challenge["url"]; $challengetokens[] = $challenge["token"]; continue 2; } throw new Exception("no http-01 challenge found"); } // *** WRITE HTTP-01 CHALLENGE FILES *** $this->createDirectory("./.well-known", 0755); $this->createDirectory("./.well-known/acme-challenge", 0755); try { foreach ($challengetokens as $challengetoken) $this->writeFile("./.well-known/acme-challenge/$challengetoken", "$challengetoken." . $this->account["thumbprint"], 0644); // *** CONFIRM CHALLENGES *** sleep(2); $payload = (object)[]; // empty object foreach ($challengeurls as $url) $challenge = $this->sendRequest($url, 200, $payload); // *** CHECK AUTHORIZATIONS *** $payload = ""; // empty foreach ($authorizationurls as $url) { for ($attempt = 1; true; ++$attempt) { sleep(1); $response = $this->sendRequest($url, 200, $payload); $authorization = $this->decodeJSON($response["body"]); if ($authorization["status"] !== "pending") break; if ($attempt == 10) throw new Exception("authorization still pending after $attempt attempts"); } if ($authorization["status"] !== "valid") throw new Exception("authorization failed"); } } finally { // *** DELETE HTTP-01 CHALLENGE FILES *** foreach ($challengetokens as $challengetoken) $this->deleteFile("./.well-known/acme-challenge/$challengetoken"); } // *** GENERATE CERTIFICATE PRIVATE KEY *** $configargs = [ "digest_alg" => "sha256", "private_key_bits" => 2048, "private_key_type" => OPENSSL_KEYTYPE_RSA ]; $privateKeyObject = openssl_pkey_new($configargs); if ($privateKeyObject === false) throw new Exception("certificate private key generate failed"); if (!openssl_pkey_export($privateKeyObject, $privateKey)) throw new Exception("certificate private key export failed"); // *** GENERATE CSR *** $dn = [ "commonName" => $domainNames[0] ]; $configargs = [ "digest_alg" => "sha256", "config" => $this->dataDirectory . "/openssl.cnf" ]; $opensslcnf = "[req]\n" . "distinguished_name = req_distinguished_name\n" . "req_extensions = v3_req\n\n" . "[req_distinguished_name]\n\n" . "[v3_req]\n" . "subjectAltName = @san\n\n" . "[san]\n"; $i = 0; foreach ($domainNames as $domainName) { ++$i; $opensslcnf .= "DNS.$i = $domainName\n"; } try { $this->writeFile($this->dataDirectory . "/openssl.cnf", $opensslcnf, 0600); $csrObject = openssl_csr_new($dn, $privateKey, $configargs); if ($csrObject === false) throw new Exception("csr generate failed"); } finally { $this->deleteFile($this->dataDirectory . "/openssl.cnf"); } if (!openssl_csr_export($csrObject, $csr)) throw new Exception("csr export failed"); // *** FINALIZE ORDER *** $url = $order["finalize"]; $regex = "~^-----BEGIN CERTIFICATE REQUEST-----([A-Za-z0-9+/]+)=?=?-----END CERTIFICATE REQUEST-----$~"; $outcome = preg_match($regex, str_replace("\n", "", $csr), $matches); if ($outcome === false) throw new Exception("regular expression match failed when extracting csr"); if ($outcome === 0) throw new Exception("csr format mismatch"); $payload = [ "csr" => strtr($matches[1], "+/", "-_") ]; $response = $this->sendRequest($url, 200, $payload); $order = $this->decodeJSON($response["body"]); if ($order["status"] !== "valid") { // *** CHECK ORDER *** $url = $orderurl; $payload = ""; // empty for ($attempt = 1; true; ++$attempt) { sleep(1); $response = $this->sendRequest($url, 200, $payload); $order = $this->decodeJSON($response["body"]); if (!( $order["status"] === "pending" || $order["status"] === "processing" || $order["status"] === "ready")) break; if ($attempt == 10) throw new Exception("order still pending after $attempt attempts"); } if ($order["status"] !== "valid") throw new Exception("order failed"); } // *** DOWNLOAD CERTIFICATE *** $url = $order["certificate"]; $payload = ""; // empty $response = $this->sendRequest($url, 200, $payload); $certificate = $response["body"]; // *** WRITE CERTIFICATE AND PRIVATE KEY *** $this->writeFile($this->dataDirectory . "/certificate.crt", $certificate, 0600); $this->writeFile($this->dataDirectory . "/private.key", $privateKey, 0600); } } try { $certsage = new CertSage($dataDirectory); if (!isset($_POST["action"])) $_POST["action"] = ""; if (!is_string($_POST["action"])) throw new Exception("action was not a string"); switch ($_POST["action"]) { case "": $page = "Input"; break; case "Proceed": // *** CHECK CODE *** if (!isset($_POST["code"])) throw new Exception("code was missing"); if (!is_string($_POST["code"])) throw new Exception("code was not a string"); if (strlen($_POST["code"]) == 0) throw new Exception("code was empty"); if ($_POST["code"] !== $certsage->code) throw new Exception("code was incorrect"); // *** PREPARE DOMAIN NAMES *** if (!isset($_POST["domainNames"])) throw new Exception("domainNames was missing"); if (!is_string($_POST["domainNames"])) throw new Exception("domainNames was not a string"); if (strlen($_POST["domainNames"]) == 0) throw new Exception("domainNames was empty"); $domainNames = []; for ($tok = strtok($_POST["domainNames"], "\r\n"); $tok !== false; $tok = strtok("\r\n")) { $tok = trim($tok); if (strlen($tok) == 0) continue; $domainNames[] = $tok; } // *** PREPARE EMAIL ADDRESS *** if (!isset($_POST["emailAddress"])) throw new Exception("emailAddress was missing"); if (!is_string($_POST["emailAddress"])) throw new Exception("emailAddress was not a string"); $emailAddresses = strlen($_POST["emailAddress"]) == 0 ? [] : [$_POST["emailAddress"]]; // *** PREPARE ENVIRONMENT *** if (!isset($_POST["environment"])) throw new Exception("environment was missing"); if (!is_string($_POST["environment"])) throw new Exception("environment was not a string"); $environment = $_POST["environment"]; // *** DO THE WORK *** $certsage->initialize($environment); if (!empty($emailAddresses)) $certsage->updateEmailAddresses($emailAddresses); $certsage->acquireCertificate($domainNames); $page = "Output"; break; default: throw new Exception("unknown action"); } } catch (Exception $e) { $error = $e->getMessage(); $page = "Error"; } finally { if (isset($certsage)) $certsage->dumpResponses("responses.txt"); } ?> CertSage
🧙🏼‍♂️ CertSage
version version ?>
support@griffin.software

Welcome!

This webpage is an ACME client that allows you to acquire free SSL/TLS certificates from the Let's Encrypt certificate authority. It works by satisfying an http-01 challenge for each fully qualified domain name covered by your certificate.


one per line, no wildcards (*)



For important Let's Encrypt notifications.
Staging and production environments have
separate accounts and stored addresses.
Leave empty to keep using stored address.



Please test using the staging environment
to avoid hitting the rate limits.




contents of


By proceeding you are agreeing to the
Let's Encrypt Subscriber Agreement

Success!

Your certificate and its private key have been saved in .

If you like free and easy SSL/TLS certificates, please consider donating to CertSage and Let's Encrypt using the links at the bottom of this page.

Click here to start over.

The test using the staging environment was successful.

If you want to acquire a trusted certificate, please use the production environment.

Click here to start over.

Troubles...

If you need help with resolving this issue, please post a topic in the help category of the Let's Encrypt Community.

Click here to start over.