diff --git a/README.md b/README.md index 75f9e4d..5958dc7 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,53 @@ foreach ($authorizations as $authorization) { 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: diff --git a/src/Client.php b/src/Client.php index 5ca07ad..3c711b6 100644 --- a/src/Client.php +++ b/src/Client.php @@ -55,6 +55,11 @@ class Client */ const VALIDATION_HTTP = 'http-01'; + /** + * DNS validation + */ + const VALIDATION_DNS = 'dns-01'; + /** * @var string */ diff --git a/src/Data/Authorization.php b/src/Data/Authorization.php index 866d3cf..f7a8a83 100644 --- a/src/Data/Authorization.php +++ b/src/Data/Authorization.php @@ -3,6 +3,7 @@ namespace Afosto\Acme\Data; use Afosto\Acme\Client; +use Afosto\Acme\Helper; class Authorization { @@ -78,6 +79,20 @@ class Authorization return false; } + /** + * @return Challenge|bool + */ + public function getDnsChallenge() + { + foreach ($this->getChallenges() as $challenge) { + if ($challenge->getType() == Client::VALIDATION_DNS) { + return $challenge; + } + } + + return false; + } + /** * @param Challenge $challenge * @return File|bool @@ -90,4 +105,15 @@ class Authorization } return false; } + + /** + * @param Challenge $challenge + * @return string containing TXT record for DNS challenge + */ + public function getTxtRecord(Challenge $challenge) + { + $raw=$challenge->getToken() . '.' . $this->digest; + $hash=hash('sha256', $raw, true); + return Helper::toSafeString($hash); + } } diff --git a/src/Helper.php b/src/Helper.php index b29534f..c555a54 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -135,4 +135,37 @@ class Helper 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; + } }