diff --git a/acme/acme/client.py b/acme/acme/client.py index 6c8d5cbbc..90edf19d2 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -10,6 +10,7 @@ import time import six from six.moves import http_client # pylint: disable=import-error +import crypto_util import josepy as jose import OpenSSL import re @@ -199,26 +200,6 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes response, authzr.body.identifier, authzr.uri) return updated_authzr, response - def revoke(self, cert, rsn): - """Revoke certificate. - - :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` - - :param int rsn: Reason code for certificate revocation. - - :raises .ClientError: If revocation is unsuccessful. - - """ - response = self.net.post(self.directory[messages.Revocation], - messages.Revocation( - certificate=cert, - reason=rsn), - content_type=None) - if response.status_code != http_client.OK: - raise errors.ClientError( - 'Successful revocation must return HTTP OK status') - class Client(ClientBase): """ACME client for a v1 API. @@ -563,9 +544,14 @@ class ClientV2(ClientBase): :rtype: `list` of `.AuthorizationResource` """ csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) - order = messages.NewOrder(csr=jose.ComparableX509(csr)) + wrapped_csr = jose.ComparableX509(csr) + identifiers = [] + for name in crypto_util._pyopenssl_cert_or_req_san(csr): + identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN, + value=name)) + order = messages.NewOrder(identifiers=identifiers) response = self.net.post(self.directory.new_order, order) - order_response = self._order_resource_from_response(response) + order_response = self._order_resource_from_response(response, csr=wrapped_csr) return order_response def poll_order_and_request_issuance(self, orderr): @@ -600,7 +586,7 @@ class ClientV2(ClientBase): latest = self._order_resource_from_response(response, uri=orderr.uri) return latest - def _order_resource_from_response(self, response, uri=None): + def _order_resource_from_response(self, response, uri=None, csr=None): body = messages.Order.from_json(response.json()) authorizations = [] for url in body.authorizations: @@ -614,7 +600,35 @@ class ClientV2(ClientBase): body=body, uri=response.headers.get('Location', uri), fullchain_pem=fullchain_pem, - authorizations=authorizations) + authorizations=authorizations, + csr=csr) + + def poll_order_and_request_issuance(self, orderr, max_time=datetime.timedelta(seconds=90)): + """Poll Order Resource for status. + responses = [] + deadline = datetime.datetime.now() + max_time + for url in orderr.body.authorizations: + while datetime.datetime.now() < deadline: + time.sleep(1) + authzr = self._authzr_from_response(self.net.get(url), uri=url) + if authzr.body.status != messages.STATUS_PENDING: + responses.append(authzr) + break + for authzr in responses: + if authzr.body.status != messages.STATUS_VALID: + for chall in authzr.body.challenges: + if chall.error != None: + raise Exception("failed challenge for %s: %s" % + (authzr.body.identifier.value, chall.error)) + raise Exception("failed authorization: %s" % authzr.body) + latest = self._order_resource_from_response(self.net.get(orderr.uri), uri=orderr.uri) + self.net.post(latest.body.finalize, messages.CertificateRequest(csr=orderr.csr)) + while datetime.datetime.now() < deadline: + time.sleep(1) + latest = self._order_resource_from_response(self.net.get(orderr.uri), uri=orderr.uri) + if latest.fullchain_pem is not None: + return latest + return None class ClientNetwork(object): # pylint: disable=too-many-instance-attributes diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 6d36cbffa..a48a81661 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -1,5 +1,6 @@ """ACME protocol messages.""" import collections +import re import six import josepy as jose @@ -171,9 +172,9 @@ class Directory(jose.JSONDeSerializable): class Meta(jose.JSONObjectWithFields): """Directory Meta.""" - terms_of_service = jose.Field('terms-of-service', omitempty=True) + terms_of_service = jose.Field('termsOfService', omitempty=True) website = jose.Field('website', omitempty=True) - caa_identities = jose.Field('caa-identities', omitempty=True) + caa_identities = jose.Field('caaIdentities', omitempty=True) @classmethod def _canon_key(cls, key): @@ -193,11 +194,19 @@ class Directory(jose.JSONDeSerializable): # not clear on that self._jobj = canon_jobj + def _camelCase(self, name): + """Convert a snake_case name to camelCase.""" + return re.sub('_([a-z])', lambda x: x.group(1).upper(), name) + def __getattr__(self, name): try: return self[name.replace('_', '-')] except KeyError as error: - raise AttributeError(str(error) + ': ' + name) + try: + print self._camelCase(name) + return self[self._camelCase(name)] + except KeyError as error: + raise AttributeError(str(error) + ': ' + name) def __getitem__(self, name): try: @@ -251,7 +260,7 @@ class Registration(ResourceBody): contact = jose.Field('contact', omitempty=True, default=()) agreement = jose.Field('agreement', omitempty=True) status = jose.Field('status', omitempty=True) - terms_of_service_agreed = jose.Field('terms-of-service-agreed', omitempty=True) + terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True) phone_prefix = 'tel:' email_prefix = 'mailto:' @@ -494,10 +503,11 @@ class Order(ResourceBody): :ivar datetime.datetime expires: """ - csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) - status = jose.Field('status', omitempty=True) + identifiers = jose.Field('identifiers', omitempty=True) + status = jose.Field('status', omitempty=True, default=None) authorizations = jose.Field('authorizations', omitempty=True) certificate = jose.Field('certificate', omitempty=True) + finalize = jose.Field('finalize', omitempty=True) expires = fields.RFC3339Field('expires', omitempty=True) class OrderResource(ResourceWithURI): @@ -507,6 +517,7 @@ class OrderResource(ResourceWithURI): """ body = jose.Field('body', decoder=Order.from_json) + csr = jose.Field('csr', omitempty=True) authorizations = jose.Field('authorizations') fullchain_pem = jose.Field('fullchain_pem', omitempty=True)