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.
This commit is contained in:
Jacob Hoffman-Andrews 2025-02-24 19:18:51 -08:00 committed by GitHub
parent 9105cd21ba
commit 792a76569d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 30 additions and 4 deletions

View file

@ -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"""

View file

@ -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 = []

View file

@ -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)