Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88629772c4 | ||
|
|
82578e4370 | ||
|
|
063eebf76d | ||
|
|
9348c2f4ad | ||
|
|
2629d2bfc7 | ||
|
|
9265ed6464 | ||
|
|
a9c29ac6de | ||
|
|
fe0454ab44 | ||
|
|
27bcd0d1c2 | ||
|
|
3318b227ab | ||
|
|
6bfa882aa0 | ||
|
|
19bc2af47d | ||
|
|
61b463ad70 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/build/
|
||||
/data/
|
||||
/vendor/
|
||||
/composer.lock
|
||||
61
README.md
61
README.md
@@ -8,14 +8,65 @@ It's an alternative for the [official client](https://github.com/letsencrypt/let
|
||||
|
||||
> **Warning**: This software is under development. Use at your own risk.
|
||||
|
||||
The client has been updated on Mar 12th in a non-backwards compatible manner. Please review the changes or use a new clone.
|
||||
|
||||
## Installation
|
||||
|
||||
**Requirements**
|
||||
|
||||
* PHP 5.5+
|
||||
* Composer
|
||||
|
||||
**Instructions**
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/kelunik/acme-client && cd acme-client
|
||||
|
||||
# Checkout latest release
|
||||
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Install dependencies
|
||||
composer install --no-dev
|
||||
```
|
||||
git clone https://github.com/kelunik/acme-client
|
||||
cd acme-client
|
||||
composer install
|
||||
|
||||
## Migration from 0.1.x to 0.2.x
|
||||
|
||||
```bash
|
||||
# Start in ./data
|
||||
cd data
|
||||
|
||||
# Move your account key to new location:
|
||||
|
||||
mkdir accounts
|
||||
mv account/key.pem accounts/acme-v01.api.letsencrypt.org.directory.pem
|
||||
# or accounts/acme-staging.api.letsencrypt.org.directory.pem if it's a staging key
|
||||
|
||||
# account should now be empty or contain just a config.json, you can delete the folder then
|
||||
rm -rf account
|
||||
|
||||
# Migrate certificates to new location:
|
||||
|
||||
cd certs
|
||||
mkdir acme-v01.api.letsencrypt.org.directory
|
||||
|
||||
# Move all your certificate directories
|
||||
# Repeat for all directories!
|
||||
mv example.com acme-v01.api.letsencrypt.org.directory
|
||||
# or acme-staging.api.letsencrypt.org.directory
|
||||
|
||||
# Delete all config.json files which may exist
|
||||
find -name "config.json" | xargs rm
|
||||
|
||||
# Update to current version
|
||||
git checkout master && git pull
|
||||
|
||||
# Check out latest release
|
||||
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Update dependencies
|
||||
composer update --no-dev
|
||||
|
||||
# Reconfigure your webserver to use the new paths
|
||||
# and check (and fix) your automation commands.
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"name": "kelunik/acme-client",
|
||||
"description": "Standalone PHP ACME client.",
|
||||
"keywords": [
|
||||
"ACME",
|
||||
"letsencrypt",
|
||||
"certificate",
|
||||
"https",
|
||||
"encryption",
|
||||
"ssl",
|
||||
"tls"
|
||||
],
|
||||
"require": {
|
||||
"amphp/process": "^0.1.1",
|
||||
"bramus/monolog-colored-line-formatter": "^2",
|
||||
"ext-posix": "*",
|
||||
"ext-openssl": "*",
|
||||
"kelunik/acme": "^0.3",
|
||||
"kelunik/certificate": "^1",
|
||||
@@ -33,6 +41,16 @@
|
||||
]
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5"
|
||||
"phpunit/phpunit": "^5",
|
||||
"macfja/phar-builder": "^0.2.3"
|
||||
},
|
||||
"extra": {
|
||||
"phar-builder": {
|
||||
"compression": "GZip",
|
||||
"name": "acme.phar",
|
||||
"output-dir": "build",
|
||||
"include": ["bin", "src", "vendor"],
|
||||
"entry-point": "bin/acme"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Amp\CombinatorException;
|
||||
use Amp\Dns\Record;
|
||||
use Exception;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\Acme\KeyPair;
|
||||
use Kelunik\Acme\OpenSSLKeyGenerator;
|
||||
use Kelunik\AcmeClient\Stores\CertificateStore;
|
||||
use Kelunik\AcmeClient\Stores\ChallengeStore;
|
||||
@@ -43,9 +45,25 @@ class Issue implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
$domains = array_map("trim", explode(":", $args->get("domains")));
|
||||
$domains = array_map("trim", explode(":", str_replace(",", ":", $args->get("domains"))));
|
||||
yield \Amp\resolve($this->checkDnsRecords($domains));
|
||||
|
||||
$docRoots = explode(":", str_replace("\\", "/", $args->get("path")));
|
||||
$docRoots = array_map(function($root) {
|
||||
return rtrim($root, "/");
|
||||
}, $docRoots);
|
||||
|
||||
if (count($domains) < count($docRoots)) {
|
||||
throw new AcmeException("Specified more document roots than domains.");
|
||||
}
|
||||
|
||||
if (count($domains) > count($docRoots)) {
|
||||
$docRoots = array_merge(
|
||||
$docRoots,
|
||||
array_fill(count($docRoots), count($domains) - count($docRoots), end($docRoots))
|
||||
);
|
||||
}
|
||||
|
||||
$keyStore = new KeyStore(dirname(dirname(__DIR__)) . "/data");
|
||||
|
||||
$server = \Kelunik\AcmeClient\resolveServer($args->get("server"));
|
||||
@@ -61,51 +79,20 @@ class Issue implements Command {
|
||||
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair));
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
list($location, $challenges) = (yield $acme->requestChallenges($domain));
|
||||
$goodChallenges = $this->findSuitableCombination($challenges);
|
||||
$promises = [];
|
||||
|
||||
if (empty($goodChallenges)) {
|
||||
throw new AcmeException("Couldn't find any combination of challenges which this client can solve!");
|
||||
foreach ($domains as $i => $domain) {
|
||||
$promises[] = \Amp\resolve($this->solveChallenge($acme, $keyPair, $domain, $docRoots[$i]));
|
||||
}
|
||||
|
||||
list($errors) = (yield \Amp\any($promises));
|
||||
|
||||
if (!empty($errors)) {
|
||||
foreach ($errors as $error) {
|
||||
$this->logger->error($error->getMessage());
|
||||
}
|
||||
|
||||
$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($keyPair, $token);
|
||||
|
||||
$this->logger->info("Providing payload at http://{$domain}/.well-known/acme-challenge/{$token}");
|
||||
$docRoot = rtrim(str_replace("\\", "/", $args->get("path")), "/");
|
||||
|
||||
$challengeStore = new ChallengeStore($docRoot);
|
||||
|
||||
try {
|
||||
$challengeStore->put($token, $payload, isset($user) ? $user : null);
|
||||
|
||||
yield $acme->verifyHttp01Challenge($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.");
|
||||
|
||||
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;
|
||||
}
|
||||
throw new AcmeException("Issuance failed, not all challenges could be solved.");
|
||||
}
|
||||
|
||||
$path = "certs/" . $keyFile . "/" . reset($domains) . "/key.pem";
|
||||
@@ -130,6 +117,53 @@ class Issue implements Command {
|
||||
$this->logger->info("Successfully issued certificate, see {$path}/" . reset($domains));
|
||||
}
|
||||
|
||||
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!");
|
||||
}
|
||||
|
||||
$this->logger->debug("Generating payload...");
|
||||
$payload = $acme->generateHttp01Payload($keyPair, $token);
|
||||
|
||||
$this->logger->info("Providing payload at http://{$domain}/.well-known/acme-challenge/{$token}");
|
||||
|
||||
|
||||
$challengeStore = new ChallengeStore($path);
|
||||
|
||||
try {
|
||||
$challengeStore->put($token, $payload, isset($user) ? $user : null);
|
||||
|
||||
yield $acme->verifyHttp01Challenge($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.");
|
||||
|
||||
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 checkDnsRecords($domains) {
|
||||
$promises = [];
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Kelunik\AcmeClient\Stores;
|
||||
|
||||
use Amp\CoroutineResult;
|
||||
use Amp\File\FilesystemException;
|
||||
use InvalidArgumentException;
|
||||
use Kelunik\Certificate\Certificate;
|
||||
@@ -26,7 +27,8 @@ class CertificateStore {
|
||||
Assert::string($name, "Name must be a string. Got: %s");
|
||||
|
||||
try {
|
||||
return yield \Amp\File\get($this->root . "/" . $name . "/cert.pem");
|
||||
$contents = (yield \Amp\File\get($this->root . "/" . $name . "/cert.pem"));
|
||||
yield new CoroutineResult($contents);
|
||||
} catch (FilesystemException $e) {
|
||||
throw new CertificateStoreException("Failed to load certificate.", 0, $e);
|
||||
}
|
||||
@@ -88,4 +90,4 @@ class CertificateStore {
|
||||
|
||||
yield \Amp\File\rmdir($this->root . "/" . $name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user