From cada512a3adbb4a425e7b0b19527c81bbe63157d Mon Sep 17 00:00:00 2001 From: nhatdo Date: Thu, 11 May 2023 10:30:35 +0700 Subject: [PATCH 1/7] update get certificate regards to the changes from Let's Encrypt https://community.letsencrypt.org/t/enabling-asynchronous-order-finalization/193522 --- src/Client.php | 88 ++++++++++++++++++++++++++++++++-------------- src/Data/Order.php | 19 +++++++++- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/src/Client.php b/src/Client.php index 17fef64..ead954c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -164,7 +164,8 @@ public function getOrder($id): Order $data['expires'], $data['identifiers'], $data['authorizations'], - $data['finalize'] + $data['finalize'], + $data['certificate'] ?? '' ); } @@ -195,7 +196,7 @@ public function createOrder(array $domains): Order foreach ($domains as $domain) { $identifiers[] = [ - 'type' => 'dns', + 'type' => 'dns', 'value' => $domain, ]; } @@ -311,10 +312,11 @@ public function validate(Challenge $challenge, int $maxAttempts = 15): bool * Return a certificate * * @param Order $order + * @param int $maxAttempts * @return Certificate * @throws \Exception */ - public function getCertificate(Order $order): Certificate + public function getCertificate(Order $order, int $maxAttempts = 15): Certificate { $privateKey = Helper::getNewKey($this->getOption('key_length', 4096)); $csr = Helper::getCsr($order->getDomains(), $privateKey); @@ -329,11 +331,28 @@ public function getCertificate(Order $order): Certificate ); $data = json_decode((string)$response->getBody(), true); - $certificateResponse = $this->request( - $data['certificate'], - $this->signPayloadKid(null, $data['certificate']) - ); - $chain = $str = preg_replace('/^[ \t]*[\r\n]+/m', '', (string)$certificateResponse->getBody()); + $chain = null; + + if (isset($data['certificate'])) { + $chain = $this->downloadCertificate($data['certificate']); + } elseif ($data['status'] == 'processing') { + do { + sleep(5); + $order = $this->getOrder($order->getId()); + + if ($order->getStatus() == 'valid') { + $chain = $this->downloadCertificate($order->getCertificate()); + break; + } + + $maxAttempts--; + } while ($maxAttempts > 0); + } + + if (empty($chain)) { + throw new \LogicException("Could not get certificate"); + } + return new Certificate($privateKey, $csr, $chain); } @@ -389,8 +408,8 @@ protected function getHttpClient() protected function getSelfTestClient() { return new HttpClient([ - 'verify' => false, - 'timeout' => 10, + 'verify' => false, + 'timeout' => 10, 'connect_timeout' => 3, 'allow_redirects' => true, ]); @@ -465,9 +484,9 @@ protected function selfDNSTest(Authorization $authorization, $maxAttempts) protected function getSelfTestDNSClient() { return new HttpClient([ - 'base_uri' => 'https://cloudflare-dns.com', + 'base_uri' => 'https://cloudflare-dns.com', 'connect_timeout' => 10, - 'headers' => [ + 'headers' => [ 'Accept' => 'application/dns-json', ], ]); @@ -514,7 +533,7 @@ protected function tosAgree() $this->getUrl(self::DIRECTORY_NEW_ACCOUNT), $this->signPayloadJWK( [ - 'contact' => [ + 'contact' => [ 'mailto:' . $this->getOption('username'), ], 'termsOfServiceAgreed' => true, @@ -535,9 +554,9 @@ 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); + 'basePath', + 'le' + ) . DIRECTORY_SEPARATOR . $userDirectory . ($path === null ? '' : DIRECTORY_SEPARATOR . $path); } /** @@ -593,7 +612,7 @@ protected function request($url, $payload = [], $method = 'POST'): ResponseInter { try { $response = $this->getHttpClient()->request($method, $url, [ - 'json' => $payload, + 'json' => $payload, 'headers' => [ 'Content-Type' => 'application/jose+json', ] @@ -653,9 +672,9 @@ protected function getAccountKey() protected function getJWKHeader(): array { return [ - 'e' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['e']), + 'e' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['e']), 'kty' => 'RSA', - 'n' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['n']), + 'n' => Helper::toSafeString(Helper::getKeyDetails($this->getAccountKey())['rsa']['n']), ]; } @@ -674,10 +693,10 @@ protected function getJWK($url): array $this->nonce = $response->getHeaderLine('replay-nonce'); } return [ - 'alg' => 'RS256', - 'jwk' => $this->getJWKHeader(), + 'alg' => 'RS256', + 'jwk' => $this->getJWKHeader(), 'nonce' => $this->nonce, - 'url' => $url + 'url' => $url ]; } @@ -694,10 +713,10 @@ protected function getKID($url): array $nonce = $response->getHeaderLine('replay-nonce'); return [ - "alg" => "RS256", - "kid" => $this->account->getAccountURL(), + "alg" => "RS256", + "kid" => $this->account->getAccountURL(), "nonce" => $nonce, - "url" => $url + "url" => $url ]; } @@ -723,7 +742,7 @@ protected function signPayloadJWK($payload, $url): array return [ 'protected' => $protected, - 'payload' => $payload, + 'payload' => $payload, 'signature' => Helper::toSafeString($signature), ]; } @@ -749,8 +768,23 @@ protected function signPayloadKid($payload, $url): array return [ 'protected' => $protected, - 'payload' => $payload, + 'payload' => $payload, 'signature' => Helper::toSafeString($signature), ]; } + + /** + * @param string $certificateDownloadLink + * @return string + * @throws \Exception + */ + protected function downloadCertificate(string $certificateDownloadLink): string + { + $certificateResponse = $this->request( + $certificateDownloadLink, + $this->signPayloadKid(null, $certificateDownloadLink) + ); + + return preg_replace('/^[ \t]*[\r\n]+/m', '', (string)$certificateResponse->getBody()); + } } diff --git a/src/Data/Order.php b/src/Data/Order.php index f1f85f5..5f96646 100644 --- a/src/Data/Order.php +++ b/src/Data/Order.php @@ -41,6 +41,11 @@ class Order */ protected $domains; + /** + * @var string + */ + protected $certificate = null; + /** * Order constructor. * @param array $domains @@ -50,6 +55,7 @@ class Order * @param array $identifiers * @param array $authorizations * @param string $finalizeURL + * @param string $certificate * @throws \Exception */ public function __construct( @@ -59,7 +65,8 @@ public function __construct( string $expiresAt, array $identifiers, array $authorizations, - string $finalizeURL + string $finalizeURL, + string $certificate = '' ) { //Handle the microtime date format if (strpos($expiresAt, '.') !== false) { @@ -72,6 +79,7 @@ public function __construct( $this->identifiers = $identifiers; $this->authorizations = $authorizations; $this->finalizeURL = $finalizeURL; + $this->certificate = $certificate; } @@ -146,4 +154,13 @@ public function getDomains(): array { return $this->domains; } + + /** + * Returns domains for the order + * @return null|string + */ + public function getCertificate(): string + { + return $this->certificate; + } } From a070435a6485b96683b0c46d1c761cdaabe3f85f Mon Sep 17 00:00:00 2001 From: Nguyen Huu Phuc Date: Tue, 10 Dec 2024 11:33:08 +0700 Subject: [PATCH 2/7] fixed remove initialIp from letencrypt API --- src/Client.php | 2 +- src/Data/Account.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index ead954c..4997dc8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -378,7 +378,7 @@ public function getAccount(): 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); + return new Account($data['contact'], $date, ($data['status'] == 'valid'), $data['initialIp'] ?? '', $accountURL); } /** diff --git a/src/Data/Account.php b/src/Data/Account.php index 9a957c8..f478fc0 100644 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -90,6 +90,8 @@ public function getContact(): array } /** + * @deprecated + * * Return initial IP * @return string */ From aaadee15045f9c2b0466d73094573db6bdd96ddc Mon Sep 17 00:00:00 2001 From: sasani Date: Thu, 5 Jun 2025 11:53:41 +0330 Subject: [PATCH 3/7] remove contact in account constructor --- src/Client.php | 2 +- src/Data/Account.php | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/Client.php b/src/Client.php index 4a9fe7d..b5707a8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -359,7 +359,7 @@ public function getAccount(): 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'), $accountURL); + return new Account($date, ($data['status'] == 'valid'), $accountURL); } /** diff --git a/src/Data/Account.php b/src/Data/Account.php index 99f8b66..6532f6e 100644 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -4,12 +4,6 @@ class Account { - - /** - * @var array - */ - protected $contact; - /** * @var string */ @@ -28,18 +22,15 @@ class Account /** * Account constructor. - * @param array $contact * @param \DateTime $createdAt * @param bool $isValid * @param string $accountURL */ public function __construct( - array $contact, \DateTime $createdAt, bool $isValid, string $accountURL ) { - $this->contact = $contact; $this->createdAt = $createdAt; $this->isValid = $isValid; $this->accountURL = $accountURL; @@ -72,15 +63,6 @@ public function getAccountURL(): string return $this->accountURL; } - /** - * Return contact data - * @return array - */ - public function getContact(): array - { - return $this->contact; - } - /** * Returns validation status * @return bool From d6ca39caae077a28294a5c2f871a63118866889c Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Huu Date: Wed, 8 Apr 2026 11:04:11 +0700 Subject: [PATCH 4/7] Add support for dns-persist-01 (ACME DNS account persistence) challenge validation --- src/Client.php | 93 +++++++++++++++++++++++++++++++++----- src/Data/Authorization.php | 66 ++++++++++++++++++++++++++- src/Data/Challenge.php | 26 +++++++++-- 3 files changed, 167 insertions(+), 18 deletions(-) diff --git a/src/Client.php b/src/Client.php index 4903460..95cdd8c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -60,6 +60,11 @@ class Client */ const VALIDATION_DNS = 'dns-01'; + /** + * DNS persist validation + */ + const VALIDATION_DNS_PERSIST = 'dns-persist-01'; + /** * @var string */ @@ -234,13 +239,15 @@ public function createOrder(array $domains): Order public function authorize(Order $order): array { $authorizations = []; - foreach ($order->getAuthorizationURLs() as $authorizationURL) { + $identifiers = $order->getIdentifiers(); + foreach ($order->getAuthorizationURLs() as $index => $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()); + $authorization = new Authorization($data['identifier']['value'], $data['expires'], $this->getDigest(), $this->getAccount()->getAccountURL()); + $authorization->setOrderDomain($identifiers[$index]['value']); foreach ($data['challenges'] as $challengeData) { $challenge = new Challenge( @@ -248,7 +255,8 @@ public function authorize(Order $order): array $challengeData['type'], $challengeData['status'], $challengeData['url'], - $challengeData['token'] + $challengeData['token'] ?? null, + $challengeData['issuer-domain-names'] ?? [] ); $authorization->addChallenge($challenge); } @@ -271,6 +279,8 @@ public function selfTest(Authorization $authorization, $type = self::VALIDATION_ return $this->selfHttpTest($authorization, $maxAttempts); } elseif ($type == self::VALIDATION_DNS) { return $this->selfDNSTest($authorization, $maxAttempts); + } elseif ($type == self::VALIDATION_DNS_PERSIST) { + return $this->selfDnsPersistTest($authorization, $maxAttempts); } return false; } @@ -285,14 +295,23 @@ public function selfTest(Authorization $authorization, $type = self::VALIDATION_ */ public function validate(Challenge $challenge, int $maxAttempts = 15): bool { - $this->request( - $challenge->getUrl(), - $this->signPayloadKid([ - 'keyAuthorization' => $challenge->getToken() . '.' . $this->getDigest() - ], $challenge->getUrl()) - ); + if ($challenge->getType() == self::VALIDATION_DNS_PERSIST) { + $this->request( + $challenge->getUrl(), + $this->signPayloadKid( + (object)[], + $challenge->getUrl() + ) + ); + } else { + $this->request( + $challenge->getUrl(), + $this->signPayloadKid([ + 'keyAuthorization' => $challenge->getToken() . '.' . $this->getDigest() + ], $challenge->getUrl()) + ); + } - $data = []; do { $response = $this->request( $challenge->getAuthorizationURL(), @@ -477,6 +496,56 @@ protected function selfDNSTest(Authorization $authorization, $maxAttempts) return false; } + /** + * Self DNS persist test client that uses Cloudflare's DNS API + * Verifies that a _validation-persist TXT record exists with the correct issuer domain and account URI + * @param Authorization $authorization + * @param $maxAttempts + * @return bool + */ + protected function selfDnsPersistTest(Authorization $authorization, int $maxAttempts) + { + $record = $authorization->getDnsPersistRecord(); + if ($record === false) { + return false; + } + + do { + $response = $this->getSelfTestDNSClient()->get( + '/dns-query', + [ + 'query' => [ + 'name' => $record->getName(), + 'type' => 'TXT' + ] + ] + ); + $data = json_decode((string)$response->getBody(), true); + if (isset($data['Answer'])) { + foreach ($data['Answer'] as $result) { + $txtData = trim($result['data'], "\"."); + if ($this->txtRecordContainsAll($txtData, $record->getValue())) { + return true; + } + } + } + if ($maxAttempts > 1) { + sleep(ceil(45 / $maxAttempts)); + } + $maxAttempts--; + } while ($maxAttempts > 0); + + return false; + } + + protected function txtRecordContainsAll(string $actual, string $expected): bool + { + $expectedSegments = array_map('trim', explode(';', $expected)); + $actualSegments = array_map('trim', explode(';', $actual)); + + return empty(array_diff($expectedSegments, $actualSegments)); + } + /** * Return the preconfigured client to call Cloudflare's DNS API * @return HttpClient @@ -730,7 +799,7 @@ protected function getKID($url): array */ protected function signPayloadJWK($payload, $url): array { - $payload = is_array($payload) ? str_replace('\\/', '/', json_encode($payload)) : ''; + $payload = (is_array($payload) || is_object($payload)) ? str_replace('\\/', '/', json_encode($payload)) : ''; $payload = Helper::toSafeString($payload); $protected = Helper::toSafeString(json_encode($this->getJWK($url))); @@ -757,7 +826,7 @@ protected function signPayloadJWK($payload, $url): array */ protected function signPayloadKid($payload, $url): array { - $payload = is_array($payload) ? str_replace('\\/', '/', json_encode($payload)) : ''; + $payload = (is_array($payload) || is_object($payload)) ? str_replace('\\/', '/', json_encode($payload)) : ''; $payload = Helper::toSafeString($payload); $protected = Helper::toSafeString(json_encode($this->getKID($url))); diff --git a/src/Data/Authorization.php b/src/Data/Authorization.php index 5864ab6..36e6ca1 100644 --- a/src/Data/Authorization.php +++ b/src/Data/Authorization.php @@ -13,6 +13,11 @@ class Authorization */ protected $domain; + /** + * @var string|null + */ + protected $orderDomain; + /** * @var \DateTime */ @@ -28,18 +33,41 @@ class Authorization */ protected $digest; + /** + * @var string + */ + protected $accountUri; + /** * Authorization constructor. * @param string $domain * @param string $expires * @param string $digest + * @param string $accountUri * @throws \Exception */ - public function __construct(string $domain, string $expires, string $digest) + public function __construct(string $domain, string $expires, string $digest, string $accountUri) { $this->domain = $domain; $this->expires = (new \DateTime())->setTimestamp(strtotime($expires)); $this->digest = $digest; + $this->accountUri = $accountUri; + } + + /** + * @param string $orderDomain + */ + public function setOrderDomain(string $orderDomain) + { + $this->orderDomain = $orderDomain; + } + + /** + * @return string|null + */ + public function getOrderDomain(): ?string + { + return $this->orderDomain; } /** @@ -108,6 +136,21 @@ public function getDnsChallenge() return false; } + /** + * Return the DNS persist challenge + * @return Challenge|bool + */ + public function getDnsPersistChallenge() + { + foreach ($this->getChallenges() as $challenge) { + if ($challenge->getType() == Client::VALIDATION_DNS_PERSIST) { + return $challenge; + } + } + + return false; + } + /** * Return File object for the given challenge * @return File|bool @@ -137,4 +180,25 @@ public function getTxtRecord() return false; } + + /** + * Returns the DNS persist record object for dns-persist-01 validation + * + * @return Record|bool + */ + public function getDnsPersistRecord() + { + $challenge = $this->getDnsPersistChallenge(); + if ($challenge === false) { + return false; + } + + $issuerDomainNames = $challenge->getIssuerDomainNames(); + if (empty($issuerDomainNames)) { + return false; + } + + $value = $issuerDomainNames[0] . '; accounturi=' . $this->accountUri; + return new Record('_validation-persist.' . $this->getDomain(), $value); + } } diff --git a/src/Data/Challenge.php b/src/Data/Challenge.php index 95493b3..64cc6ab 100644 --- a/src/Data/Challenge.php +++ b/src/Data/Challenge.php @@ -26,25 +26,32 @@ class Challenge protected $url; /** - * @var string + * @var string|null */ protected $token; + /** + * @var array + */ + protected $issuerDomainNames; + /** * Challenge constructor. * @param string $authorizationURL * @param string $type * @param string $status * @param string $url - * @param string $token + * @param string|null $token + * @param array $issuerDomainNames */ - public function __construct(string $authorizationURL, string $type, string $status, string $url, string $token) + public function __construct(string $authorizationURL, string $type, string $status, string $url, ?string $token = null, array $issuerDomainNames = []) { $this->authorizationURL = $authorizationURL; $this->type = $type; $this->status = $status; $this->url = $url; $this->token = $token; + $this->issuerDomainNames = $issuerDomainNames; } /** @@ -67,9 +74,9 @@ public function getType(): string /** * Returns the token - * @return string + * @return string|null */ - public function getToken(): string + public function getToken(): ?string { return $this->token; } @@ -91,4 +98,13 @@ public function getAuthorizationURL(): string { return $this->authorizationURL; } + + /** + * Returns the issuer domain names (used by dns-persist-01) + * @return array + */ + public function getIssuerDomainNames(): array + { + return $this->issuerDomainNames; + } } From 7cc1e4cd5980a1ae054c3c2ff8044af9450e1d45 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Huu Date: Wed, 8 Apr 2026 14:14:58 +0700 Subject: [PATCH 5/7] Pass orderDomain via Authorization constructor and add wildcard policy to dns-persist-01 validation record --- src/Client.php | 3 +-- src/Data/Authorization.php | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Client.php b/src/Client.php index 95cdd8c..fa6fe81 100644 --- a/src/Client.php +++ b/src/Client.php @@ -246,8 +246,7 @@ public function authorize(Order $order): array $this->signPayloadKid(null, $authorizationURL) ); $data = json_decode((string)$response->getBody(), true); - $authorization = new Authorization($data['identifier']['value'], $data['expires'], $this->getDigest(), $this->getAccount()->getAccountURL()); - $authorization->setOrderDomain($identifiers[$index]['value']); + $authorization = new Authorization($data['identifier']['value'], $identifiers[$index]['value'], $data['expires'], $this->getDigest(), $this->getAccount()->getAccountURL()); foreach ($data['challenges'] as $challengeData) { $challenge = new Challenge( diff --git a/src/Data/Authorization.php b/src/Data/Authorization.php index 36e6ca1..d592fa1 100644 --- a/src/Data/Authorization.php +++ b/src/Data/Authorization.php @@ -41,24 +41,18 @@ class Authorization /** * Authorization constructor. * @param string $domain + * @param string $orderDomain * @param string $expires * @param string $digest * @param string $accountUri * @throws \Exception */ - public function __construct(string $domain, string $expires, string $digest, string $accountUri) + public function __construct(string $domain, string $orderDomain, string $expires, string $digest, string $accountUri) { $this->domain = $domain; $this->expires = (new \DateTime())->setTimestamp(strtotime($expires)); $this->digest = $digest; $this->accountUri = $accountUri; - } - - /** - * @param string $orderDomain - */ - public function setOrderDomain(string $orderDomain) - { $this->orderDomain = $orderDomain; } @@ -199,6 +193,17 @@ public function getDnsPersistRecord() } $value = $issuerDomainNames[0] . '; accounturi=' . $this->accountUri; + + if ($this->isWildcard()) { + $value .= '; policy=wildcard'; + } + return new Record('_validation-persist.' . $this->getDomain(), $value); } + + private function isWildcard(): bool + { + $orderDomain = $this->getOrderDomain(); + return $orderDomain && strpos($orderDomain, '*.') === 0; + } } From cc9d3e55199842483ce7f03cd43dcefbd5085a28 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Huu Date: Wed, 8 Apr 2026 16:19:00 +0700 Subject: [PATCH 6/7] Refactor Authorization to derive wildcard status from order identifiers instead of passing orderDomain through constructor --- src/Client.php | 20 +++++++++++++++++--- src/Data/Authorization.php | 38 +++++++++++--------------------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/Client.php b/src/Client.php index fa6fe81..9e8322e 100644 --- a/src/Client.php +++ b/src/Client.php @@ -239,14 +239,17 @@ public function createOrder(array $domains): Order public function authorize(Order $order): array { $authorizations = []; - $identifiers = $order->getIdentifiers(); - foreach ($order->getAuthorizationURLs() as $index => $authorizationURL) { + 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'], $identifiers[$index]['value'], $data['expires'], $this->getDigest(), $this->getAccount()->getAccountURL()); + $isWildcard = $this->isWildcardDomain($data['identifier']['value'], $order); + $authorization = new Authorization($data['identifier']['value'], $data['expires'], $this->getDigest(), [ + 'isWildcard' => $isWildcard, + 'accountUri' => $this->getAccount()->getAccountURL() + ]); foreach ($data['challenges'] as $challengeData) { $challenge = new Challenge( @@ -855,4 +858,15 @@ protected function downloadCertificate(string $certificateDownloadLink): string return preg_replace('/^[ \t]*[\r\n]+/m', '', (string)$certificateResponse->getBody()); } + + private function isWildcardDomain(string $domain, Order $order): bool + { + $wildcardDomain = '*.' . $domain; + foreach ($order->getIdentifiers() as $identifier) { + if ($wildcardDomain === $identifier['value']) { + return true; + } + } + return false; + } } diff --git a/src/Data/Authorization.php b/src/Data/Authorization.php index d592fa1..7fd794f 100644 --- a/src/Data/Authorization.php +++ b/src/Data/Authorization.php @@ -13,11 +13,6 @@ class Authorization */ protected $domain; - /** - * @var string|null - */ - protected $orderDomain; - /** * @var \DateTime */ @@ -34,34 +29,29 @@ class Authorization protected $digest; /** - * @var string + * @var string|null */ protected $accountUri; + /** + * @var bool + */ + protected $isWildcard; + /** * Authorization constructor. * @param string $domain - * @param string $orderDomain * @param string $expires * @param string $digest - * @param string $accountUri - * @throws \Exception + * @param array $options */ - public function __construct(string $domain, string $orderDomain, string $expires, string $digest, string $accountUri) + public function __construct(string $domain, string $expires, string $digest, array $options = []) { $this->domain = $domain; $this->expires = (new \DateTime())->setTimestamp(strtotime($expires)); $this->digest = $digest; - $this->accountUri = $accountUri; - $this->orderDomain = $orderDomain; - } - - /** - * @return string|null - */ - public function getOrderDomain(): ?string - { - return $this->orderDomain; + $this->accountUri = $options['accountUri'] ?? null; + $this->isWildcard = $options['isWildcard'] ?? false; } /** @@ -194,16 +184,10 @@ public function getDnsPersistRecord() $value = $issuerDomainNames[0] . '; accounturi=' . $this->accountUri; - if ($this->isWildcard()) { + if ($this->isWildcard) { $value .= '; policy=wildcard'; } return new Record('_validation-persist.' . $this->getDomain(), $value); } - - private function isWildcard(): bool - { - $orderDomain = $this->getOrderDomain(); - return $orderDomain && strpos($orderDomain, '*.') === 0; - } } From 7139b8f5a4ae2c71e9c85a10673441f14e207b0c Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Huu Date: Wed, 8 Apr 2026 17:22:53 +0700 Subject: [PATCH 7/7] Use ACME API wildcard field directly, add isWildcard() accessor, and fix dns-persist-01 validation checks --- src/Client.php | 28 ++++++++++++---------------- src/Data/Authorization.php | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Client.php b/src/Client.php index 9e8322e..7dab1d2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -245,9 +245,8 @@ public function authorize(Order $order): array $this->signPayloadKid(null, $authorizationURL) ); $data = json_decode((string)$response->getBody(), true); - $isWildcard = $this->isWildcardDomain($data['identifier']['value'], $order); $authorization = new Authorization($data['identifier']['value'], $data['expires'], $this->getDigest(), [ - 'isWildcard' => $isWildcard, + 'wildcard' => $data['wildcard'] ?? false, 'accountUri' => $this->getAccount()->getAccountURL() ]); @@ -314,6 +313,7 @@ public function validate(Challenge $challenge, int $maxAttempts = 15): bool ); } + $data = []; do { $response = $this->request( $challenge->getAuthorizationURL(), @@ -499,10 +499,10 @@ protected function selfDNSTest(Authorization $authorization, $maxAttempts) } /** - * Self DNS persist test client that uses Cloudflare's DNS API - * Verifies that a _validation-persist TXT record exists with the correct issuer domain and account URI + * Self-test for dns-persist-01 validation using Cloudflare's DNS API. + * Verifies that a _validation-persist TXT record exists with the correct issuer domain and account URI. * @param Authorization $authorization - * @param $maxAttempts + * @param int $maxAttempts * @return bool */ protected function selfDnsPersistTest(Authorization $authorization, int $maxAttempts) @@ -525,7 +525,7 @@ protected function selfDnsPersistTest(Authorization $authorization, int $maxAtte $data = json_decode((string)$response->getBody(), true); if (isset($data['Answer'])) { foreach ($data['Answer'] as $result) { - $txtData = trim($result['data'], "\"."); + $txtData = trim($result['data'], "\""); if ($this->txtRecordContainsAll($txtData, $record->getValue())) { return true; } @@ -540,6 +540,12 @@ protected function selfDnsPersistTest(Authorization $authorization, int $maxAtte return false; } + /** + * Check if the actual TXT record contains all expected semicolon-separated segments. + * @param string $actual + * @param string $expected + * @return bool + */ protected function txtRecordContainsAll(string $actual, string $expected): bool { $expectedSegments = array_map('trim', explode(';', $expected)); @@ -859,14 +865,4 @@ protected function downloadCertificate(string $certificateDownloadLink): string return preg_replace('/^[ \t]*[\r\n]+/m', '', (string)$certificateResponse->getBody()); } - private function isWildcardDomain(string $domain, Order $order): bool - { - $wildcardDomain = '*.' . $domain; - foreach ($order->getIdentifiers() as $identifier) { - if ($wildcardDomain === $identifier['value']) { - return true; - } - } - return false; - } } diff --git a/src/Data/Authorization.php b/src/Data/Authorization.php index 7fd794f..e397bbc 100644 --- a/src/Data/Authorization.php +++ b/src/Data/Authorization.php @@ -51,7 +51,7 @@ public function __construct(string $domain, string $expires, string $digest, arr $this->expires = (new \DateTime())->setTimestamp(strtotime($expires)); $this->digest = $digest; $this->accountUri = $options['accountUri'] ?? null; - $this->isWildcard = $options['isWildcard'] ?? false; + $this->isWildcard = $options['wildcard'] ?? false; } /** @@ -72,6 +72,16 @@ public function getDomain(): string return $this->domain; } + /** + * Return the order is wildcard or not + * + * @return bool + */ + public function isWildcard(): bool + { + return $this->isWildcard; + } + /** * Return the expiry of the authorization @@ -178,7 +188,7 @@ public function getDnsPersistRecord() } $issuerDomainNames = $challenge->getIssuerDomainNames(); - if (empty($issuerDomainNames)) { + if (empty($issuerDomainNames) || $this->accountUri === null) { return false; }