From 22dcfe48c5390171ae8aa586b9cb8225976c38a0 Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Mon, 7 Sep 2026 16:28:39 +0200 Subject: [PATCH] Poll HTTP-01 challenge with POST-as-GET instead of re-sending the trigger The wait loop in signDomains() re-POSTs the {"keyAuthorization": ...} trigger payload on every iteration. Boulder accepts that only while the authorization is "pending": once validation completes it answers 400 malformed - Unable to update challenge :: authorization must be pending so the exception is thrown at the moment the challenge actually succeeds, and the order is abandoned. Any domain that becomes valid during the loop's first sleep(1) can therefore never be issued. Send the trigger once and poll with POST-as-GET afterwards, as RFC 8555 7.5.1 prescribes and as the order-status loop below already does. signedRequest() encodes an empty/null payload as "", so no other change is needed. --- Lescript.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lescript.php b/Lescript.php index b81d01f..deb1f18 100644 --- a/Lescript.php +++ b/Lescript.php @@ -159,9 +159,14 @@ function ($domain) { $result = null; while ($loopCount < $maxAllowedLoops) { + // Only the first request may carry the trigger payload: the + // server accepts it while the authorization is "pending" and + // rejects it with 400 "Unable to update challenge :: + // authorization must be pending" as soon as validation has + // completed. Poll with POST-as-GET afterwards (RFC 8555 7.5.1). $result = $this->signedRequest( $challenge['url'], - array("keyAuthorization" => $payload) + $loopCount === 1 ? array("keyAuthorization" => $payload) : null ); if (empty($result['status']) || $result['status'] == "invalid") {