diff --git a/README.md b/README.md index 8f43c9a..db66c7d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ `kelunik/acme-client` is a standalone ACME client written in PHP. It's an alternative for the [official client](https://github.com/letsencrypt/letsencrypt) which is written in python. -> **Warning**: This software is under heavy development. Use at your own risk. Revocation is not yet supported by this client. +> **Warning**: This software is under heavy development. Use at your own risk. ## Installation @@ -18,6 +18,9 @@ composer install ## Usage +> **Note**: This client stores all data in `./data`, be sure to backup this folder regularly. +> It contains your account keys, domain keys and certificates. + Before you can issue certificates, you have to register an account first and read and understand the terms of service of the ACME CA you're using. For Let's Encrypt there's a [subscriber agreement](https://letsencrypt.org/repository/) you have to accept. @@ -41,4 +44,12 @@ sudo bin/acme issue \ --path /var/www/example.com ``` -For renewal, just run this command again. \ No newline at end of file +For renewal, just run this command again. + +To revoke a certificate, you need a valid account key currently, just like for issuance. + +``` +sudo bin/acme revoke \ + --server acme-v01.api.letsencrypt.org/directory \ + --cert data/live/example.com/cert.pem +``` \ No newline at end of file diff --git a/composer.json b/composer.json index 04ed83d..3809941 100644 --- a/composer.json +++ b/composer.json @@ -5,9 +5,9 @@ "php": ">=7.0.0", "ext-posix": "*", "ext-openssl": "*", - "amphp/aerys": "dev-master", "bramus/monolog-colored-line-formatter": "^2", "kelunik/acme": "dev-master", + "kelunik/certificate": "dev-master", "league/climate": "^3", "monolog/monolog": "^1.17", "psr/log": "^1", diff --git a/src/Commands/Revoke.php b/src/Commands/Revoke.php index 144ffeb..6927a66 100644 --- a/src/Commands/Revoke.php +++ b/src/Commands/Revoke.php @@ -3,20 +3,97 @@ namespace Kelunik\AcmeClient\Commands; use Amp\Promise; +use Generator; +use Kelunik\Acme\AcmeClient; use Kelunik\Acme\AcmeException; +use Kelunik\Acme\AcmeService; +use Kelunik\Acme\KeyPair; +use Kelunik\Certificate\Certificate; use League\CLImate\Argument\Manager; +use Psr\Log\LoggerInterface; +use function Amp\File\exists; +use function Amp\File\get; +use function Amp\resolve; class Revoke implements Command { + private $logger; + + public function __construct(LoggerInterface $logger) { + $this->logger = $logger; + } + public function execute(Manager $args): Promise { - throw new AcmeException("Command not yet implemented!"); + return resolve($this->doExecute($args)); + } + + 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."); + } + + private function checkRegistration(Manager $args) { + $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 (file_exists($pathPrivate) && file_exists($pathPublic)) { + $private = file_get_contents($pathPrivate); + $public = file_get_contents($pathPublic); + + $this->logger->info("Found account keys."); + + return new KeyPair($private, $public); + } + + throw new AcmeException("No registration found for server, please register first"); } public static function getDefinition(): array { return [ - "domains" => [ - "prefix" => "d", - "longPrefix" => "domains", - "description" => "Domains to request a certificate for.", + "cert" => [ + "prefix" => "c", + "longPrefix" => "cert", + "description" => "Certificate to be revoked.", "required" => true, ], "server" => [