From 792a76569d168b9cd7ff027027392b48c36c4769 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 24 Feb 2025 19:18:51 -0800 Subject: [PATCH] acme: add support for profiles (#10196) Recognizes the profiles map in the "meta" section of directory. Allows sending a "profile" field in order objects. Adds an optional "profile" parameter to new_order in client.py. Related to #10194. --- acme/acme/_internal/tests/messages_test.py | 23 ++++++++++++++++++++-- acme/acme/client.py | 6 ++++-- acme/acme/messages.py | 5 +++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/acme/acme/_internal/tests/messages_test.py b/acme/acme/_internal/tests/messages_test.py index a47541efd..76757f19c 100644 --- a/acme/acme/_internal/tests/messages_test.py +++ b/acme/acme/_internal/tests/messages_test.py @@ -162,6 +162,10 @@ class DirectoryTest(unittest.TestCase): terms_of_service='https://example.com/acme/terms', website='https://www.example.com/', caa_identities=['example.com'], + profiles={ + "example": "some profile", + "other example": "a different profile" + } ), }) @@ -191,6 +195,10 @@ class DirectoryTest(unittest.TestCase): 'termsOfService': 'https://example.com/acme/terms', 'website': 'https://www.example.com/', 'caaIdentities': ['example.com'], + 'profiles': { + 'example': 'some profile', + 'other example': 'a different profile' + } }, } @@ -528,14 +536,25 @@ class NewOrderTest(unittest.TestCase): def setUp(self): from acme.messages import NewOrder - self.reg = NewOrder( + self.order = NewOrder( identifiers=mock.sentinel.identifiers) def test_to_partial_json(self): - assert self.reg.to_json() == { + assert self.order.to_json() == { 'identifiers': mock.sentinel.identifiers, } + def test_default_profile_empty(self): + assert self.order.profile is None + + def test_non_empty_profile(self): + from acme.messages import NewOrder + order = NewOrder(identifiers=mock.sentinel.identifiers, profile='example') + assert order.to_json() == { + 'identifiers': mock.sentinel.identifiers, + 'profile': 'example', + } + class JWSPayloadRFC8555Compliant(unittest.TestCase): """Test for RFC8555 compliance of JWS generated from resources/challenges""" diff --git a/acme/acme/client.py b/acme/acme/client.py index c035739b4..e5f610f2c 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -116,7 +116,7 @@ class ClientV2: self.net.account = new_regr return new_regr - def new_order(self, csr_pem: bytes) -> messages.OrderResource: + def new_order(self, csr_pem: bytes, profile: Optional[str] = None) -> messages.OrderResource: """Request a new Order object from the server. :param bytes csr_pem: A CSR in PEM format. @@ -139,7 +139,9 @@ class ClientV2: for ip in ipNames: identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_IP, value=str(ip))) - order = messages.NewOrder(identifiers=identifiers) + if profile is None: + profile = "" + order = messages.NewOrder(identifiers=identifiers, profile=profile) response = self._post(self.directory['newOrder'], order) body = messages.Order.from_json(response.json()) authorizations = [] diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 08b712dd7..46593d82c 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -232,6 +232,7 @@ class Directory(jose.JSONDeSerializable): website: str = jose.field('website', omitempty=True) caa_identities: List[str] = jose.field('caaIdentities', omitempty=True) external_account_required: bool = jose.field('externalAccountRequired', omitempty=True) + profiles: Dict[str, str] = jose.field('profiles', omitempty=True) def __init__(self, **kwargs: Any) -> None: kwargs = {self._internal_name(k): v for k, v in kwargs.items()} @@ -624,6 +625,8 @@ class Revocation(jose.JSONObjectWithFields): class Order(ResourceBody): """Order Resource Body. + :ivar profile: The profile to request. + :vartype profile: str :ivar identifiers: List of identifiers for the certificate. :vartype identifiers: `list` of `.Identifier` :ivar acme.messages.Status status: @@ -635,6 +638,8 @@ class Order(ResourceBody): :ivar datetime.datetime expires: When the order expires. :ivar ~.Error error: Any error that occurred during finalization, if applicable. """ + # https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ + profile: str = jose.field('profile', omitempty=True) identifiers: List[Identifier] = jose.field('identifiers', omitempty=True) status: Status = jose.field('status', decoder=Status.from_json, omitempty=True) authorizations: List[str] = jose.field('authorizations', omitempty=True)