mirror of
https://github.com/anikeen-com/yaac.git
synced 2026-03-13 13:46:10 +00:00
Initial commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.data
|
||||
index.php
|
||||
.idea
|
||||
vendor
|
||||
.store
|
||||
13
LICENSE.md
Normal file
13
LICENSE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
Copyright 2018 Afosto SaaS BV
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
140
README.md
Normal file
140
README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# yaac - Yet another ACME client
|
||||
|
||||
Written in PHP, this client aims to be a decoupled LetsEncrypt client, based on ACME V2.
|
||||
|
||||
## Decoupled from a filesystem or webserver
|
||||
|
||||
In stead of, for example writing the certificate to the disk under an nginx configuration, this client just returns the
|
||||
data (the certificate and private key).
|
||||
|
||||
## Why
|
||||
|
||||
Why whould I need this package? At Afosto we run our software in a multi tenant setup, as any other SaaS would do, and
|
||||
therefore we cannot make use of the many clients that are already out there.
|
||||
|
||||
Almost all clients are coupled to a type of webserver or a fixed (set of) domain(s). This package can be extremely
|
||||
useful in case you need to dynamically fetch and install certificates.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP7+
|
||||
- openssl
|
||||
- [Flysystem](http://flysystem.thephpleague.com/) (any adapter would do) - to store the Lets Encrypt account information
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
Getting started is easy. First install the client, then you need to construct a flysystem filesystem, instantiate the
|
||||
client and you can start requesting certificates.
|
||||
|
||||
### Installation
|
||||
|
||||
Installing this package is done easily with composer.
|
||||
```bash
|
||||
composer require afosto/yaac
|
||||
```
|
||||
|
||||
### Instantiate the client
|
||||
|
||||
To start the client you need 3 things; a username for your LetsEncrypt account, a bootstrapped Flysystem and you need to
|
||||
decide whether you want to issue `Fake LE Intermediate X1` (staging: `MODE_STAGING`) or `Let's Encrypt Authority X3`
|
||||
(live: `MODE_LIVE`, use for production) certificates.
|
||||
|
||||
```php
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\Adapter\Local;
|
||||
use Afosto\LetsEncrypt\Client;
|
||||
|
||||
//Prepare flysystem
|
||||
$adapter = new Local('data');
|
||||
$filesystem = new Filesystem($adapter);
|
||||
|
||||
//Construct the client
|
||||
$client = new Client([
|
||||
'username' => 'example@example.org',
|
||||
'fs' => $filesystem,
|
||||
'mode' => Client::MODE_STAGING,
|
||||
]);
|
||||
```
|
||||
|
||||
While you instantiate the client, when needed a new LetsEcrypt account is created and then agrees to the TOS.
|
||||
|
||||
|
||||
### Create an order
|
||||
|
||||
To start retrieving certificates, we need to create an order first. This is done as follows:
|
||||
|
||||
```php
|
||||
$order = $client->createOrder(['example.org', 'www.example.org']);
|
||||
```
|
||||
|
||||
In the example above the primary domain is followed by a secondary domain(s). Make sure that for each domain you are
|
||||
able to prove ownership. As a result the certificate will be valid for all provided domains.
|
||||
|
||||
### Prove ownership
|
||||
|
||||
Before you can obtain a certificate for a given domain you need to prove that you own the given domain(s). In this
|
||||
example we will show you how to do this for http-01 validation (where serve specific content at a specific url on the
|
||||
domain, like: `example.org/.well-known/acme-challenge/*`).
|
||||
|
||||
Obtain the authorizations for order. For each domain supplied in the create order request an authorization is returned.
|
||||
|
||||
```php
|
||||
$authorizations = $client->authorize($order);
|
||||
```
|
||||
|
||||
|
||||
You now have an array of `Authorization` objects. These have the challenges you can use (both `DNS` and `HTTP`) to
|
||||
provide proof of ownership.
|
||||
|
||||
Use the following example to get the HTTP validation going. First obtain the challenges, the next step is to make the
|
||||
challenges accessible from
|
||||
```php
|
||||
foreach ($authorizations as $authorization) {
|
||||
$challenge = $authorization->getHttpChallenge();
|
||||
|
||||
$file = $authorization->getFile($challenge);
|
||||
file_put_contents($file->getFilename(), $file->getContents());
|
||||
}
|
||||
```
|
||||
|
||||
Now that the challenges are in place and accessible through `example.org/.well-known/acme-challenge/*` we can request
|
||||
validation.
|
||||
|
||||
### Request validation
|
||||
|
||||
Next step is to request validation of ownership. For each authorization (domain) we ask LetsEncrypt to verify the
|
||||
challenge.
|
||||
|
||||
```php
|
||||
foreach ($authorizations as $authorization) {
|
||||
$ok = $client->validate($authorization->getHttpChallenge(), 15);
|
||||
}
|
||||
```
|
||||
|
||||
The method above will perform 15 attempts to ask LetsEncrypt to validate the challenge (with 1 second intervals) and
|
||||
retrieve an updated status (it might take Lets Encrypt a few seconds to validate the challenge).
|
||||
|
||||
### Get the certificate
|
||||
|
||||
Now to know if validation was successful, test if the order is ready as follows:
|
||||
|
||||
```php
|
||||
if ($client->isReady($order)) {
|
||||
//The validation was successful.
|
||||
}
|
||||
```
|
||||
|
||||
We now know validation was completed and can obtain the certificate. This is done as follows:
|
||||
|
||||
```php
|
||||
$certificate = $client->getCertificate($order);
|
||||
```
|
||||
|
||||
We now have the certificate, to store it on the filesystem:
|
||||
```php
|
||||
//Store the certificate and private key where you need it
|
||||
file_put_contents('certificate.cert', $certificate->getCertificate());
|
||||
file_put_contents('private.key', $certificate->getPrivateKey());
|
||||
```
|
||||
30
composer.json
Normal file
30
composer.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "afosto/yaac",
|
||||
"description": "Yet Another ACME client",
|
||||
"type": "package",
|
||||
"keywords": [
|
||||
"afosto",
|
||||
"lets",
|
||||
"acme",
|
||||
"acmev2",
|
||||
"v2",
|
||||
"encrypt"
|
||||
],
|
||||
"homepage": "https://afosto.com",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Afosto Team",
|
||||
"homepage": "https://afosto.com"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Afosto\\LetsEncrypt\\": "./src"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/guzzle": "^6.3",
|
||||
"league/flysystem": "^1.0"
|
||||
}
|
||||
}
|
||||
381
composer.lock
generated
Normal file
381
composer.lock
generated
Normal file
@@ -0,0 +1,381 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "11a6068373f0fe8d54154f4905027ef6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "6.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/guzzle.git",
|
||||
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
|
||||
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/promises": "^1.0",
|
||||
"guzzlehttp/psr7": "^1.6.1",
|
||||
"php": ">=5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
|
||||
"psr/log": "^1.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
|
||||
"psr/log": "Required for using the Log middleware"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "6.5-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Guzzle is a PHP HTTP client library",
|
||||
"homepage": "http://guzzlephp.org/",
|
||||
"keywords": [
|
||||
"client",
|
||||
"curl",
|
||||
"framework",
|
||||
"http",
|
||||
"http client",
|
||||
"rest",
|
||||
"web service"
|
||||
],
|
||||
"time": "2019-12-23T11:57:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/promises",
|
||||
"version": "v1.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/promises.git",
|
||||
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
|
||||
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\Promise\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Guzzle promises library",
|
||||
"keywords": [
|
||||
"promise"
|
||||
],
|
||||
"time": "2016-12-20T10:07:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
"version": "1.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/psr7.git",
|
||||
"reference": "239400de7a173fe9901b9ac7c06497751f00727a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
|
||||
"reference": "239400de7a173fe9901b9ac7c06497751f00727a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"psr/http-message": "~1.0",
|
||||
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-message-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-zlib": "*",
|
||||
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
|
||||
},
|
||||
"suggest": {
|
||||
"zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\Psr7\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
},
|
||||
{
|
||||
"name": "Tobias Schultze",
|
||||
"homepage": "https://github.com/Tobion"
|
||||
}
|
||||
],
|
||||
"description": "PSR-7 message implementation that also provides common utility methods",
|
||||
"keywords": [
|
||||
"http",
|
||||
"message",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response",
|
||||
"stream",
|
||||
"uri",
|
||||
"url"
|
||||
],
|
||||
"time": "2019-07-01T23:21:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "1.0.64",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "d13c43dbd4b791f815215959105a008515d1a2e0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d13c43dbd4b791f815215959105a008515d1a2e0",
|
||||
"reference": "d13c43dbd4b791f815215959105a008515d1a2e0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-fileinfo": "*",
|
||||
"php": ">=5.5.9"
|
||||
},
|
||||
"conflict": {
|
||||
"league/flysystem-sftp": "<1.0.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/phpspec": "^3.4",
|
||||
"phpunit/phpunit": "^5.7.26"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-fileinfo": "Required for MimeType",
|
||||
"ext-ftp": "Allows you to use FTP server storage",
|
||||
"ext-openssl": "Allows you to use FTPS server storage",
|
||||
"league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
|
||||
"league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
|
||||
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
|
||||
"league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
|
||||
"league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
|
||||
"league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
|
||||
"league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
|
||||
"league/flysystem-webdav": "Allows you to use WebDAV storage",
|
||||
"league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
|
||||
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
|
||||
"srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frenky.net"
|
||||
}
|
||||
],
|
||||
"description": "Filesystem abstraction: Many filesystems, one API.",
|
||||
"keywords": [
|
||||
"Cloud Files",
|
||||
"WebDAV",
|
||||
"abstraction",
|
||||
"aws",
|
||||
"cloud",
|
||||
"copy.com",
|
||||
"dropbox",
|
||||
"file systems",
|
||||
"files",
|
||||
"filesystem",
|
||||
"filesystems",
|
||||
"ftp",
|
||||
"rackspace",
|
||||
"remote",
|
||||
"s3",
|
||||
"sftp",
|
||||
"storage"
|
||||
],
|
||||
"time": "2020-02-05T18:14:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"homepage": "https://github.com/php-fig/http-message",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"time": "2016-08-06T14:39:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ralouphie/getallheaders",
|
||||
"version": "3.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ralouphie/getallheaders.git",
|
||||
"reference": "120b605dfeb996808c31b6477290a714d356e822"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
|
||||
"reference": "120b605dfeb996808c31b6477290a714d356e822",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "^2.1",
|
||||
"phpunit/phpunit": "^5 || ^6.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/getallheaders.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ralph Khattar",
|
||||
"email": "ralph.khattar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A polyfill for getallheaders.",
|
||||
"time": "2019-03-08T08:55:37+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": []
|
||||
}
|
||||
629
src/Client.php
Normal file
629
src/Client.php
Normal file
@@ -0,0 +1,629 @@
|
||||
<?php
|
||||
|
||||
namespace Afosto\LetsEncrypt;
|
||||
|
||||
use Afosto\LetsEncrypt\Data\Account;
|
||||
use Afosto\LetsEncrypt\Data\Authorization;
|
||||
use Afosto\LetsEncrypt\Data\Certificate;
|
||||
use Afosto\LetsEncrypt\Data\Challenge;
|
||||
use Afosto\LetsEncrypt\Data\Order;
|
||||
use GuzzleHttp\Client as HttpClient;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use League\Flysystem\Filesystem;
|
||||
use LEClient\LEFunctions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class Client
|
||||
{
|
||||
/**
|
||||
* Live url
|
||||
*/
|
||||
const DIRECTORY_LIVE = 'https://acme-v02.api.letsencrypt.org/directory';
|
||||
|
||||
/**
|
||||
* Staging url
|
||||
*/
|
||||
const DIRECTORY_STAGING = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
|
||||
/**
|
||||
* Flag for production
|
||||
*/
|
||||
const MODE_LIVE = 'live';
|
||||
|
||||
/**
|
||||
* Flag for staging
|
||||
*/
|
||||
const MODE_STAGING = 'staging';
|
||||
|
||||
/**
|
||||
* New account directory
|
||||
*/
|
||||
const DIRECTORY_NEW_ACCOUNT = 'newAccount';
|
||||
|
||||
/**
|
||||
* Nonce directory
|
||||
*/
|
||||
const DIRECTORY_NEW_NONCE = 'newNonce';
|
||||
|
||||
/**
|
||||
* Order certificate directory
|
||||
*/
|
||||
const DIRECTORY_NEW_ORDER = 'newOrder';
|
||||
|
||||
/**
|
||||
* Http validation
|
||||
*/
|
||||
const VALIDATION_HTTP = 'http-01';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $nonce;
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $privateKeyDetails;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $accountKey;
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
protected $filesystem;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $directories = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $header = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $digest;
|
||||
|
||||
/**
|
||||
* @var HttpClient
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Client constructor.
|
||||
*
|
||||
* @param array $config
|
||||
*
|
||||
* @type string $mode The mode for ACME (production / staging)
|
||||
* @type Filesystem $fs Filesystem for storage of static data
|
||||
* @type string $basePath The base path for the filesystem (used to store account information and csr / keys
|
||||
* @type string $username The acme username
|
||||
* }
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->httpClient = new HttpClient([
|
||||
'base_uri' => (
|
||||
($this->getOption('mode', self::MODE_LIVE) == self::MODE_LIVE) ?
|
||||
self::DIRECTORY_LIVE : self::DIRECTORY_STAGING),
|
||||
]);
|
||||
|
||||
if ($this->getOption('fs', false)) {
|
||||
$this->filesystem = $this->getOption('fs');
|
||||
} else {
|
||||
throw new \LogicException('No filesystem option supplied');
|
||||
}
|
||||
|
||||
if ($this->getOption('username', false) === false) {
|
||||
throw new \LogicException('Username not provided');
|
||||
}
|
||||
|
||||
$this->init();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get an existing order by ID
|
||||
*
|
||||
* @param $id
|
||||
* @return Order
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getOrder($id): Order
|
||||
{
|
||||
$url = str_replace('new-order', 'order', $this->getUrl(self::DIRECTORY_NEW_ORDER));
|
||||
$url = $url . '/' . $this->getAccount()->getId() . '/' . $id;
|
||||
$response = $this->request($url, $this->signPayloadKid(null, $url));
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
|
||||
$domains = [];
|
||||
foreach ($data['identifiers'] as $identifier) {
|
||||
$domains[] = $identifier['value'];
|
||||
}
|
||||
|
||||
return new Order(
|
||||
$domains,
|
||||
$response->getHeaderLine('location'),
|
||||
$data['status'],
|
||||
$data['expires'],
|
||||
$data['identifiers'],
|
||||
$data['authorizations'],
|
||||
$data['finalize']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ready status for order
|
||||
*
|
||||
* @param Order $order
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function isReady(Order $order): bool
|
||||
{
|
||||
$order = $this->getOrder($order->getId());
|
||||
return $order->getStatus() == 'ready';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new order
|
||||
*
|
||||
* @param array $domains
|
||||
* @return Order
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createOrder(array $domains): Order
|
||||
{
|
||||
$identifiers = [];
|
||||
foreach ($domains as $domain) {
|
||||
$identifiers[] =
|
||||
[
|
||||
'type' => 'dns',
|
||||
'value' => $domain,
|
||||
];
|
||||
}
|
||||
|
||||
$url = $this->getUrl(self::DIRECTORY_NEW_ORDER);
|
||||
$response = $this->request($url, $this->signPayloadKid(
|
||||
[
|
||||
'identifiers' => $identifiers,
|
||||
],
|
||||
$url
|
||||
));
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
$order = new Order(
|
||||
$domains,
|
||||
$response->getHeaderLine('location'),
|
||||
$data['status'],
|
||||
$data['expires'],
|
||||
$data['identifiers'],
|
||||
$data['authorizations'],
|
||||
$data['finalize']
|
||||
);
|
||||
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain authorizations
|
||||
*
|
||||
* @param Order $order
|
||||
* @return array|Authorization[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function authorize(Order $order): array
|
||||
{
|
||||
$authorizations = [];
|
||||
foreach ($order->getAuthorizationURLs() as $authorizationURL) {
|
||||
$response = $this->request(
|
||||
$authorizationURL,
|
||||
$this->signPayloadKid(null, $authorizationURL)
|
||||
);
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
$authorization = new Authorization($data['identifier']['value'], $data['expires'], $this->getDigest());
|
||||
|
||||
foreach ($data['challenges'] as $challengeData) {
|
||||
$challenge = new Challenge(
|
||||
$authorizationURL,
|
||||
$challengeData['type'],
|
||||
$challengeData['status'],
|
||||
$challengeData['url'],
|
||||
$challengeData['token']
|
||||
);
|
||||
$authorization->addChallenge($challenge);
|
||||
}
|
||||
$authorizations[] = $authorization;
|
||||
}
|
||||
|
||||
return $authorizations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a challenge
|
||||
*
|
||||
* @param Challenge $challenge
|
||||
* @param int $maxAttempts
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function validate(Challenge $challenge, $maxAttempts = 15): bool
|
||||
{
|
||||
$this->request(
|
||||
$challenge->getUrl(),
|
||||
$this->signPayloadKid([
|
||||
'keyAuthorization' => $challenge->getToken() . '.' . $this->getDigest()
|
||||
], $challenge->getUrl())
|
||||
);
|
||||
|
||||
$data = [];
|
||||
do {
|
||||
$maxAttempts--;
|
||||
$response = $this->request(
|
||||
$challenge->getAuthorizationURL(),
|
||||
$this->signPayloadKid(null, $challenge->getAuthorizationURL())
|
||||
);
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
sleep(1);
|
||||
} while ($maxAttempts < 0 && $data['status'] == 'pending');
|
||||
|
||||
return (isset($data['status']) && $data['status'] == 'ready');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a certificate
|
||||
*
|
||||
* @param Order $order
|
||||
* @return Certificate
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getCertificate(Order $order): Certificate
|
||||
{
|
||||
$privateKey = Helper::getNewKey();
|
||||
$csr = Helper::getCsr($order->getDomains(), $privateKey);
|
||||
$der = Helper::toDer($csr);
|
||||
|
||||
$response = $this->request(
|
||||
$order->getFinalizeURL(),
|
||||
$this->signPayloadKid(
|
||||
['csr' => Helper::toSafeString($der)],
|
||||
$order->getFinalizeURL()
|
||||
)
|
||||
);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
$certificateResponse = $this->request(
|
||||
$data['certificate'],
|
||||
$this->signPayloadKid(null, $data['certificate'])
|
||||
);
|
||||
$certificate = $str = preg_replace('/^[ \t]*[\r\n]+/m', '', (string)$certificateResponse->getBody());
|
||||
return new Certificate($privateKey, $csr, $certificate);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return LE account information
|
||||
*
|
||||
* @return Account
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAccount(): Account
|
||||
{
|
||||
$response = $this->request(
|
||||
$this->getUrl(self::DIRECTORY_NEW_ACCOUNT),
|
||||
$this->signPayloadJWK(
|
||||
[
|
||||
'onlyReturnExisting' => true,
|
||||
],
|
||||
$this->getUrl(self::DIRECTORY_NEW_ACCOUNT)
|
||||
)
|
||||
);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
$accountURL = $response->getHeaderLine('Location');
|
||||
$date = (new \DateTime())->setTimestamp(strtotime($data['createdAt']));
|
||||
return new Account($data['contact'], $date, ($data['status'] == 'valid'), $data['initialIp'], $accountURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the client
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
//Load the directories from the LE api
|
||||
$response = $this->httpClient->get('/directory');
|
||||
$result = \GuzzleHttp\json_decode((string)$response->getBody(), true);
|
||||
$this->directories = $result;
|
||||
|
||||
//Prepare LE account
|
||||
$this->loadKeys();
|
||||
$this->tosAgree();
|
||||
$this->account = $this->getAccount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the keys in memory
|
||||
*
|
||||
* @throws \League\Flysystem\FileExistsException
|
||||
* @throws \League\Flysystem\FileNotFoundException
|
||||
*/
|
||||
protected function loadKeys()
|
||||
{
|
||||
//Make sure a private key is in place
|
||||
if ($this->getFilesystem()->has($this->getPath('account.pem')) === false) {
|
||||
$this->getFilesystem()->write($this->getPath('account.pem'), Helper::getNewKey());
|
||||
}
|
||||
$privateKey = $this->getFilesystem()->read($this->getPath('account.pem'));
|
||||
$privateKey = openssl_pkey_get_private($privateKey);
|
||||
$this->privateKeyDetails = openssl_pkey_get_details($privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Agree to the terms of service
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function tosAgree()
|
||||
{
|
||||
$this->request(
|
||||
$this->getUrl(self::DIRECTORY_NEW_ACCOUNT),
|
||||
$this->signPayloadJWK(
|
||||
[
|
||||
'contact' => [
|
||||
'mailto:' . $this->getOption('username'),
|
||||
],
|
||||
'termsOfServiceAgreed' => true,
|
||||
],
|
||||
$this->getUrl(self::DIRECTORY_NEW_ACCOUNT)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted path
|
||||
*
|
||||
* @param null $path
|
||||
* @return string
|
||||
*/
|
||||
protected function getPath($path = null): string
|
||||
{
|
||||
$userDirectory = preg_replace('/[^a-z0-9]+/', '-', strtolower($this->getOption('username')));
|
||||
|
||||
return $this->getOption(
|
||||
'basePath',
|
||||
'le'
|
||||
) . DIRECTORY_SEPARATOR . $userDirectory . ($path === null ? '' : DIRECTORY_SEPARATOR . $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Filesystem
|
||||
*/
|
||||
protected function getFilesystem(): Filesystem
|
||||
{
|
||||
return $this->filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a defined option
|
||||
*
|
||||
* @param $key
|
||||
* @param null $default
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function getOption($key, $default = null)
|
||||
{
|
||||
if (isset($this->config[$key])) {
|
||||
return $this->config[$key];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key fingerprint
|
||||
*
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDigest(): string
|
||||
{
|
||||
if ($this->digest === null) {
|
||||
$this->digest = Helper::toSafeString(hash('sha256', json_encode($this->getJWKHeader()), true));
|
||||
}
|
||||
|
||||
return $this->digest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the LE API
|
||||
*
|
||||
* @param $url
|
||||
* @param array $payload
|
||||
* @param string $method
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function request($url, $payload = [], $method = 'POST'): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->request($method, $url, [
|
||||
'json' => $payload,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/jose+json',
|
||||
]
|
||||
]);
|
||||
$this->nonce = $response->getHeaderLine('replay-nonce');
|
||||
} catch (ClientException $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LE directory path
|
||||
*
|
||||
* @param $directory
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getUrl($directory): string
|
||||
{
|
||||
if (isset($this->directories[$directory])) {
|
||||
return $this->directories[$directory];
|
||||
}
|
||||
|
||||
throw new \Exception('Invalid directory: ' . $directory . ' not listed');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the key
|
||||
*
|
||||
* @return bool|resource|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getAccountKey()
|
||||
{
|
||||
if ($this->accountKey === null) {
|
||||
$this->accountKey = openssl_pkey_get_private($this->getFilesystem()
|
||||
->read($this->getPath('account.pem')));
|
||||
}
|
||||
|
||||
if ($this->accountKey === false) {
|
||||
throw new \Exception('Invalid account key');
|
||||
}
|
||||
|
||||
return $this->accountKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getJWKHeader(): array
|
||||
{
|
||||
return [
|
||||
'e' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['e']),
|
||||
'kty' => 'RSA',
|
||||
'n' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['n']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWK envelope
|
||||
*
|
||||
* @param $url
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getJWK($url): array
|
||||
{
|
||||
//Require a nonce to be available
|
||||
if ($this->nonce === null) {
|
||||
$response = $this->httpClient->head($this->directories[self::DIRECTORY_NEW_NONCE]);
|
||||
$this->nonce = $response->getHeaderLine('replay-nonce');
|
||||
}
|
||||
return [
|
||||
'alg' => 'RS256',
|
||||
'jwk' => $this->getJWKHeader(),
|
||||
'nonce' => $this->nonce,
|
||||
'url' => $url
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KID envelope
|
||||
*
|
||||
* @param $url
|
||||
* @param $kid
|
||||
* @return array
|
||||
*/
|
||||
protected function getKID($url): array
|
||||
{
|
||||
$response = $this->httpClient->head($this->directories[self::DIRECTORY_NEW_NONCE]);
|
||||
$nonce = $response->getHeaderLine('replay-nonce');
|
||||
|
||||
return [
|
||||
"alg" => "RS256",
|
||||
"kid" => $this->account->getAccountURL(),
|
||||
"nonce" => $nonce,
|
||||
"url" => $url
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the payload to the JWS format
|
||||
*
|
||||
* @param $payload
|
||||
* @param $url
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function signPayloadJWK($payload, $url): array
|
||||
{
|
||||
$payload = is_array($payload) ? str_replace('\\/', '/', json_encode($payload)) : '';
|
||||
$payload = Helper::toSafeString($payload);
|
||||
$protected = Helper::toSafeString(json_encode($this->getJWK($url)));
|
||||
|
||||
$result = openssl_sign($protected . '.' . $payload, $signature, $this->getAccountKey(), "SHA256");
|
||||
|
||||
if ($result === false) {
|
||||
throw new \Exception('Could not sign');
|
||||
}
|
||||
|
||||
return [
|
||||
'protected' => $protected,
|
||||
'payload' => $payload,
|
||||
'signature' => Helper::toSafeString($signature),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the payload to the KID format
|
||||
*
|
||||
* @param $payload
|
||||
* @param $url
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function signPayloadKid($payload, $url): array
|
||||
{
|
||||
$payload = is_array($payload) ? str_replace('\\/', '/', json_encode($payload)) : '';
|
||||
$payload = Helper::toSafeString($payload);
|
||||
$protected = Helper::toSafeString(json_encode($this->getKID($url)));
|
||||
|
||||
$result = openssl_sign($protected . '.' . $payload, $signature, $this->getAccountKey(), "SHA256");
|
||||
if ($result === false) {
|
||||
throw new \Exception('Could not sign');
|
||||
}
|
||||
|
||||
return [
|
||||
'protected' => $protected,
|
||||
'payload' => $payload,
|
||||
'signature' => Helper::toSafeString($signature),
|
||||
];
|
||||
}
|
||||
}
|
||||
87
src/Data/Account.php
Normal file
87
src/Data/Account.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Afosto\LetsEncrypt\Data;
|
||||
|
||||
class Account
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $contact;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $createdAt;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $isValid;
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $initialIp;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $accountURL;
|
||||
|
||||
|
||||
public function __construct(
|
||||
array $contact,
|
||||
\DateTime $createdAt,
|
||||
bool $isValid,
|
||||
string $initialIp,
|
||||
string $accountURL
|
||||
) {
|
||||
$this->initialIp = $initialIp;
|
||||
$this->contact = $contact;
|
||||
$this->createdAt = $createdAt;
|
||||
$this->isValid = $isValid;
|
||||
$this->accountURL = $accountURL;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return substr($this->accountURL, strrpos($this->accountURL, '/') + 1);
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
|
||||
public function getAccountURL(): string
|
||||
{
|
||||
return $this->accountURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getContact(): array
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInitialIp(): string
|
||||
{
|
||||
return $this->initialIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->isValid;
|
||||
}
|
||||
}
|
||||
93
src/Data/Authorization.php
Normal file
93
src/Data/Authorization.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Afosto\LetsEncrypt\Data;
|
||||
|
||||
use Afosto\LetsEncrypt\Client;
|
||||
|
||||
class Authorization
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $domain;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
protected $expires;
|
||||
|
||||
/**
|
||||
* @var Challenge[]
|
||||
*/
|
||||
protected $challenges = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $digest;
|
||||
|
||||
public function __construct(string $domain, string $expires, string $digest)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
$this->expires = (new \DateTime())->setTimestamp(strtotime($expires));
|
||||
$this->digest = $digest;
|
||||
}
|
||||
|
||||
public function addChallenge(Challenge $challenge)
|
||||
{
|
||||
$this->challenges[] = $challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDomain(): string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getExpires(): \DateTime
|
||||
{
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Challenge[]
|
||||
*/
|
||||
public function getChallenges(): array
|
||||
{
|
||||
return $this->challenges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Challenge|bool
|
||||
*/
|
||||
public function getHttpChallenge()
|
||||
{
|
||||
foreach ($this->getChallenges() as $challenge) {
|
||||
if ($challenge->getType() == Client::VALIDATION_HTTP) {
|
||||
return $challenge;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Challenge $challenge
|
||||
* @return File|bool
|
||||
*/
|
||||
public function getFile(Challenge $challenge)
|
||||
{
|
||||
if ($challenge->getType() == Client::VALIDATION_HTTP) {
|
||||
$file = new File($challenge->getToken(), $challenge->getToken() . '.' . $this->digest);
|
||||
return $file;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
76
src/Data/Certificate.php
Normal file
76
src/Data/Certificate.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Afosto\LetsEncrypt\Data;
|
||||
|
||||
use Afosto\LetsEncrypt\Helper;
|
||||
|
||||
class Certificate
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $privateKey;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $certificate;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $csr;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
protected $expiryDate;
|
||||
|
||||
/**
|
||||
* Certificate constructor.
|
||||
* @param $privateKey
|
||||
* @param $csr
|
||||
* @param $certificate
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($privateKey, $csr, $certificate)
|
||||
{
|
||||
$this->privateKey = $privateKey;
|
||||
$this->csr = $csr;
|
||||
$this->certificate = $certificate;
|
||||
$this->expiryDate = Helper::getCertExpiryDate($certificate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCsr(): string
|
||||
{
|
||||
return $this->csr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getExpiryDate(): \DateTime
|
||||
{
|
||||
return $this->expiryDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificate(): string
|
||||
{
|
||||
return $this->certificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPrivateKey(): string
|
||||
{
|
||||
return $this->privateKey;
|
||||
}
|
||||
}
|
||||
83
src/Data/Challenge.php
Normal file
83
src/Data/Challenge.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Afosto\LetsEncrypt\Data;
|
||||
|
||||
class Challenge
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $authorizationURL;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
/**
|
||||
* Challenge constructor.
|
||||
* @param string $authorizationURL
|
||||
* @param string $type
|
||||
* @param string $status
|
||||
* @param string $url
|
||||
* @param string $token
|
||||
*/
|
||||
public function __construct(string $authorizationURL, string $type, string $status, string $url, string $token)
|
||||
{
|
||||
$this->authorizationURL = $authorizationURL;
|
||||
$this->type = $type;
|
||||
$this->status = $status;
|
||||
$this->url = $url;
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getAuthorizationURL(): string
|
||||
{
|
||||
return $this->authorizationURL;
|
||||
}
|
||||
}
|
||||
40
src/Data/File.php
Normal file
40
src/Data/File.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Afosto\LetsEncrypt\Data;
|
||||
|
||||
class File
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filename;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $contents;
|
||||
|
||||
|
||||
public function __construct(string $filename, string $contents)
|
||||
{
|
||||
$this->contents = $contents;
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContents(): string
|
||||
{
|
||||
return $this->contents;
|
||||
}
|
||||
}
|
||||
102
src/Data/Order.php
Normal file
102
src/Data/Order.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Afosto\LetsEncrypt\Data;
|
||||
|
||||
class Order
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
protected $expiresAt;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $identifiers;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $authorizations;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $finalizeURL;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $domains;
|
||||
|
||||
|
||||
public function __construct(
|
||||
array $domains,
|
||||
string $url,
|
||||
string $status,
|
||||
string $expiresAt,
|
||||
array $identifiers,
|
||||
array $authorizations,
|
||||
string $finalizeURL
|
||||
) {
|
||||
$this->domains = $domains;
|
||||
$this->url = $url;
|
||||
$this->status = $status;
|
||||
$this->expiresAt = (new \DateTime())->setTimestamp(strtotime($expiresAt));
|
||||
$this->identifiers = $identifiers;
|
||||
$this->authorizations = $authorizations;
|
||||
$this->finalizeURL = $finalizeURL;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return substr($this->url, strrpos($this->url, '/') + 1);
|
||||
}
|
||||
|
||||
public function getURL(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getAuthorizationURLs(): array
|
||||
{
|
||||
return $this->authorizations;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getExpiresAt(): \DateTime
|
||||
{
|
||||
return $this->expiresAt;
|
||||
}
|
||||
|
||||
public function getIdentifiers(): array
|
||||
{
|
||||
return $this->identifiers;
|
||||
}
|
||||
|
||||
public function getFinalizeURL(): string
|
||||
{
|
||||
return $this->finalizeURL;
|
||||
}
|
||||
|
||||
public function getDomains(): array
|
||||
{
|
||||
return $this->domains;
|
||||
}
|
||||
}
|
||||
138
src/Helper.php
Normal file
138
src/Helper.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Afosto\LetsEncrypt;
|
||||
|
||||
use Afosto\LetsEncrypt\Data\Authorization;
|
||||
use GuzzleHttp\Client as HttpClient;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
class Helper
|
||||
{
|
||||
|
||||
/**
|
||||
* Formatter
|
||||
* @param $pem
|
||||
* @return false|string
|
||||
*/
|
||||
public static function toDer($pem)
|
||||
{
|
||||
$lines = explode(PHP_EOL, $pem);
|
||||
$lines = array_slice($lines, 1, -1);
|
||||
|
||||
return base64_decode(implode('', $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return certificate expiry date
|
||||
*
|
||||
* @param $certificate
|
||||
*
|
||||
* @return \DateTime
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getCertExpiryDate($certificate): \DateTime
|
||||
{
|
||||
$info = openssl_x509_parse($certificate);
|
||||
if ($info === false) {
|
||||
throw new \Exception('Could not parse certificate');
|
||||
}
|
||||
$dateTime = new \DateTime();
|
||||
$dateTime->setTimestamp($info['validTo_time_t']);
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getNewKey(): string
|
||||
{
|
||||
|
||||
$key = openssl_pkey_new([
|
||||
'private_key_bits' => 4096,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
]);
|
||||
openssl_pkey_export($key, $pem);
|
||||
|
||||
return $pem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new CSR
|
||||
*
|
||||
* @param array $domains
|
||||
* @param $key
|
||||
*
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getCsr(array $domains, $key): string
|
||||
{
|
||||
$primaryDomain = current(($domains));
|
||||
$config = [
|
||||
'[req]',
|
||||
'distinguished_name=req_distinguished_name',
|
||||
'[req_distinguished_name]',
|
||||
'[v3_req]',
|
||||
'[v3_ca]',
|
||||
'[SAN]',
|
||||
'subjectAltName=' . implode(',', array_map(function ($domain) {
|
||||
return 'DNS:' . $domain;
|
||||
}, $domains)),
|
||||
];
|
||||
|
||||
$fn = tempnam(sys_get_temp_dir(), md5(microtime(true)));
|
||||
file_put_contents($fn, implode("\n", $config));
|
||||
$csr = openssl_csr_new([
|
||||
'countryName' => 'NL',
|
||||
'commonName' => $primaryDomain,
|
||||
], $key, [
|
||||
'config' => $fn,
|
||||
'req_extensions' => 'SAN',
|
||||
'digest_alg' => 'sha512',
|
||||
]);
|
||||
unlink($fn);
|
||||
|
||||
if ($csr === false) {
|
||||
throw new \Exception('Could not create a CSR');
|
||||
}
|
||||
|
||||
if (openssl_csr_export($csr, $result) == false) {
|
||||
throw new \Exception('CRS export failed');
|
||||
}
|
||||
|
||||
$result = trim($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a safe base64 string
|
||||
*
|
||||
* @param $data
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function toSafeString($data): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key information
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getKeyDetails($key): array
|
||||
{
|
||||
$accountDetails = openssl_pkey_get_details($key);
|
||||
if ($accountDetails === false) {
|
||||
throw new \Exception('Could not load account details');
|
||||
}
|
||||
|
||||
return $accountDetails;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user