# 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\Acme\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). ### Alternative ownership validation via DNS You can also use DNS validation - to do this, you will need access to an API for your DNS provider to create TXT records for the target domains. ```php //store a map of domain=>TXT record we can use to wait with $dnsRecords[]; foreach ($authorizations as $authorization) { $challenge = $authorization->getDnsChallenge(); $txtRecord = $authorization->getTxtRecord($challenge); $domain=$authorization->getDomain(); $validationDomain='_acme-challenge.'.$domain; //remember the record we're about to set $dnsRecords[$validationDomain] = $txtRecord; //set TXT record for $validationDomain to $txtRecord value //-- //-- you need to add code for your DNS provider here //-- } ``` A helper is included which will allow you to wait until you can see the DNS changes before asking Let's Encrypt to validate it, e.g. ```php //wait up to 60 seconds for all our DNS updates to propagate if (!Helper::waitForDNS($dnsRecords, 60)) { throw new \Exception('Unable to verify TXT record update'); } ``` Once this passes we can ask Let's Encrypt to do the same... ```php foreach ($authorizations as $authorization) { $ok = $client->validate($authorization->getDnsChallenge(), 15); } ``` ### 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()); ```