Skip to content

Commit

Permalink
Merge pull request #5 from afosto/feature/dns
Browse files Browse the repository at this point in the history
DNS support added
Big thanks to @lordelph for working on this
  • Loading branch information
bakkerpeter authored Mar 19, 2020
2 parents ccaec49 + fae8ff8 commit fe6fc30
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 25 deletions.
66 changes: 52 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,23 @@ $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.
### Prove ownership

Before you can obtain a certificate for a given domain you need to prove that you own the given domain(s).
We request the authorizations to prove ownership. 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.


You now have an array of `Authorization` objects. These have the challenges you can use (both `DNS` and `HTTP`) to
provide proof of ownership.
#### HTTP validation

HTTP validation (where serve specific content at a specific url on the domain, like:
`example.org/.well-known/acme-challenge/*`) is done as follows:

Use the following example to get the HTTP validation going. First obtain the challenges, the next step is to make the
challenges accessible from
Expand All @@ -97,26 +99,62 @@ foreach ($authorizations as $authorization) {
}
```

Now that the challenges are in place and accessible through `example.org/.well-known/acme-challenge/*` we can request
validation.

#### DNS validation
You can also use DNS validation - to do this, you will need access to an API of your DNS
provider to create TXT records for the target domains.

```php
foreach ($authorizations as $authorization) {
$txtRecord = $authorization->getTxtRecord();

//To get the name of the TXT record call:
$txtRecord->getName();

//To get the value of the TXT record call:
$txtRecord->getValue();
}
```


### Self test

After exposing the challenges (made accessible through HTTP or DNS) we can perform a self test just to
be sure it works. For a DNS test call:

```php
$client->selfTest($authorization, Client::VALIDATON_DNS);
```

For a HTTP challenge test call:
```php
$client->selfTest($authorization, Client::VALIDATION_HTTP);
```


### Request validation

Next step is to request validation of ownership. For each authorization (domain) we ask LetsEncrypt to verify the
challenge.

For HTTP validation:
```php
foreach ($authorizations as $authorization) {
if ($client->selfTest($authorization, Client::VALIDATION_HTTP)) {
$client->validate($authorization->getHttpChallenge(), 15);
}

$client->validate($authorization->getHttpChallenge(), 15);
}
```

For DNS validation:
```php
foreach ($authorizations as $authorization) {
$client->validate($authorization->getDnsChallenge(), 15);
}
```

The code above will first perform a self test and, if successful, will do 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 we can request a certificate for the order, test if the order is ready as follows:
Expand Down
71 changes: 63 additions & 8 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class Client
*/
const VALIDATION_HTTP = 'http-01';

/**
* DNS validation
*/
const VALIDATION_DNS = 'dns-01';

/**
* @var string
*/
Expand Down Expand Up @@ -262,6 +267,8 @@ public function selfTest(Authorization $authorization, $type = self::VALIDATION_
{
if ($type == self::VALIDATION_HTTP) {
return $this->selfHttpTest($authorization, $maxAttempts);
} elseif ($type == self::VALIDATION_DNS) {
return $this->selfDNSTest($authorization, $maxAttempts);
}
}

Expand Down Expand Up @@ -289,7 +296,9 @@ public function validate(Challenge $challenge, $maxAttempts = 15): bool
$this->signPayloadKid(null, $challenge->getAuthorizationURL())
);
$data = json_decode((string)$response->getBody(), true);
sleep(ceil(15 / $maxAttempts));
if ($maxAttempts > 1) {
sleep(ceil(15 / $maxAttempts));
}
$maxAttempts--;
} while ($maxAttempts > 0 && $data['status'] != 'valid');

