Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cfcb575fa | ||
|
|
fffaec6d84 | ||
|
|
1bc25c738c | ||
|
|
9c8d67b2e9 | ||
|
|
251a47ebaa | ||
|
|
a33ac65a77 | ||
|
|
746bf84adb | ||
|
|
ff6e14e2de |
@@ -13,6 +13,7 @@ cache:
|
||||
install:
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer self-update
|
||||
- composer config --global discard-changes true
|
||||
- if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.5" ]]; then composer require --dev --no-update phpunit/phpunit ^4; fi
|
||||
- composer require satooshi/php-coveralls dev-master --dev --no-update
|
||||
- composer update --ignore-platform-reqs
|
||||
|
||||
157
README.md
157
README.md
@@ -1,154 +1,9 @@
|
||||
# acme
|
||||

|
||||
|
||||

|
||||

|
||||
`kelunik/acme-client` is an ACME client written in PHP. ACME is the protocol that powers the [Let's Encrypt](https://letsencrypt.org) certificate authority.
|
||||
|
||||
`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.
|
||||
## Documentation
|
||||
|
||||
> **Warning**: This software is under development. Use at your own risk.
|
||||
|
||||
## Installation
|
||||
|
||||
**Requirements**
|
||||
|
||||
* PHP 5.5+
|
||||
* Composer
|
||||
|
||||
**Instructions using the Phar**
|
||||
|
||||
```bash
|
||||
# Go to https://github.com/kelunik/acme-client/releases/latest
|
||||
# Download the latest release archive
|
||||
|
||||
# Run it.
|
||||
chmod +x acme-client.phar
|
||||
./acme-client.phar
|
||||
|
||||
# Or install it globally
|
||||
mv ./acme-client.phar /usr/local/bin/acme-client
|
||||
```
|
||||
|
||||
If you want to update, just replace the old phar with a new one.
|
||||
|
||||
All commands require an additional `--storage` argument when using the phar. That's the path where your keys and certificates will be stored.
|
||||
On Unix you could use something like `--storage /etc/acme`.
|
||||
|
||||
**Instructions using Composer**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
> **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.
|
||||
|
||||
By using this client you agree to any agreement and any further updates by continued usage.
|
||||
You're responsible to react to updates and stop the automation if you no longer agree with the terms of service.
|
||||
|
||||
```
|
||||
bin/acme setup -s letsencrypt --email me@example.com
|
||||
```
|
||||
|
||||
`-s` / `--server` can either be a URI or a shortcut. Available shortcuts:
|
||||
* `letsencrypt` / `letsencrypt:production`
|
||||
* `letsencrypt:staging`
|
||||
|
||||
After a successful registration you're able to issue certificates.
|
||||
This client assumes you have a HTTP server setup and running.
|
||||
You must have a document root setup in order to use this client.
|
||||
|
||||
```
|
||||
bin/acme issue -s letsencrypt -d example.com:www.example.com -p /var/www/example.com
|
||||
```
|
||||
|
||||
To revoke a certificate, you need a valid account key currently, just like for issuance.
|
||||
|
||||
```
|
||||
bin/acme revoke --name example.com -s letsencrypt
|
||||
```
|
||||
|
||||
For renewal, there's the `bin/acme check` subcommand.
|
||||
It exists with a non-zero exit code, if the certificate is going to expire soon.
|
||||
Default check time is 30 days, but you can use `--ttl` to customize it.
|
||||
|
||||
You may use this as daily cron:
|
||||
|
||||
```
|
||||
bin/acme check --name example.com --ttl 30 -s letsencrypt || bin/acme issue ...
|
||||
```
|
||||
|
||||
You can also use a more advanced script to automatically reload the server as well.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd /git/kelunik/acme-client
|
||||
|
||||
bin/acme check --name example.com --ttl 30 -s letsencrypt
|
||||
|
||||
if [ $? -eq 1 ]; then
|
||||
bin/acme issue -d example.com:www.example.com -p /var/www -s letsencrypt
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
nginx -t -q
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
nginx -s reload
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
```
|
||||
* [Installation](./doc/installation.md)
|
||||
* [Usage](./doc/usage.md)
|
||||
* [Migration guide for 0.1.x → 0.2.x](./doc/migrations/0.2.0.md)
|
||||
|
||||
27
bin/acme
27
bin/acme
@@ -21,6 +21,21 @@ HELP;
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if (!function_exists("openssl_pkey_get_private")) {
|
||||
echo <<<HELP
|
||||
|
||||
____ __________ ___ ___
|
||||
/ __ `/ ___/ __ `__ \/ _ \
|
||||
/ /_/ / /__/ / / / / / __/
|
||||
\__,_/\___/_/ /_/ /_/\___/
|
||||
|
||||
You need to enable OpenSSL in your php.ini
|
||||
|
||||
|
||||
HELP;
|
||||
exit(-2);
|
||||
}
|
||||
|
||||
require __DIR__ . "/../vendor/autoload.php";
|
||||
|
||||
$commands = [
|
||||
@@ -30,8 +45,10 @@ $commands = [
|
||||
"revoke",
|
||||
];
|
||||
|
||||
$help = implode("\n ", array_map(function ($command) {
|
||||
return "bin/acme {$command}";
|
||||
$binary = \Kelunik\AcmeClient\getBinary();
|
||||
|
||||
$help = implode("\n ", array_map(function ($command) use ($binary) {
|
||||
return "{$binary} {$command}";
|
||||
}, $commands));
|
||||
|
||||
$help = <<<EOT
|
||||
@@ -90,12 +107,12 @@ try {
|
||||
$climate->arguments->parse(array_values($args));
|
||||
} catch (Exception $e) {
|
||||
if (count($argv) === 3 && in_array($argv[2], ["h", "-h", "--help", "help"], true)) {
|
||||
$climate->usage(["bin/acme {$argv[1]}"]);
|
||||
$climate->usage(["{$binary} {$argv[1]}"]);
|
||||
$climate->br();
|
||||
|
||||
exit(0);
|
||||
} else {
|
||||
$climate->usage(["bin/acme {$argv[1]}"]);
|
||||
$climate->usage(["{$binary} {$argv[1]}"]);
|
||||
$climate->br();
|
||||
|
||||
$climate->error($e->getMessage());
|
||||
@@ -128,7 +145,7 @@ Amp\run(function () use ($command, $climate) {
|
||||
$exitCode = (yield $command->execute($climate->arguments));
|
||||
|
||||
if ($exitCode === null) {
|
||||
$logger->warning("Invalid exit code: null, falling back to 0. Please consider reporting this as bug.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
exit($exitCode);
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"kelunik/certificate": "^1",
|
||||
"league/climate": "^3",
|
||||
"rdlowrey/auryn": "^1",
|
||||
"webmozart/assert": "^1"
|
||||
"webmozart/assert": "^1",
|
||||
"symfony/yaml": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5",
|
||||
@@ -47,7 +48,7 @@
|
||||
"compression": "GZip",
|
||||
"name": "acme-client.phar",
|
||||
"output-dir": "build",
|
||||
"include": ["src", "vendor/kelunik/acme/res"],
|
||||
"include": ["src", "vendor/kelunik/acme/res", "vendor/amphp/socket/var"],
|
||||
"entry-point": "bin/acme"
|
||||
}
|
||||
}
|
||||
|
||||
66
doc/installation.md
Normal file
66
doc/installation.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Installation
|
||||
|
||||
## Installation using Phar
|
||||
|
||||
This is the preferred installation method for usage on a production system.
|
||||
|
||||
### Requirements
|
||||
|
||||
* PHP 5.5+
|
||||
|
||||
### Instructions
|
||||
|
||||
```bash
|
||||
# Go to https://github.com/kelunik/acme-client/releases/latest
|
||||
# Download the latest release archive.
|
||||
|
||||
# Make it executable.
|
||||
chmod +x acme-client.phar
|
||||
|
||||
# Run it.
|
||||
./acme-client.phar
|
||||
|
||||
# Or install it globally.
|
||||
mv ./acme-client.phar /usr/local/bin/acme-client
|
||||
acme-client
|
||||
```
|
||||
|
||||
If you want to update, just replace the old `.phar` with a new one.
|
||||
|
||||
All commands require a `--storage` argument when using the Phar. That's the path where your keys and certificates will be stored.
|
||||
On Unix you could use something like `--storage /etc/acme`.
|
||||
|
||||
You can add a file named `acme-client.yml` next to the `.phar` with the two keys `storage` and `server`.
|
||||
These values will be used as default if you don't specify them, but you can still use another server by explicitly adding it as argument.
|
||||
|
||||
```yml
|
||||
# Sample YAML configuration.
|
||||
|
||||
# Storage directory for certificates and keys.
|
||||
storage: /etc/acme
|
||||
|
||||
# Server to use. Available shortcuts: letsencrypt, letsencrypt:staging
|
||||
# You can also use full URLs to the directory resource of an ACME server
|
||||
server: letsencrypt
|
||||
```
|
||||
|
||||
## Installation using Composer
|
||||
|
||||
If you plan to actively develop this client, you probably don't want the Phar but install the dependencies using Composer.
|
||||
|
||||
### Requirements
|
||||
|
||||
* PHP 5.5+
|
||||
* [Composer](https://getcomposer.org/)
|
||||
|
||||
### Instructions
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/kelunik/acme-client && cd acme-client
|
||||
|
||||
# Install dependencies
|
||||
composer install
|
||||
```
|
||||
|
||||
You can use `./bin/acme` as script instead of the Phar. Please note, that all data will be stored in `./data` as long as you don't provide the `--storage` argument.
|
||||
43
doc/migrations/0.2.0.md
Normal file
43
doc/migrations/0.2.0.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Migration from 0.1.x to 0.2.x
|
||||
|
||||
If you used this client before `0.2.0`, you have a different directory structure than the current one. If you want to upgrade, but keep all your data, here's a migration guide.
|
||||
|
||||
```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
|
||||
# Alternatively have a look at the new installation instructions and use the Phar
|
||||
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.
|
||||
```
|
||||
79
doc/usage.md
Normal file
79
doc/usage.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Usage
|
||||
|
||||
**The client stores all data in `./data` if you're using the Composer installation method, otherwise in the directory you configured. 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 the Let's Encrypt certificate authority, there's a [subscriber agreement](https://letsencrypt.org/repository/) you have to accept.
|
||||
|
||||
By using this client you agree to any agreement and any further updates by continued usage.
|
||||
You're responsible to react to updates and stop the automation if you no longer agree with the terms of service.
|
||||
|
||||
These usage instructions assume you have installed the client globally as a Phar. If you are using the Phar, but don't have it globally, replace `acme-client` with the location to your Phar.
|
||||
|
||||
If you're using the client with Composer, replace `acme-client` with `bin/acme`. You have to specify the server with `-s` / `--server`, because there's currently no config file support for this installation method.
|
||||
|
||||
## Register an Account
|
||||
|
||||
```
|
||||
acme-client setup --email me@example.com
|
||||
```
|
||||
|
||||
After a successful registration you're able to issue certificates.
|
||||
This client assumes you have a HTTP server setup and running.
|
||||
You must have a document root setup in order to use this client.
|
||||
|
||||
## Issue a Certificate
|
||||
|
||||
```
|
||||
acme-client issue -d example.com:www.example.com -p /var/www/example.com
|
||||
```
|
||||
|
||||
You can separate multiple domains (`-d`) with `,`, `:` or `;`. You can separate multiple document roots (`-p`) with your system's path separator:
|
||||
* Colon (`:`) for Unix
|
||||
* Semicolon (`;`) for Windows
|
||||
|
||||
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`.
|
||||
|
||||
## Revoke a Certificate
|
||||
|
||||
To revoke a certificate, you need a valid account key, just like for issuance.
|
||||
|
||||
```
|
||||
acme-client revoke --name example.com
|
||||
```
|
||||
|
||||
`--name` is the common name of the certificate that you want to revoke.
|
||||
|
||||
## Renewing a Certificate
|
||||
|
||||
For renewal, there's the `acme-client check` subcommand.
|
||||
It exists with a non-zero exit code, if the certificate is going to expire soon.
|
||||
Default check time is 30 days, but you can use `--ttl` to customize it.
|
||||
|
||||
You may use this as daily cron:
|
||||
|
||||
```
|
||||
acme-client check --name example.com || acme-client issue ...
|
||||
```
|
||||
|
||||
You can also use a more advanced script to automatically reload the server as well. For this example we assume you're using Nginx. Something similar should work for Apache.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
acme-client check --name example.com --ttl 30
|
||||
|
||||
if [ $? -eq 1 ]; then
|
||||
acme-client issue -d example.com:www.example.com -p /var/www
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
nginx -t -q
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
nginx -s reload
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
```
|
||||
BIN
res/logo-avatar.png
Normal file
BIN
res/logo-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
res/logo.png
Normal file
BIN
res/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -2,7 +2,10 @@
|
||||
|
||||
namespace Kelunik\AcmeClient;
|
||||
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Phar;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
function suggestCommand($badCommand, array $commands, $suggestThreshold = 70) {
|
||||
@@ -74,15 +77,52 @@ function normalizePath($path) {
|
||||
function getArgumentDescription($argument) {
|
||||
$isPhar = \Kelunik\AcmeClient\isPhar();
|
||||
|
||||
$config = [];
|
||||
|
||||
if ($isPhar) {
|
||||
$configPath = substr(dirname(Phar::running(true)), strlen("phar://")) . "/acme-client.yml";
|
||||
|
||||
if (file_exists($configPath)) {
|
||||
$configContent = file_get_contents($configPath);
|
||||
|
||||
try {
|
||||
$value = Yaml::parse($configContent);
|
||||
|
||||
if (isset($value["server"]) && is_string($value["server"])) {
|
||||
$config["server"] = $value["server"];
|
||||
unset($value["server"]);
|
||||
}
|
||||
|
||||
if (isset($value["storage"]) && is_string($value["storage"])) {
|
||||
$config["storage"] = $value["storage"];
|
||||
unset($value["storage"]);
|
||||
}
|
||||
|
||||
if (!empty($value)) {
|
||||
throw new AcmeException("Provided YAML file had unknown options: " . implode(", ", array_keys($value)));
|
||||
}
|
||||
} catch (ParseException $e) {
|
||||
throw new AcmeException("Unable to parse the YAML file ({$configPath}): " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch ($argument) {
|
||||
case "server":
|
||||
return [
|
||||
$argument = [
|
||||
"prefix" => "s",
|
||||
"longPrefix" => "server",
|
||||
"description" => "ACME server to use for registration and issuance of certificates.",
|
||||
"required" => true,
|
||||
];
|
||||
|
||||
if (isset($config["server"])) {
|
||||
$argument["required"] = false;
|
||||
$argument["defaultValue"] = $config["server"];
|
||||
}
|
||||
|
||||
return $argument;
|
||||
|
||||
case "storage":
|
||||
$argument = [
|
||||
"longPrefix" => "storage",
|
||||
@@ -92,6 +132,9 @@ function getArgumentDescription($argument) {
|
||||
|
||||
if (!$isPhar) {
|
||||
$argument["defaultValue"] = dirname(__DIR__) . "/data";
|
||||
} else if (isset($config["storage"])) {
|
||||
$argument["required"] = false;
|
||||
$argument["defaultValue"] = $config["storage"];
|
||||
}
|
||||
|
||||
return $argument;
|
||||
@@ -99,4 +142,31 @@ function getArgumentDescription($argument) {
|
||||
default:
|
||||
throw new \InvalidArgumentException("Unknown argument: " . $argument);
|
||||
}
|
||||
}
|
||||
|
||||
function getBinary() {
|
||||
$binary = "bin/acme";
|
||||
|
||||
if (isPhar()) {
|
||||
$binary = substr(Phar::running(true), strlen("phar://"));
|
||||
|
||||
$path = getenv("PATH");
|
||||
$locations = explode(PATH_SEPARATOR, $path);
|
||||
|
||||
$binaryPath = dirname($binary);
|
||||
|
||||
foreach ($locations as $location) {
|
||||
if ($location === $binaryPath) {
|
||||
return substr($binary, strlen($binaryPath) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
$cwd = getcwd();
|
||||
|
||||
if ($cwd && strpos($binary, $cwd) === 0) {
|
||||
$binary = "." . substr($binary, strlen($cwd));
|
||||
}
|
||||
}
|
||||
|
||||
return $binary;
|
||||
}
|
||||
Reference in New Issue
Block a user