private function doExecute(Manager $args) : Generator { if (posix_geteuid() !== 0) { throw new AcmeException("Please run this script as root!"); } $server = $args->get("server"); $protocol = substr($server, 0, strpos("://", $server)); if (!$protocol || $protocol === $server) { $server = "https://" . $server; } elseif ($protocol !== "https") { throw new \InvalidArgumentException("Invalid server protocol, only HTTPS supported"); } $keyPair = $this->checkRegistration($args); $acme = new AcmeService(new AcmeClient($server, $keyPair), $keyPair); $this->logger->info("Revoking certificate ..."); $pem = (yield get($args->get("cert"))); $cert = new Certificate($pem); if ($cert->getValidTo() < time()) { $this->logger->warning("Certificate did already expire, no need to revoke it."); return; } $this->logger->info("Certificate was valid for: " . implode(", ", $cert->getNames())); (yield $acme->revokeCertificate($pem)); $this->logger->info("Certificate has been revoked."); }
/** * Register user at ACME * * @throws \Kelunik\Acme\AcmeException */ public function register() { try { \amp\wait($this->acme->register($this->email)); } catch (\Exception $e) { throw new \Exception('Error registering ' . $this->email . ': ' . $e->getMessage(), 0, $e); } $this->config('status', 'registered at Let\'s Encrypt'); $this->config('email', $this->email); }
public function doExecute(Manager $args) : Generator { if (posix_geteuid() !== 0) { throw new AcmeException("Please run this script as root!"); } $email = $args->get("email"); (yield resolve($this->checkEmail($email))); $server = $args->get("server"); $protocol = substr($server, 0, strpos("://", $server)); if (!$protocol || $protocol === $server) { $server = "https://" . $server; } elseif ($protocol !== "https") { throw new \InvalidArgumentException("Invalid server protocol, only HTTPS supported"); } $identity = str_replace(["/", "%"], "-", substr($server, 8)); $path = __DIR__ . "/../../data/accounts"; $pathPrivate = "{$path}/{$identity}.private.key"; $pathPublic = "{$path}/{$identity}.public.key"; if ((yield exists($pathPrivate)) && (yield exists($pathPublic))) { $this->logger->info("Loading existing keys ..."); $private = file_get_contents($pathPrivate); $public = file_get_contents($pathPublic); $keyPair = new KeyPair($private, $public); } else { $this->logger->info("Generating key keys ..."); $keyPair = (new OpenSSLKeyGenerator())->generate(4096); if (!file_exists($path) && !mkdir($path, 0700, true)) { throw new AcmeException("Couldn't create account directory"); } file_put_contents($pathPrivate, $keyPair->getPrivate()); file_put_contents($pathPublic, $keyPair->getPublic()); chmod($pathPrivate, 600); chmod($pathPrivate, 600); } $acme = new AcmeService(new AcmeClient($server, $keyPair), $keyPair); $this->logger->info("Registering with ACME server " . substr($server, 8) . " ..."); /** @var Registration $registration */ $registration = (yield $acme->register($email)); $this->logger->notice("Registration successful with contact " . json_encode($registration->getContact())); }
private function doExecute(Manager $args) : Generator { if (posix_geteuid() !== 0) { throw new AcmeException("Please run this script as root!"); } $user = $args->get("user") ?? "www-data"; $server = $args->get("server"); $protocol = substr($server, 0, strpos("://", $server)); if (!$protocol || $protocol === $server) { $server = "https://" . $server; } elseif ($protocol !== "https") { throw new \InvalidArgumentException("Invalid server protocol, only HTTPS supported"); } $domains = $args->get("domains"); $domains = array_map("trim", explode(",", $domains)); yield from $this->checkDnsRecords($domains); $keyPair = $this->checkRegistration($args); $acme = new AcmeService(new AcmeClient($server, $keyPair), $keyPair); foreach ($domains as $domain) { list($location, $challenges) = (yield $acme->requestChallenges($domain)); $goodChallenges = $this->findSuitableCombination($challenges); if (empty($goodChallenges)) { throw new AcmeException("Couldn't find any combination of challenges which this server can solve!"); } $challenge = $challenges->challenges[reset($goodChallenges)]; $token = $challenge->token; if (!preg_match("#^[a-zA-Z0-9-_]+\$#", $token)) { throw new AcmeException("Protocol Violation: Invalid Token!"); } $this->logger->debug("Generating payload..."); $payload = $acme->generateHttp01Payload($token); $docRoot = rtrim($args->get("path") ?? __DIR__ . "/../../data/public", "/\\"); $path = $docRoot . "/.well-known/acme-challenge"; try { if (!file_exists($docRoot)) { throw new AcmeException("Document root doesn't exist: " . $docRoot); } if (!file_exists($path) && !@mkdir($path, 0770, true)) { throw new AcmeException("Couldn't create public dir to serve the challenges: " . $path); } if (!($userInfo = posix_getpwnam($user))) { throw new AcmeException("Unknown user: "******"/.well-known", $userInfo["uid"]); chown($docRoot . "/.well-known/acme-challenge", $userInfo["uid"]); $this->logger->info("Providing payload for {$domain} at {$path}/{$token}"); file_put_contents("{$path}/{$token}", $payload); chown("{$path}/{$token}", $userInfo["uid"]); chmod("{$path}/{$token}", 0660); (yield $acme->selfVerify($domain, $token, $payload)); $this->logger->info("Successfully self-verified challenge."); (yield $acme->answerChallenge($challenge->uri, $payload)); $this->logger->info("Answered challenge... waiting"); (yield $acme->pollForChallenge($location)); $this->logger->info("Challenge successful. {$domain} is now authorized."); @unlink("{$path}/{$token}"); } catch (Throwable $e) { // no finally because generators... @unlink("{$path}/{$token}"); throw $e; } } $path = __DIR__ . "/../../data/live/" . reset($domains); if (!file_exists($path) && !mkdir($path, 0700, true)) { throw new AcmeException("Couldn't create directory: {$path}"); } if (file_exists($path . "/private.pem") && file_exists($path . "/public.pem")) { $private = file_get_contents($path . "/private.pem"); $public = file_get_contents($path . "/public.pem"); $this->logger->info("Using existing domain key found at {$path}"); $domainKeys = new KeyPair($private, $public); } else { $domainKeys = (new OpenSSLKeyGenerator())->generate(2048); file_put_contents($path . "/private.pem", $domainKeys->getPrivate()); file_put_contents($path . "/public.pem", $domainKeys->getPublic()); $this->logger->info("Saved new domain key at {$path}"); chmod($path . "/private.pem", 0600); chmod($path . "/public.pem", 0600); } $this->logger->info("Requesting certificate ..."); $location = (yield $acme->requestCertificate($domainKeys, $domains)); $certificates = (yield $acme->pollForCertificate($location)); $this->logger->info("Saving certificate ..."); file_put_contents($path . "/cert.pem", reset($certificates)); file_put_contents($path . "/fullchain.pem", implode("\n", $certificates)); array_shift($certificates); file_put_contents($path . "/chain.pem", implode("\n", $certificates)); $this->logger->info("Successfully issued certificate."); }
/** * Register user at ACME * * @throws \Kelunik\Acme\AcmeException */ public function register() { $this->acme->register($this->email); $this->config('status', 'registered at Let\'s Encrypt'); $this->config('email', $this->email); }
/** * @param AcmeService $acme * @param KeyPair $keyPair * @param $domain * @return \Generator * @throws AcmeException * @throws \Exception * @throws \Throwable */ private function solveChallenge(AcmeService $acme, KeyPair $keyPair, $domain) { list($location, $challenges) = (yield $acme->requestChallenges($domain)); $goodChallenges = $this->findSuitableCombination($challenges); if (empty($goodChallenges)) { throw new AcmeException("Couldn't find any combination of challenges which this client can solve!"); } $challenge = $challenges->challenges[reset($goodChallenges)]; $token = $challenge->token; if (!preg_match("#^[a-zA-Z0-9-_]+\$#", $token)) { throw new AcmeException("Protocol violation: Invalid Token!"); } $payload = $acme->generateHttp01Payload($keyPair, $token); $challengeStore = $this->getChallengeStorage(); try { $challengeStore->put($token, $payload); (yield $acme->verifyHttp01Challenge($domain, $token, $payload)); (yield $acme->answerChallenge($challenge->uri, $payload)); (yield $acme->pollForChallenge($location)); $challengeStore->delete($token); } catch (\Exception $e) { // no finally because generators... $challengeStore->delete($token); throw $e; } catch (\Throwable $e) { // no finally because generators... $challengeStore->delete($token); throw $e; } }
/** * Register user at ACME * * @throws \Kelunik\Acme\AcmeException */ public function register() { $this->acme->register($this->email); }
private function solveChallenge(AcmeService $acme, KeyPair $keyPair, $domain, $path) { list($location, $challenges) = (yield $acme->requestChallenges($domain)); $goodChallenges = $this->findSuitableCombination($challenges); if (empty($goodChallenges)) { throw new AcmeException("Couldn't find any combination of challenges which this client can solve!"); } $challenge = $challenges->challenges[reset($goodChallenges)]; $token = $challenge->token; if (!preg_match("#^[a-zA-Z0-9-_]+\$#", $token)) { throw new AcmeException("Protocol violation: Invalid Token!"); } $payload = $acme->generateHttp01Payload($keyPair, $token); $this->climate->whisper(" Providing payload at http://{$domain}/.well-known/acme-challenge/{$token}"); $challengeStore = new ChallengeStore($path); try { (yield $challengeStore->put($token, $payload, isset($user) ? $user : null)); (yield $acme->verifyHttp01Challenge($domain, $token, $payload)); (yield $acme->answerChallenge($challenge->uri, $payload)); (yield $acme->pollForChallenge($location)); $this->climate->comment(" {$domain} is now authorized."); (yield $challengeStore->delete($token)); } catch (Exception $e) { // no finally because generators... (yield $challengeStore->delete($token)); throw $e; } catch (Throwable $e) { // no finally because generators... (yield $challengeStore->delete($token)); throw $e; } }
private function doEncrypt(string $acmeServer, array $contact) : Generator { $domain = strtok(str_replace("https://", "", $acmeServer), "/"); $info = $this->host->export(); $dns = $info["name"]; $this->mkdirs($this->path . "/accounts/{$domain}", $this->path . "/keys/{$dns}", $this->path . "/live", $this->path . "/challenges/{$dns}/.well-known/acme-challenge"); $this->host->use(root($this->path . "/challenges/{$dns}")); $this->host->use($this); $accountKeyPair = (yield $this->loadKeyPair($this->path . "/accounts/{$domain}")); $domainKeyPair = (yield $this->loadKeyPair($this->path . "/keys/{$dns}")); $certificateService = new AcmeService(new AcmeClient($acmeServer, $accountKeyPair), $accountKeyPair, new AcmeAdapter($this->path)); list($selfSigned, $lifetime) = (yield $certificateService->getCertificateData($dns)); if ($lifetime > 30 * 24 * 60 * 60 && !$selfSigned) { // valid for more than 30 days and not self signed $this->host->encrypt($this->path . "/live/{$dns}.pem"); // TODO Add timer to renew certificate! return; } try { (yield put($this->path . "/live/{$dns}.lock", $dns)); } catch (FilesystemException $e) { } $lock = new Lock($this->path . "/live/{$dns}.lock"); try { $lock->acquire(); if ($lifetime < 1 || $selfSigned) { // don't touch valid certificate here if still in place. $privateKey = openssl_pkey_get_private($domainKeyPair->getPrivate()); $csr = openssl_csr_new(["commonName" => $dns, "organizationName" => "kelunik/aerys-acme"], $privateKey, ["digest_alg" => "sha256"]); if (!$csr) { throw new AcmeException("CSR couldn't be generated!"); } $privateCertificate = openssl_csr_sign($csr, null, $privateKey, 90, ["digest_alg" => "sha256"], random_int(0, PHP_INT_MAX)); openssl_x509_export($privateCertificate, $cert); file_put_contents($this->path . "/live/{$dns}.pem", implode("\n", [$domainKeyPair->getPrivate(), $cert])); } $this->host->encrypt($this->path . "/live/{$dns}.pem"); $this->onBoot = function (Server $server) use($certificateService, $dns, $contact, $lock) { $certificateService->issueCertificate($dns, $contact, $this->agreement)->when(function (Throwable $error = null) use($server, $lock, $dns) { $lock->release(); unlink($this->path . "/live/{$dns}.lock"); if ($error) { $this->logger->emergency($error); // $server->stop(); } }); }; } catch (LockException $e) { do { (yield new Pause(500)); } while (!(yield exists($this->path . "/live/{$dns}.pem"))); $this->host->encrypt($this->path . "/live/{$dns}.pem"); } }