Compare commits
19 Commits
v0.3.0-alp
...
v0.3.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35070bb70a | ||
|
|
6da46ddaf6 | ||
|
|
51acff5bd3 | ||
|
|
ea3e9dc68c | ||
|
|
6f01055884 | ||
|
|
297e1aa9b1 | ||
|
|
b7cfe3c0f1 | ||
|
|
4053094860 | ||
|
|
a80b7b8497 | ||
|
|
f13b0856c7 | ||
|
|
d4f2009315 | ||
|
|
2b4a200263 | ||
|
|
256aa76011 | ||
|
|
69bc88daf1 | ||
|
|
19f6550e33 | ||
|
|
ed3da3c98d | ||
|
|
56955155fe | ||
|
|
e3d7723da3 | ||
|
|
0ae207fce3 |
@@ -28,7 +28,10 @@ certificates:
|
||||
# user: User running the web server. Challenge files are world readable,
|
||||
# but some servers might require to be owner of files they serve.
|
||||
#
|
||||
# rekey: Regenerate certificate key pairs even if a key pair already exists.
|
||||
#
|
||||
- bits: 4096
|
||||
rekey: true
|
||||
paths:
|
||||
/var/www/example:
|
||||
- example.org
|
||||
|
||||
11
.travis.yml
11
.travis.yml
@@ -12,15 +12,14 @@ cache:
|
||||
|
||||
install:
|
||||
- phpenv config-rm xdebug.ini || true
|
||||
- composer config --global discard-changes true
|
||||
- composer update
|
||||
- composer require satooshi/php-coveralls dev-master --dev
|
||||
- composer show --installed
|
||||
- composer install
|
||||
|
||||
script:
|
||||
- find -name "*.php" -not -path "./vendor/*" -print0 | xargs -n 1 -0 php -l
|
||||
- phpdbg -qrr vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
|
||||
- php vendor/bin/php-cs-fixer --diff --dry-run -v fix
|
||||
- PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix
|
||||
|
||||
after_script:
|
||||
- php vendor/bin/coveralls -v
|
||||
- curl -OL https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.0/coveralls.phar
|
||||
- chmod +x coveralls.phar
|
||||
- ./coveralls.phar -v
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 Niklas Keller
|
||||
Copyright (c) 2015-2018 Niklas Keller
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
14
bin/acme
14
bin/acme
@@ -1,12 +1,10 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Amp\File\BlockingDriver;
|
||||
use Amp\Loop;
|
||||
use Auryn\Injector;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
use League\CLImate\CLImate;
|
||||
use function Amp\File\filesystem;
|
||||
|
||||
$logo = <<<LOGO
|
||||
____ __________ ___ ___
|
||||
@@ -107,9 +105,6 @@ if (!array_key_exists($argv[1], $commands)) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Use blocking driver for now, as amphp/parallel doesn't work inside PHARs
|
||||
filesystem(new BlockingDriver);
|
||||
|
||||
/** @var \Kelunik\AcmeClient\Commands\Command $class */
|
||||
$class = "Kelunik\\AcmeClient\\Commands\\" . ucfirst($argv[1]);
|
||||
$definition = $class::getDefinition();
|
||||
@@ -144,8 +139,9 @@ $injector->share(new AcmeFactory);
|
||||
$injector->share(new Amp\Artax\DefaultClient);
|
||||
|
||||
$command = $injector->make($class);
|
||||
$exitCode = 1;
|
||||
|
||||
Loop::run(function () use ($command, $climate) {
|
||||
Loop::run(function () use ($command, $climate, &$exitCode) {
|
||||
$handler = function ($e) use ($climate) {
|
||||
$error = (string) $e;
|
||||
$lines = explode("\n", $error);
|
||||
@@ -164,13 +160,13 @@ Loop::run(function () use ($command, $climate) {
|
||||
$exitCode = yield $command->execute($climate->arguments);
|
||||
|
||||
if ($exitCode === null) {
|
||||
exit(0);
|
||||
$exitCode = 0;
|
||||
}
|
||||
|
||||
exit($exitCode);
|
||||
} catch (Throwable $e) {
|
||||
$handler($e);
|
||||
}
|
||||
|
||||
Loop::stop();
|
||||
});
|
||||
|
||||
exit($exitCode);
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
"require": {
|
||||
"php": ">=7",
|
||||
"ext-openssl": "*",
|
||||
"amphp/process": "^0.2",
|
||||
"amphp/process": "^0.3.3",
|
||||
"amphp/parallel": "^0.2.5",
|
||||
"kelunik/acme": "^0.5",
|
||||
"kelunik/certificate": "^1",
|
||||
"league/climate": "^3.2",
|
||||
"rdlowrey/auryn": "^1.4.2",
|
||||
"webmozart/assert": "^1.2",
|
||||
"symfony/yaml": "^3.0"
|
||||
"symfony/yaml": "^3.0",
|
||||
"amphp/log": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^6",
|
||||
@@ -41,6 +43,11 @@
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.0.0"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"phar-builder": {
|
||||
"compression": "GZip",
|
||||
|
||||
919
composer.lock
generated
919
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ You can separate multiple domains (`-d`) with `,`, `:` or `;`. You can separate
|
||||
|
||||
If you specify less paths than domains, the last one will be used for the remaining domains.
|
||||
|
||||
Please note that Let's Encrypt has rate limits. Currently it's five certificates per domain per seven days. If you combine multiple subdomains in a single certificate, they count as just one certificate. If you just want to test things out, you can use their staging server, which has way higher rate limits by appending `--s letsencrypt:staging`.
|
||||
Please note that Let's Encrypt has rate limits. Currently it's five certificates per domain per seven days. If you combine multiple subdomains in a single certificate, they count as just one certificate. If you just want to test things out, you can use their staging server, which has way higher rate limits by appending `--server letsencrypt:staging`.
|
||||
|
||||
## Revoke a Certificate
|
||||
|
||||
|
||||
@@ -53,7 +53,10 @@ certificates:
|
||||
# user: User running the web server. Challenge files are world readable,
|
||||
# but some servers might require to be owner of files they serve.
|
||||
#
|
||||
# rekey: Regenerate certificate key pairs even if a key pair already exists.
|
||||
#
|
||||
- bits: 4096
|
||||
rekey: true
|
||||
paths:
|
||||
/var/www/example:
|
||||
- example.org
|
||||
|
||||
@@ -2,12 +2,27 @@
|
||||
|
||||
namespace Kelunik\AcmeClient;
|
||||
|
||||
use Amp\ByteStream\ResourceOutputStream;
|
||||
use Amp\Log\ConsoleFormatter;
|
||||
use Amp\Log\StreamHandler;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\Acme\Crypto\PrivateKey;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
class AcmeFactory {
|
||||
public function build(string $directory, PrivateKey $keyPair): AcmeService {
|
||||
return new AcmeService(new AcmeClient($directory, $keyPair));
|
||||
$logger = null;
|
||||
if (\getenv('ACME_LOG')) {
|
||||
$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, $logger));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,13 @@ class Auto implements Command {
|
||||
return self::EXIT_CONFIG_ERROR;
|
||||
}
|
||||
|
||||
foreach ($config['certificates'] as $certificateConfig) {
|
||||
if (isset($certificateConfig['rekey']) && !\is_bool($certificateConfig['rekey'])) {
|
||||
$this->climate->error("Config file ({$configPath}) defines an invalid 'rekey' value.");
|
||||
return self::EXIT_CONFIG_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
$concurrency = isset($config['challenge-concurrency']) ? (int) $config['challenge-concurrency'] : null;
|
||||
|
||||
$process = new Process([
|
||||
@@ -179,8 +186,7 @@ class Auto implements Command {
|
||||
$domainPathMap = $this->toDomainPathMap($certificate['paths']);
|
||||
$domains = \array_keys($domainPathMap);
|
||||
$commonName = \reset($domains);
|
||||
|
||||
$process = new Process([
|
||||
$processArgs = [
|
||||
PHP_BINARY,
|
||||
$GLOBALS['argv'][0],
|
||||
'check',
|
||||
@@ -192,7 +198,13 @@ class Auto implements Command {
|
||||
$commonName,
|
||||
'--names',
|
||||
\implode(',', $domains),
|
||||
]);
|
||||
];
|
||||
|
||||
if ($certificate['rekey'] ?? false) {
|
||||
$processArgs[] = '--rekey';
|
||||
}
|
||||
|
||||
$process = new Process($processArgs);
|
||||
|
||||
$process->start();
|
||||
$exit = yield $process->join();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Promise;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
@@ -96,26 +97,34 @@ class Issue implements Command {
|
||||
throw new AcmeException('Issuance failed, not all challenges could be solved.');
|
||||
}
|
||||
|
||||
$path = 'certs/' . $keyFile . '/' . \reset($domains) . '/key.pem';
|
||||
$keyPath = 'certs/' . $keyFile . '/' . \reset($domains) . '/key.pem';
|
||||
$bits = $args->get('bits');
|
||||
|
||||
$regenerateKey = $args->get('rekey');
|
||||
|
||||
try {
|
||||
$key = yield $keyStore->get($path);
|
||||
$key = yield $keyStore->get($keyPath);
|
||||
} catch (KeyStoreException $e) {
|
||||
$regenerateKey = true;
|
||||
}
|
||||
|
||||
if ($regenerateKey) {
|
||||
$this->climate->whisper(' Generating new key pair ...');
|
||||
$key = (new RsaKeyGenerator($bits))->generateKey();
|
||||
$key = yield $keyStore->put($path, $key);
|
||||
}
|
||||
|
||||
$this->climate->br();
|
||||
$this->climate->whisper(' Requesting certificate ...');
|
||||
|
||||
$csr = (new OpensslCsrGenerator)->generateCsr($key, $domains);
|
||||
$csr = yield (new OpensslCsrGenerator)->generateCsr($key, $domains);
|
||||
|
||||
$location = yield $acme->requestCertificate($csr);
|
||||
$certificates = yield $acme->pollForCertificate($location);
|
||||
|
||||
$path = AcmeClient\normalizePath($args->get('storage')) . '/certs/' . $keyFile;
|
||||
$certificateStore = new CertificateStore($path);
|
||||
|
||||
yield $keyStore->put($keyPath, $key);
|
||||
yield $certificateStore->put($certificates);
|
||||
|
||||
$this->climate->info(' Successfully issued certificate.');
|
||||
@@ -161,7 +170,10 @@ class Issue implements Command {
|
||||
}
|
||||
|
||||
private function checkDnsRecords(array $domains): \Generator {
|
||||
$promises = AcmeClient\concurrentMap(10, \array_combine($domains, $domains), 'Amp\Dns\resolve');
|
||||
$promises = AcmeClient\concurrentMap(10, $domains, function (string $domain): Promise {
|
||||
return Dns\resolve($domain);
|
||||
});
|
||||
|
||||
list($errors) = yield Promise\any($promises);
|
||||
|
||||
if ($errors) {
|
||||
@@ -228,6 +240,11 @@ class Issue implements Command {
|
||||
'defaultValue' => 10,
|
||||
'castTo' => 'int',
|
||||
],
|
||||
'rekey' => [
|
||||
'longPrefix' => 'rekey',
|
||||
'description' => 'Regenerate the key pair even if a key pair already exists.',
|
||||
'noValue' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,12 @@ class CertificateStore {
|
||||
$chain = \array_slice($certificates, 1);
|
||||
$path = $this->root . '/' . $commonName;
|
||||
|
||||
if (!yield File\isdir($path) && !yield File\mkdir($path, 0644, true) && !yield File\isdir($path)) {
|
||||
throw new FilesystemException("Couldn't create certificate directory: '{$path}'");
|
||||
if (!yield File\isdir($path)) {
|
||||
yield File\mkdir($path, 0755, true);
|
||||
|
||||
if (!yield File\isdir($path)) {
|
||||
throw new FilesystemException("Couldn't create certificate directory: '{$path}'");
|
||||
}
|
||||
}
|
||||
|
||||
yield File\put($path . '/cert.pem', $certificates[0]);
|
||||
|
||||
@@ -22,8 +22,12 @@ class ChallengeStore {
|
||||
throw new ChallengeStoreException("Document root doesn't exist: '{$this->docroot}'");
|
||||
}
|
||||
|
||||
if (!yield File\isdir($path) && !yield File\mkdir($path, 0644, true) && !yield File\isdir($path)) {
|
||||
throw new ChallengeStoreException("Couldn't create key directory: '{$path}'");
|
||||
if (!yield File\isdir($path)) {
|
||||
yield File\mkdir($path, 0755, true);
|
||||
|
||||
if (!yield File\isdir($path)) {
|
||||
throw new ChallengeStoreException("Couldn't create key directory: '{$path}'");
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && !$userInfo = \posix_getpwnam($user)) {
|
||||
@@ -35,13 +39,13 @@ class ChallengeStore {
|
||||
yield File\chown($this->docroot . '/.well-known/acme-challenge', $userInfo['uid'], -1);
|
||||
}
|
||||
|
||||
yield \Amp\File\put("{$path}/{$token}", $payload);
|
||||
yield File\put("{$path}/{$token}", $payload);
|
||||
|
||||
if ($userInfo !== null) {
|
||||
yield \Amp\File\chown("{$path}/{$token}", $userInfo['uid'], -1);
|
||||
yield File\chown("{$path}/{$token}", $userInfo['uid'], -1);
|
||||
}
|
||||
|
||||
yield \Amp\File\chmod("{$path}/{$token}", 0644);
|
||||
yield File\chmod("{$path}/{$token}", 0644);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +54,7 @@ class ChallengeStore {
|
||||
$path = $this->docroot . "/.well-known/acme-challenge/{$token}";
|
||||
|
||||
if (yield File\exists($path)) {
|
||||
yield \Amp\File\unlink($path);
|
||||
yield File\unlink($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,16 +18,21 @@ class KeyStore {
|
||||
public function get(string $path): Promise {
|
||||
return call(function () use ($path) {
|
||||
$file = $this->root . '/' . $path;
|
||||
$privateKey = yield File\get($file);
|
||||
|
||||
// Check key here to be valid, PrivateKey doesn't do that, we fail early here
|
||||
$res = \openssl_pkey_get_private($privateKey);
|
||||
try {
|
||||
$privateKey = yield File\get($file);
|
||||
|
||||
if ($res === false) {
|
||||
throw new KeyStoreException("Invalid private key: '{$file}'");
|
||||
// Check key here to be valid, PrivateKey doesn't do that, we fail early here
|
||||
$res = \openssl_pkey_get_private($privateKey);
|
||||
|
||||
if ($res === false) {
|
||||
throw new KeyStoreException("Invalid private key: '{$file}'");
|
||||
}
|
||||
|
||||
return new PrivateKey($privateKey);
|
||||
} catch (FilesystemException $e) {
|
||||
throw new KeyStoreException("Key not found: '{$file}'");
|
||||
}
|
||||
|
||||
return new PrivateKey($privateKey);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,8 +43,12 @@ class KeyStore {
|
||||
try {
|
||||
$dir = \dirname($file);
|
||||
|
||||
if (!yield File\isdir($dir) && !yield File\mkdir($dir, 0644, true) && !yield File\isdir($dir)) {
|
||||
throw new FilesystemException("Couldn't create key directory: '{$path}'");
|
||||
if (!yield File\isdir($dir)) {
|
||||
yield File\mkdir($dir, 0755, true);
|
||||
|
||||
if (!yield File\isdir($dir)) {
|
||||
throw new FilesystemException("Couldn't create key directory: '{$dir}'");
|
||||
}
|
||||
}
|
||||
|
||||
yield File\put($file, $key->toPem());
|
||||
|
||||
Reference in New Issue
Block a user