mirror of
https://github.com/anikeen-com/yaac.git
synced 2026-03-17 23:56:08 +00:00
added dns support
This commit is contained in:
109
README.md
109
README.md
@@ -72,22 +72,24 @@ $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
|
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.
|
able to prove ownership. As a result the certificate will be valid for all provided domains.
|
||||||
|
|
||||||
|
|
||||||
### Prove ownership
|
### 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
|
Before you can obtain a certificate for a given domain you need to prove that you own the given domain(s).
|
||||||
example we will show you how to do this for http-01 validation (where serve specific content at a specific url on the
|
We request the authorizations to prove ownership. Obtain the authorizations for order. For each domain supplied in the
|
||||||
domain, like: `example.org/.well-known/acme-challenge/*`).
|
create order request an authorization is returned.
|
||||||
|
|
||||||
Obtain the authorizations for order. For each domain supplied in the create order request an authorization is returned.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$authorizations = $client->authorize($order);
|
$authorizations = $client->authorize($order);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
You now have an array of `Authorization` objects. These have the challenges you can use (both `DNS` and `HTTP`) to
|
You now have an array of `Authorization` objects. These have the challenges you can use (both `DNS` and `HTTP`) to
|
||||||
provide proof of ownership.
|
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
|
Use the following example to get the HTTP validation going. First obtain the challenges, the next step is to make the
|
||||||
challenges accessible from
|
challenges accessible from
|
||||||
```php
|
```php
|
||||||
@@ -97,72 +99,61 @@ 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::TYPE_DNS);
|
||||||
|
```
|
||||||
|
|
||||||
|
For a HTTP challenge test call:
|
||||||
|
```php
|
||||||
|
$client->selfTest($authorization, Client::VALIDATION_HTTP);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Request validation
|
### Request validation
|
||||||
|
|
||||||
Next step is to request validation of ownership. For each authorization (domain) we ask LetsEncrypt to verify the
|
Next step is to request validation of ownership. For each authorization (domain) we ask LetsEncrypt to verify the
|
||||||
challenge.
|
challenge.
|
||||||
|
|
||||||
|
For HTTP validation:
|
||||||
```php
|
```php
|
||||||
foreach ($authorizations as $authorization) {
|
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
|
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).
|
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
|
### Get the certificate
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,8 @@ class Client
|
|||||||
{
|
{
|
||||||
if ($type == self::VALIDATION_HTTP) {
|
if ($type == self::VALIDATION_HTTP) {
|
||||||
return $this->selfHttpTest($authorization, $maxAttempts);
|
return $this->selfHttpTest($authorization, $maxAttempts);
|
||||||
|
} elseif ($type == self::VALIDATION_DNS) {
|
||||||
|
return $this->selfDNSTest($authorization, $maxAttempts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,21 +396,17 @@ class Client
|
|||||||
*/
|
*/
|
||||||
protected function selfHttpTest(Authorization $authorization, $maxAttempts)
|
protected function selfHttpTest(Authorization $authorization, $maxAttempts)
|
||||||
{
|
{
|
||||||
$file = $authorization->getFile();
|
|
||||||
$authorization->getDomain();
|
|
||||||
do {
|
do {
|
||||||
$maxAttempts--;
|
$maxAttempts--;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $this->getSelfTestClient()->request(
|
$response = $this->getSelfTestClient()->request(
|
||||||
'GET',
|
'GET',
|
||||||
'http://' . $authorization->getDomain() . '/.well-known/acme-challenge/' . $file->getFilename()
|
'http://' . $authorization->getDomain() . '/.well-known/acme-challenge/' .
|
||||||
|
$authorization->getFile()->getFilename()
|
||||||
);
|
);
|
||||||
$contents = (string)$response->getBody();
|
$contents = (string)$response->getBody();
|
||||||
if ($contents == $file->getContents()) {
|
if ($contents == $authorization->getFile()->getContents()) {
|
||||||
{
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (RequestException $e) {
|
} catch (RequestException $e) {
|
||||||
}
|
}
|
||||||
@@ -417,6 +415,54 @@ class Client
|
|||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(ceil(30 / $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
|
* Initialize the client
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -122,13 +122,20 @@ class Authorization
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Returns the DNS record object
|
||||||
|
*
|
||||||
* @param Challenge $challenge
|
* @param Challenge $challenge
|
||||||
* @return string containing TXT record for DNS challenge
|
* @return Record|bool
|
||||||
*/
|
*/
|
||||||
public function getTxtRecord(Challenge $challenge)
|
public function getTxtRecord()
|
||||||
{
|
{
|
||||||
$raw=$challenge->getToken() . '.' . $this->digest;
|
$challenge = $this->getDnsChallenge();
|
||||||
$hash=hash('sha256', $raw, true);
|
if ($challenge !== false) {
|
||||||
return Helper::toSafeString($hash);
|
$hash = hash('sha256', $challenge->getToken() . '.' . $this->digest, true);
|
||||||
|
$value = Helper::toSafeString($hash);
|
||||||
|
return new Record('_acme-challenge.' . $this->getDomain(), $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/Data/Record.php
Normal file
46
src/Data/Record.php
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,11 +92,11 @@ class Helper
|
|||||||
file_put_contents($fn, implode("\n", $config));
|
file_put_contents($fn, implode("\n", $config));
|
||||||
$csr = openssl_csr_new([
|
$csr = openssl_csr_new([
|
||||||
'countryName' => 'NL',
|
'countryName' => 'NL',
|
||||||
'commonName' => $primaryDomain,
|
'commonName' => $primaryDomain,
|
||||||
], $key, [
|
], $key, [
|
||||||
'config' => $fn,
|
'config' => $fn,
|
||||||
'req_extensions' => 'SAN',
|
'req_extensions' => 'SAN',
|
||||||
'digest_alg' => 'sha512',
|
'digest_alg' => 'sha512',
|
||||||
]);
|
]);
|
||||||
unlink($fn);
|
unlink($fn);
|
||||||
|
|
||||||
@@ -140,37 +140,4 @@ class Helper
|
|||||||
|
|
||||||
return $accountDetails;
|
return $accountDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait until a set of DNS records return specific TXT record values
|
|
||||||
*
|
|
||||||
* @param array mapping domain to desired TXT record value
|
|
||||||
* @param $txtRecord
|
|
||||||
* @param int $maxSeconds to wait
|
|
||||||
* @return bool true if record found, false otherwise
|
|
||||||
*/
|
|
||||||
public static function waitForDNS(array $records, $maxSeconds=60)
|
|
||||||
{
|
|
||||||
$waitUntil = time() + $maxSeconds;
|
|
||||||
|
|
||||||
do {
|
|
||||||
//validate all remaining records..
|
|
||||||
foreach($records as $domain=>$txtRecord) {
|
|
||||||
$record=dns_get_record($domain, DNS_TXT);
|
|
||||||
if (isset($record[0]['txt']) && ($record[0]['txt']===$txtRecord)) {
|
|
||||||
unset($records[$domain]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//did we find them all?
|
|
||||||
if (empty($records)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//otherwise still domains to check...have a short sleep
|
|
||||||
sleep(1);
|
|
||||||
} while(time() < $waitUntil);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user