Skip to content

Commit

Permalink
AcmeClient.get_challeges replaced with AcmeClient.get_authorization_s…
Browse files Browse the repository at this point in the history
…tatus
  • Loading branch information
dvolodin7 committed Nov 23, 2023
1 parent b325cf3 commit ed94f4b
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 33 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

To see unreleased changes, please see the [CHANGELOG on the master branch](https://github.com/gufolabs/gufo_acme/blob/master/CHANGELOG.md) guide.

## [Unreleased]

## Added

* `AcmeAuthorizationStatus` structure

## Changed

* `AcmeClient.get_challenges` replaced with `AcmeClient.get_authorization_status`.
* Respond to challenges only if authorization status is `pending`.

## 0.3.0 - 2023-11-23

## Added
Expand Down
72 changes: 39 additions & 33 deletions src/gufo/acme/clients/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from ..log import logger
from ..types import (
AcmeAuthorization,
AcmeAuthorizationStatus,
AcmeChallenge,
AcmeDirectory,
AcmeOrder,
Expand Down Expand Up @@ -505,11 +506,11 @@ async def new_order(
finalize=data["finalize"],
)

async def get_challenges(
async def get_authorization_status(
self: "AcmeClient", auth: AcmeAuthorization
) -> List[AcmeChallenge]:
) -> AcmeAuthorizationStatus:
"""
Get a challenge for an authoriations.
Get an authorization status.
Performs RFC-8555 pp. 7.5 sequence.
Expand All @@ -525,7 +526,7 @@ async def get_challenges(
"sub.example.com"
])
for auth in order.authorizations:
challenges = await client.get_challenges(auth)
auth_status = await client.get_authorization_status(auth)
```
Args:
Expand All @@ -538,14 +539,17 @@ async def get_challenges(
Raises:
AcmeError: In case of the errors.
"""
logger.warning("Getting challenges for %s", auth.domain)
logger.warning("Getting authorization status for %s", auth.domain)
self._check_bound()
resp = await self._post(auth.url, None)
data = resp.json()
return [
AcmeChallenge(type=d["type"], url=d["url"], token=d["token"])
for d in data["challenges"]
]
return AcmeAuthorizationStatus(
status=data["status"],
challenges=[
AcmeChallenge(type=d["type"], url=d["url"], token=d["token"])
for d in data["challenges"]
],
)

async def respond_challenge(
self: "AcmeClient", challenge: AcmeChallenge
Expand All @@ -559,7 +563,7 @@ async def respond_challenge(
Args:
challenge: ACME challenge as returned by
`get_challenges` function.
`get_authorization_status` function.
"""
logger.warning("Responding challenge %s", challenge.type)
self._check_bound()
Expand All @@ -574,21 +578,18 @@ async def wait_for_authorization(
Args:
auth: ACME Authorization
"""
logger.warning("Polling authorization for %s", auth.domain)
while True:
logger.warning("Polling authorization for %s", auth.domain)
self._check_bound()
resp = await self._post(auth.url, None)
data = resp.json()
status = data.get("status") or "pending"
status = await self.get_authorization_status(auth)
logger.warning(
"Authorization status for %s is %s", auth.domain, status
"Authorization status for %s is %s", auth.domain, status.status
)
if status == "valid":
if status.status == "valid":
return
if status == "pending":
if status.status == "pending":
await self._random_delay(3.0)
else:
msg = f"Status is {status}"
msg = f"Status is {status.status}"
raise AcmeAuthorizationError(msg)

@staticmethod
Expand Down Expand Up @@ -722,20 +723,25 @@ async def fulfill_http_01(
# Process authorizations
for auth in order.authorizations:
logger.warning("Processing authorization for %s", auth.domain)
# Get challenges
challenges = await self.get_challenges(auth)
fulfilled_challenge = None
for ch in challenges:
if await self.fulfill_challenge(domain, ch):
await self.respond_challenge(ch)
fulfilled_challenge = ch
break
else:
raise AcmeFulfillmentFailed
# Wait for authorization became valid
await self._wait_for(self.wait_for_authorization(auth), 60.0)
# Clear challenge
await self.clear_challenge(domain, fulfilled_challenge)
# Get authorization status.
auth_status = await self.get_authorization_status(auth)
if auth_status.status == "pending":
# Get challenges
fulfilled_challenge = None
for ch in auth_status.challenges:
if await self.fulfill_challenge(domain, ch):
await self.respond_challenge(ch)
fulfilled_challenge = ch
break
else:
raise AcmeFulfillmentFailed
# Wait for authorization became valid
await self._wait_for(self.wait_for_authorization(auth), 60.0)
# Clear challenge
await self.clear_challenge(domain, fulfilled_challenge)
elif auth_status.status != "valid":
msg = f"Status is {auth_status.status}"
raise AcmeAuthorizationError(msg)
# Finalize order and get certificate
return await self._wait_for(
self.finalize_and_wait(order, csr=csr), 60.0
Expand Down
14 changes: 14 additions & 0 deletions src/gufo/acme/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ class AcmeChallenge(object):
token: str


@dataclass
class AcmeAuthorizationStatus(object):
"""
Authorization status response.
Attributes:
status: Current status.
challenges: List of ACME challenge.
"""

status: str
challenges: List[AcmeChallenge]


@dataclass
class AcmeDirectory(object):
"""
Expand Down
42 changes: 42 additions & 0 deletions tests/clients/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,45 @@ def test_valid_order_status() -> None:
resp = httpx.Response(200, json={"status": "valid"})
s = AcmeClient._get_order_status(resp)
assert s == "valid"


@pytest.mark.parametrize(
("input", "expected"),
[
(
"0twaM-fK_6yfQ_rxH4eZdqj1O6blhB2",
b"\xd2\xdc\x1a3\xe7\xca\xff\xac\x9fC\xfa\xf1\x1f\x87\x99v\xa8\xf5;\xa6\xe5\x84\x1d",
),
(
"0twaM+fK_6yfQ/rxH4eZdqj1O6blhB2",
b"\xd2\xdc\x1a3\xe7\xca\xff\xac\x9fC\xfa\xf1\x1f\x87\x99v\xa8\xf5;\xa6\xe5\x84\x1d",
),
],
)
def test_decode_auto_base64(input: str, expected: bytes) -> None:
r = AcmeClient.decode_auto_base64(input)
assert r == expected


TEST_EAB_KID = "53271e20d46fc7462c1b615ef239e853"
TEST_EAB_HMAC = (
"WA2XYh7UvKnG0twaM-fK_6yfQ_rxH4eZdqj1O6blhB2RIwyh3KjFDIrPyUZk"
"ao5EyyPaUaYmk1Hl24LwgmqdEA"
)


def test_get_eab() -> None:
client = AcmeClient(LE_STAGE_DIRECTORY, key=KEY)
eab = client._get_eab(
ExternalAccountBinding(
kid=TEST_EAB_KID,
hmac_key=AcmeClient.decode_auto_base64(TEST_EAB_HMAC),
),
"http://127.0.0.1/new_account",
)
print(eab)
assert eab == {
"protected": "eyJhbGciOiAiSFMyNTYiLCAia2lkIjogIjUzMjcxZTIwZDQ2ZmM3NDYyYzFiNjE1ZWYyMzllODUzIiwgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xL25ld19hY2NvdW50In0",
"signature": "WdpEOa1e-Q9zyib7xzAFq3MrbUj82Gwt9RcLqqsr__Y",
"payload": "eyJuIjogImd2dmpvSlBkMUw0c3ExYlQwcTJDOTROM1dWN1c3bHJvQV9NekYtU0dNVllGYXNJMmx2cXcza0FrRlJ4RzM2NkpmSHIzQjFSLXhsQ3pFUEhOaXhiTDZiMGNjdlBGWlpzdW5nbng1bV91R0wyRk1paXN1MTg2ZE1uZnNrNllzc3ZlYm94aVFYRWhHTXhJOVQ2R2pFNmw2ZWMxUEdZNXVCNzB2UDJ3a0dQeGt2UkxEMnRHYWVfLTdrQ2dSenZGMnhPYUdaalQtanhIY1lwV3V0Tk4tcVF6RG9IbmhMdTBMSXdXbFhCYXpBczZ6YmtQdlBXOVBOWkFVZW5jV3h4UTVoSnRMa1ZTdmdTWXd6STFjeGxyQzhsQ2pnNnJJUjlMQThzNVBMemVlX25Fb3RsamxVMGxqWHozZXlEOVc0Zmw0ckM0NnY4LXVmazVFejl1dFFRMnNWaklNUSIsICJlIjogIkFRQUIiLCAia3R5IjogIlJTQSJ9",
}

0 comments on commit ed94f4b

Please sign in to comment.