Author: Niklas Keller (me@kelunik.com)
Exemple #1
0
 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.");
 }
Exemple #2
0
 /**
  * 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()));
 }
Exemple #4
0
 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);
 }
Exemple #6
0
 /**
  * @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;
     }
 }
Exemple #7
0
 /**
  * Register user at ACME
  *
  * @throws \Kelunik\Acme\AcmeException
  */
 public function register()
 {
     $this->acme->register($this->email);
 }
Exemple #8
0
 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;
     }
 }
Exemple #9
0
 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");
     }
 }