Expand Down Expand Up @@ -389,29 +398,75 @@ protected function getSelfTestClient()
*/
protected function selfHttpTest(Authorization $authorization, $maxAttempts)
{
$file = $authorization->getFile();
$authorization->getDomain();
do {
$maxAttempts--;

try {
$response = $this->getSelfTestClient()->request(
'GET',
'http://' . $authorization->getDomain() . '/.well-known/acme-challenge/' . $file->getFilename()
'http://' . $authorization->getDomain() . '/.well-known/acme-challenge/' .
$authorization->getFile()->getFilename()
);
$contents = (string)$response->getBody();
if ($contents == $file->getContents()) {
{
if ($contents == $authorization->getFile()->getContents()) {
return true;
}
} catch (RequestException $e) {
}
} while ($maxAttempts > 0);

return false;
}

/**
* Self DNS test client that uses Cloudflare's DNS API
* @param Authorization $authorization
* @param $maxAttempts
* @return bool
*/
protected function selfDNSTest(Authorization $authorization, $maxAttempts)
{
do {
$response = $this->getSelfTestDNSClient()->get(
'/dns-query',
[
'query' => [
'name' => $authorization->getTxtRecord()->getName(),
'type' => 'TXT'
]
]
);
$data = json_decode((string)$response->getBody(), true);
if (isset($data['Answer'])) {
foreach ($data['Answer'] as $result) {
if (trim($result['data'], "\"") == $authorization->getTxtRecord()->getValue()) {
return true;
}
}
} catch (RequestException $e) {
}
if ($maxAttempts > 1) {
sleep(ceil(45 / $maxAttempts));
}
$maxAttempts--;
} while ($maxAttempts > 0);

return false;
}

/**
* Return the preconfigured client to call Cloudflare's DNS API
* @return HttpClient
*/
protected function getSelfTestDNSClient()
{
return new HttpClient([
'base_uri' => 'https://cloudflare-dns.com',
'connect_timeout' => 10,
'headers' => [
'Accept' => 'application/dns-json',
],
]);
}

/**
* Initialize the client
*/
Expand Down
33 changes: 33 additions & 0 deletions src/Data/Authorization.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Afosto\Acme\Data;

use Afosto\Acme\Client;
use Afosto\Acme\Helper;

class Authorization
{
Expand Down Expand Up @@ -93,6 +94,20 @@ public function getHttpChallenge()
return false;
}

/**
* @return Challenge|bool
*/
public function getDnsChallenge()
{
foreach ($this->getChallenges() as $challenge) {
if ($challenge->getType() == Client::VALIDATION_DNS) {
return $challenge;
}
}

return false;
}

/**
* Return File object for the given challenge
* @return File|bool
Expand All @@ -105,4 +120,22 @@ public function getFile()
}
return false;
}

/**
* Returns the DNS record object
*
* @param Challenge $challenge
* @return Record|bool
*/
public function getTxtRecord()
{
$challenge = $this->getDnsChallenge();
if ($challenge !== false) {
$hash = hash('sha256', $challenge->getToken() . '.' . $this->digest, true);
$value = Helper::toSafeString($hash);
return new Record('_acme-challenge.' . $this->getDomain(), $value);
}

return false;
}
}
46 changes: 46 additions & 0 deletions src/Data/Record.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Afosto\Acme\Data;

class Record
{

/**
* @var string
*/
protected $name;

/**
* @var string
*/
protected $value;

/**
* Record constructor.
* @param string $name
* @param string $value
*/
public function __construct(string $name, string $value)
{
$this->name = $name;
$this->value = $value;
}

/**
* Return the DNS TXT record name for validation
* @return string
*/
public function getName(): string
{
return $this->name;
}

/**
* Return the record value for DNS validation
* @return string
*/
public function getValue(): string
{
return $this->value;
}
}
6 changes: 3 additions & 3 deletions src/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ public static function getCsr(array $domains, $key): string
file_put_contents($fn, implode("\n", $config));
$csr = openssl_csr_new([
'countryName' => 'NL',
'commonName' => $primaryDomain,
'commonName' => $primaryDomain,
], $key, [
'config' => $fn,
'config' => $fn,
'req_extensions' => 'SAN',
'digest_alg' => 'sha512',
'digest_alg' => 'sha512',
]);
unlink($fn);

Expand Down

0 comments on commit fe6fc30

Please sign in to comment.