Upgrade to ACME RFC (v2)
This commit is contained in:
16
bin/acme
16
bin/acme
@@ -1,10 +1,14 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Amp\Http\Client\HttpClientBuilder;
|
||||
use Amp\Loop;
|
||||
use Auryn\Injector;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
use Kelunik\AcmeClient\Commands\Command;
|
||||
use League\CLImate\CLImate;
|
||||
use function Kelunik\AcmeClient\getBinary;
|
||||
use function Kelunik\AcmeClient\suggestCommand;
|
||||
|
||||
$logo = <<<LOGO
|
||||
____ __________ ___ ___
|
||||
@@ -51,7 +55,7 @@ $commands = [
|
||||
'help' => 'Print this help information.',
|
||||
];
|
||||
|
||||
$binary = \Kelunik\AcmeClient\getBinary();
|
||||
$binary = getBinary();
|
||||
|
||||
$help = implode(PHP_EOL, array_map(function ($command) use ($commands) {
|
||||
$help = " <green>{$command}</green>\n";
|
||||
@@ -81,8 +85,8 @@ if (!in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID < 70000) {
|
||||
$climate->yellow("You're using an older version of PHP which is no longer supported by this client. Have a look at http://php.net/supported-versions.php and upgrade at least to PHP 7.0!");
|
||||
if (PHP_VERSION_ID < 70400) {
|
||||
$climate->yellow("You're using an older version of PHP which is no longer supported by this client. Have a look at https://php.net/supported-versions.php and upgrade at least to PHP 7.4!");
|
||||
$climate->br(2);
|
||||
}
|
||||
|
||||
@@ -94,7 +98,7 @@ if (count($argv) === 1 || in_array($argv[1], ['-h', 'help', '--help'], true)) {
|
||||
if (!array_key_exists($argv[1], $commands)) {
|
||||
$climate->error("Unknown command '{$argv[1]}'. Use --help for a list of available commands.");
|
||||
|
||||
$suggestion = \Kelunik\AcmeClient\suggestCommand($argv[1], array_keys($commands));
|
||||
$suggestion = suggestCommand($argv[1], array_keys($commands));
|
||||
|
||||
if ($suggestion) {
|
||||
$climate->br()->out(" Did you mean '$suggestion'?");
|
||||
@@ -105,7 +109,7 @@ if (!array_key_exists($argv[1], $commands)) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/** @var \Kelunik\AcmeClient\Commands\Command $class */
|
||||
/** @var string|Command $class */
|
||||
$class = "Kelunik\\AcmeClient\\Commands\\" . ucfirst($argv[1]);
|
||||
$definition = $class::getDefinition();
|
||||
|
||||
@@ -136,7 +140,7 @@ try {
|
||||
$injector = new Injector;
|
||||
$injector->share($climate);
|
||||
$injector->share(new AcmeFactory);
|
||||
$injector->share(new Amp\Artax\DefaultClient);
|
||||
$injector->share(HttpClientBuilder::buildDefault());
|
||||
|
||||
$command = $injector->make($class);
|
||||
$exitCode = 1;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.2.5"
|
||||
"php": "7.4.0"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
|
||||
974
composer.lock
generated
974
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,21 +10,19 @@ use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\Acme\Crypto\PrivateKey;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
use function Amp\ByteStream\getStderr;
|
||||
|
||||
class AcmeFactory
|
||||
{
|
||||
public function build(string $directory, PrivateKey $keyPair): AcmeService
|
||||
{
|
||||
$logger = null;
|
||||
if (\getenv('ACME_LOG')) {
|
||||
$handler = new StreamHandler(getStderr());
|
||||
$handler->setFormatter(new ConsoleFormatter(null, null, true, true));
|
||||
|
||||
$logger = new Logger('acme');
|
||||
$logger->pushProcessor(new PsrLogMessageProcessor);
|
||||
|
||||
$handler = new StreamHandler(new ResourceOutputStream(\STDERR));
|
||||
$handler->setFormatter(new ConsoleFormatter(null, null, true, true));
|
||||
$logger->pushHandler($handler);
|
||||
}
|
||||
|
||||
return new AcmeService(new AcmeClient($directory, $keyPair, null, null, null, $logger));
|
||||
return new AcmeService(new AcmeClient($directory, $keyPair, null, null, $logger), $logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ use Kelunik\Acme\Crypto\Backend\OpensslBackend;
|
||||
use Kelunik\Acme\Crypto\PrivateKey;
|
||||
use Kelunik\Acme\Crypto\RsaKeyGenerator;
|
||||
use Kelunik\Acme\Csr\OpensslCsrGenerator;
|
||||
use Kelunik\Acme\Protocol\Authorization;
|
||||
use Kelunik\Acme\Protocol\Challenge;
|
||||
use Kelunik\Acme\Protocol\Order;
|
||||
use Kelunik\Acme\Verifiers\Http01;
|
||||
use Kelunik\AcmeClient;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
@@ -133,12 +136,33 @@ class Issue implements Command
|
||||
$acme = $this->acmeFactory->build($server, $key);
|
||||
$concurrency = \min(20, \max($args->get('challenge-concurrency'), 1));
|
||||
|
||||
/** @var Order $order */
|
||||
$order = yield $acme->newOrder($domains);
|
||||
|
||||
/** @var \Throwable[] $errors */
|
||||
[$errors] = yield AcmeClient\concurrentMap(
|
||||
$concurrency,
|
||||
$domains,
|
||||
function ($domain, $i) use ($acme, $key, $docRoots, $user) {
|
||||
return $this->solveChallenge($acme, $key, $domain, $docRoots[$i], $user);
|
||||
$order->getAuthorizationUrls(),
|
||||
function ($authorizationUrl, $i) use ($acme, $key, $domains, $docRoots, $user) {
|
||||
/** @var Authorization $authorization */
|
||||
$authorization = yield $acme->getAuthorization($authorizationUrl);
|
||||
|
||||
if ($authorization->getIdentifier()->getType() !== 'dns') {
|
||||
throw new AcmeException('Invalid identifier: ' . $authorization->getIdentifier()->getType());
|
||||
}
|
||||
|
||||
$name = $authorization->getIdentifier()->getValue();
|
||||
if ($authorization->isWildcard()) {
|
||||
$name .= '*.';
|
||||
}
|
||||
|
||||
$index = \array_search($name, $domains, true);
|
||||
if ($index === false) {
|
||||
throw new AcmeException('Unknown identifier returned: ' . $name);
|
||||
}
|
||||
|
||||
return yield from $this->solveChallenge($acme, $key, $authorization, $domains[$i], $docRoots[$i],
|
||||
$user);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -150,6 +174,8 @@ class Issue implements Command
|
||||
throw new AcmeException('Issuance failed, not all challenges could be solved.');
|
||||
}
|
||||
|
||||
yield $acme->pollForOrderReady($order->getUrl());
|
||||
|
||||
$keyPath = 'certs/' . $keyFile . '/' . \reset($domains) . '/key.pem';
|
||||
$bits = $args->get('bits');
|
||||
|
||||
@@ -171,8 +197,13 @@ class Issue implements Command
|
||||
|
||||
$csr = yield (new OpensslCsrGenerator)->generateCsr($key, $domains);
|
||||
|
||||
$location = yield $acme->requestCertificate($csr);
|
||||
$certificates = yield $acme->pollForCertificate($location);
|
||||
yield $acme->finalizeOrder($order->getFinalizationUrl(), $csr);
|
||||
yield $acme->pollForOrderValid($order->getUrl());
|
||||
|
||||
/** @var Order $order */
|
||||
$order = yield $acme->getOrder($order->getUrl());
|
||||
|
||||
$certificates = yield $acme->downloadCertificates($order->getCertificateUrl());
|
||||
|
||||
$path = normalizePath($args->get('storage')) . '/certs/' . $keyFile;
|
||||
$certificateStore = new CertificateStore($path);
|
||||
@@ -191,20 +222,17 @@ class Issue implements Command
|
||||
private function solveChallenge(
|
||||
AcmeService $acme,
|
||||
PrivateKey $key,
|
||||
Authorization $authorization,
|
||||
string $domain,
|
||||
string $path,
|
||||
string $user = null
|
||||
): \Generator {
|
||||
[$location, $challenges] = yield $acme->requestChallenges($domain);
|
||||
$goodChallenges = $this->findSuitableCombination($challenges);
|
||||
|
||||
if (empty($goodChallenges)) {
|
||||
$httpChallenge = $this->findHttpChallenge($authorization);
|
||||
if ($httpChallenge === null) {
|
||||
throw new AcmeException("Couldn't find any combination of challenges which this client can solve!");
|
||||
}
|
||||
|
||||
$challenge = $challenges->challenges[\reset($goodChallenges)];
|
||||
$token = $challenge->token;
|
||||
|
||||
$token = $httpChallenge->getToken();
|
||||
if (!\preg_match('#^[a-zA-Z0-9-_]+$#', $token)) {
|
||||
throw new AcmeException('Protocol violation: Invalid Token!');
|
||||
}
|
||||
@@ -219,8 +247,8 @@ class Issue implements Command
|
||||
yield $challengeStore->put($token, $payload, $user);
|
||||
|
||||
yield (new Http01)->verifyChallenge($domain, $token, $payload);
|
||||
yield $acme->answerChallenge($challenge->uri, $payload);
|
||||
yield $acme->pollForChallenge($location);
|
||||
yield $acme->finalizeChallenge($httpChallenge->getUrl());
|
||||
yield $acme->pollForAuthorization($authorization->getUrl());
|
||||
|
||||
$this->climate->comment(" {$domain} is now authorized.");
|
||||
} finally {
|
||||
@@ -246,24 +274,16 @@ class Issue implements Command
|
||||
}
|
||||
}
|
||||
|
||||
private function findSuitableCombination(\stdClass $response): array
|
||||
private function findHttpChallenge(Authorization $authorization): ?Challenge
|
||||
{
|
||||
$challenges = $response->challenges ?? [];
|
||||
$combinations = $response->combinations ?? [];
|
||||
$goodChallenges = [];
|
||||
$challenges = $authorization->getChallenges();
|
||||
|
||||
foreach ($challenges as $i => $challenge) {
|
||||
if ($challenge->type === 'http-01') {
|
||||
$goodChallenges[] = $i;
|
||||
foreach ($challenges as $challenge) {
|
||||
if ($challenge->getType() === 'http-01') {
|
||||
return $challenge;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($goodChallenges as $i => $challenge) {
|
||||
if (!\in_array([$challenge], $combinations, true)) {
|
||||
unset($goodChallenges[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
return $goodChallenges;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use Amp\Dns\Record;
|
||||
use Amp\Promise;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Kelunik\Acme\Crypto\RsaKeyGenerator;
|
||||
use Kelunik\Acme\Domain\Registration;
|
||||
use Kelunik\Acme\Protocol\Account;
|
||||
use Kelunik\AcmeClient;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
@@ -33,6 +33,12 @@ class Setup implements Command
|
||||
'description' => 'E-mail for important issues, will be sent to the ACME server.',
|
||||
'required' => true,
|
||||
],
|
||||
'agree-terms' => [
|
||||
'longPrefix' => 'agree-terms',
|
||||
'description' => 'Agree to terms of service of the configured ACME server.',
|
||||
'defaultValue' => false,
|
||||
'noValue' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$configPath = AcmeClient\getConfigPath();
|
||||
@@ -90,19 +96,17 @@ class Setup implements Command
|
||||
|
||||
$this->climate->whisper(' Registering with ' . \substr($server, 8) . ' ...');
|
||||
|
||||
/** @var Registration $registration */
|
||||
$registration = yield $acme->register($email);
|
||||
$this->climate->info(' Registration successful. Contacts: ' . \implode(
|
||||
', ',
|
||||
$registration->getContact()
|
||||
));
|
||||
/** @var Account $account */
|
||||
$account = yield $acme->register($email, $args->get('agree-terms'));
|
||||
$contacts = \implode(', ', \array_map("strval", $account->getContacts()));
|
||||
$this->climate->info(' Registration successful. Contacts: ' . $contacts);
|
||||
$this->climate->br();
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
private function checkEmail(string $email)
|
||||
private function checkEmail(string $email): \Generator
|
||||
{
|
||||
$host = \substr($email, \strrpos($email, '@') + 1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user