From 40392918db6f8f255c938973bbd489f698941bab Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 4 Dec 2017 14:39:48 -0800 Subject: [PATCH 01/14] Tidy --- acme/acme/client.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 26ee6e6ad..d9b4ec2e7 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -185,18 +185,17 @@ class Client(object): # pylint: disable=too-many-instance-attributes return authzr def _order_resource_from_response(self, response, uri=None): + body = messages.Order.from_json(response.json()) authorizations = [] - for authz_uri in response.json()["authorizations"]: - authz_response = self.net.get(authz_uri) - authorizations.append(self._authzr_from_response(authz_response)) + for url in body.authorizations: + authorizations.append(self._authzr_from_response(self.net.get(url))) fullchain_pem = None - if "certificate" in response.json(): - certificate_response = self.net.get(response.json()["certificate"], - content_type=None) - if certificate_response.ok: - fullchain_pem = certificate_response.text + if body.certificate is not None: + certificate_response = self.net.get(body.certificate, content_type=None) + if certificate_response.ok: + fullchain_pem = certificate_response.text return messages.OrderResource( - body=messages.Order.from_json(response.json()), + body=body, uri=response.headers.get('Location', uri), fullchain_pem=fullchain_pem, authorizations=authorizations) From d9616c44f4e0d30b243577c9123d99f473376fbe Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 5 Dec 2017 15:59:56 -0800 Subject: [PATCH 02/14] Remove noisy print. --- acme/acme/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index d9b4ec2e7..e6acdeb2d 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -339,7 +339,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes time.sleep(1) else: break - print latest.to_json() for authz in latest.authorizations: if authz.body.status != messages.STATUS_VALID: for chall in authz.body.challenges: From 2916ec896c79aeec00071d2bb1c980d042248a55 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 5 Dec 2017 22:47:04 -0800 Subject: [PATCH 03/14] Use identifiers flow, and poll differently. --- acme/acme/client.py | 32 ++++++++++++++++++++------------ acme/acme/messages.py | 4 ++-- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index e6acdeb2d..c7ed7337c 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 OpenSSL import re import requests @@ -207,7 +208,11 @@ class Client(object): # pylint: disable=too-many-instance-attributes :rtype: `list` of `.AuthorizationResource` """ csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) - order = messages.NewOrder(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) return order_response @@ -332,24 +337,27 @@ class Client(object): # pylint: disable=too-many-instance-attributes :rtype: (`.OrderResource`) """ - while True: - response = self.net.get(orderr.uri) - latest = self._order_resource_from_response(response, uri=orderr.uri) - if any([a.body.status == messages.STATUS_PENDING for a in latest.authorizations]): - time.sleep(1) - else: - break - for authz in latest.authorizations: - if authz.body.status != messages.STATUS_VALID: + responses = [] + for url in orderr.body.authorizations: + while True: + 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 authz.body.challenges: if chall.error != None: raise Exception("failed challenge for %s: %s" % (authz.body.identifier.value, chall.error)) raise Exception("failed authorization: %s" % authz.body) - while latest.fullchain_pem is None: + return orderr + #### POST finalize url + fullchain_pem = None + while fullchain_pem is None: time.sleep(1) response = self.net.get(orderr.uri) - latest = self._order_resource_from_response(response, uri=orderr.uri) + latest = self._order_resource_from_response(response, uri=orderr.uri) return latest diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 8c079fe35..6db438fc0 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -491,8 +491,8 @@ 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) expires = fields.RFC3339Field('expires', omitempty=True) From 540d55c4f849132544d359683bfc4cac91eafab0 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 8 Dec 2017 19:23:19 -0800 Subject: [PATCH 04/14] Add csr. --- acme/acme/client.py | 1 + acme/acme/messages.py | 1 + 2 files changed, 2 insertions(+) diff --git a/acme/acme/client.py b/acme/acme/client.py index c7ed7337c..83ec0c2ad 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -215,6 +215,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes order = messages.NewOrder(identifiers=identifiers) response = self.net.post(self.directory.new_order, order) order_response = self._order_resource_from_response(response) + order_response.csr = csr return order_response def request_challenges(self, identifier, new_authzr_uri=None): diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 6db438fc0..6b754f6b2 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -504,6 +504,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) From 40e039176e816484129f85bcecd25e523d306fb5 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 13 Dec 2017 17:04:47 -0800 Subject: [PATCH 05/14] Move csr initialization. --- acme/acme/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 83ec0c2ad..f84b46099 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -185,7 +185,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes raise errors.UnexpectedUpdate(authzr) return authzr - 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: @@ -199,7 +199,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes body=body, uri=response.headers.get('Location', uri), fullchain_pem=fullchain_pem, - authorizations=authorizations) + authorizations=authorizations, + csr=csr) def new_order(self, csr_pem): """Request challenges. @@ -214,8 +215,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes 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.csr = csr + order_response = self._order_resource_from_response(response, csr=csr) return order_response def request_challenges(self, identifier, new_authzr_uri=None): From 584bcec337b8e1ac6682d757f1e1cfc21d8e888b Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 13 Dec 2017 17:41:27 -0800 Subject: [PATCH 06/14] Add deadline. --- acme/acme/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index f84b46099..0fef170e3 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -327,7 +327,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes response, authzr.body.identifier, authzr.uri) return updated_authzr, response - def poll_order_and_request_issuance(self, orderr): + def poll_order_and_request_issuance(self, orderr, max_time=datetime.timedelta(seconds=90)): """Poll Order Resource for status. :param orderr: Order Resource @@ -339,8 +339,10 @@ class Client(object): # pylint: disable=too-many-instance-attributes """ responses = [] + deadline = datetime.datetime.now() + max_time for url in orderr.body.authorizations: - while True: + 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) From 37dcbebb1c62b921a764678b67c9131be3128a79 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 13 Dec 2017 18:22:01 -0800 Subject: [PATCH 07/14] Implement order finalization. --- acme/acme/client.py | 17 +++++++++-------- acme/acme/messages.py | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 0fef170e3..1386a57d9 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -209,13 +209,14 @@ class Client(object): # pylint: disable=too-many-instance-attributes :rtype: `list` of `.AuthorizationResource` """ csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) + 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, csr=csr) + order_response = self._order_resource_from_response(response, csr=wrapped_csr) return order_response def request_challenges(self, identifier, new_authzr_uri=None): @@ -354,14 +355,14 @@ class Client(object): # pylint: disable=too-many-instance-attributes raise Exception("failed challenge for %s: %s" % (authz.body.identifier.value, chall.error)) raise Exception("failed authorization: %s" % authz.body) - return orderr - #### POST finalize url - fullchain_pem = None - while fullchain_pem is None: + latest = self._order_resource_from_response(self.net.get(orderr.uri), uri=orderr.uri) + self.net.post(latest.body.finalize_url, messages.CertificateRequest(csr=orderr.csr)) + while datetime.datetime.now() < deadline: time.sleep(1) - response = self.net.get(orderr.uri) - latest = self._order_resource_from_response(response, uri=orderr.uri) - return latest + 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 def request_issuance(self, csr, authzrs): diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 6b754f6b2..c7e1e1963 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -495,6 +495,7 @@ class Order(ResourceBody): status = jose.Field('status', omitempty=True, default=None) authorizations = jose.Field('authorizations', omitempty=True) certificate = jose.Field('certificate', omitempty=True) + finalize_url = jose.Field('finalizeURL', omitempty=True) expires = fields.RFC3339Field('expires', omitempty=True) class OrderResource(ResourceWithURI): From 15a8a1e7d824585ec9a11d8f01a96f170bb22a2d Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 14 Dec 2017 12:54:33 -0800 Subject: [PATCH 08/14] Remove verify_ssl=False. --- chisel2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chisel2.py b/chisel2.py index 3c7d91193..34f192a00 100644 --- a/chisel2.py +++ b/chisel2.py @@ -42,7 +42,7 @@ def make_client(email=None): """Build an acme.Client and register a new account with a random key.""" key = jose.JWKRSA(key=rsa.generate_private_key(65537, 2048, default_backend())) - net = acme_client.ClientNetwork(key, verify_ssl=False, acme_version=2, + net = acme_client.ClientNetwork(key, acme_version=2, user_agent="Boulder integration tester") client = acme_client.Client(DIRECTORY, key=key, net=net, acme_version=2) From ac956eb08d23c99fc8339929d4f9fd959b5f8374 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 14 Dec 2017 13:26:04 -0800 Subject: [PATCH 09/14] Use camelCase for directory and other identifiers. --- acme/acme/client.py | 2 +- acme/acme/messages.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 1386a57d9..7a3efc398 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -356,7 +356,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes (authz.body.identifier.value, chall.error)) raise Exception("failed authorization: %s" % authz.body) latest = self._order_resource_from_response(self.net.get(orderr.uri), uri=orderr.uri) - self.net.post(latest.body.finalize_url, messages.CertificateRequest(csr=orderr.csr)) + 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) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index c7e1e1963..3ce0dc063 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -1,5 +1,6 @@ """ACME protocol messages.""" import collections +import re import six from acme import challenges @@ -170,9 +171,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): @@ -192,11 +193,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: @@ -250,7 +259,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:' @@ -495,7 +504,7 @@ class Order(ResourceBody): status = jose.Field('status', omitempty=True, default=None) authorizations = jose.Field('authorizations', omitempty=True) certificate = jose.Field('certificate', omitempty=True) - finalize_url = jose.Field('finalizeURL', omitempty=True) + finalize = jose.Field('finalize', omitempty=True) expires = fields.RFC3339Field('expires', omitempty=True) class OrderResource(ResourceWithURI): From b42c23d896b824421b10ddae546879ab4cb741a2 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 14 Dec 2017 14:42:57 -0800 Subject: [PATCH 10/14] Make chisel2 use josepy. --- chisel2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chisel2.py b/chisel2.py index 34f192a00..61fb6d20a 100644 --- a/chisel2.py +++ b/chisel2.py @@ -23,12 +23,12 @@ from cryptography import x509 from cryptography.hazmat.primitives import hashes import OpenSSL +import josepy from acme import challenges from acme import client as acme_client from acme import crypto_util as acme_crypto_util from acme import errors as acme_errors -from acme import jose from acme import messages from acme import standalone @@ -40,7 +40,7 @@ DIRECTORY = os.getenv('DIRECTORY', 'http://localhost:4001/directory') def make_client(email=None): """Build an acme.Client and register a new account with a random key.""" - key = jose.JWKRSA(key=rsa.generate_private_key(65537, 2048, default_backend())) + key = josepy.JWKRSA(key=rsa.generate_private_key(65537, 2048, default_backend())) net = acme_client.ClientNetwork(key, acme_version=2, user_agent="Boulder integration tester") From c0841ca4852a1b3ac762f0a9e056ae94f4236bef Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 14 Dec 2017 14:53:14 -0800 Subject: [PATCH 11/14] Make pip loud. --- tools/pip_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pip_install.sh b/tools/pip_install.sh index fafd58e54..4be076c1c 100755 --- a/tools/pip_install.sh +++ b/tools/pip_install.sh @@ -14,4 +14,4 @@ dev_constraints="$(dirname $my_path)/pip_constraints.txt" set -x # install the requested packages using the pinned requirements as constraints -pip install -q --constraint $certbot_auto_constraints --constraint $dev_constraints "$@" +pip install --constraint $certbot_auto_constraints --constraint $dev_constraints "$@" From 8ec6b7c6e205da8386ebbcf6cf80b3ed2eabb6f5 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 2 Jan 2018 10:55:06 -0800 Subject: [PATCH 12/14] authz -> authzr --- acme/acme/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 92452ca20..b1703f529 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -350,7 +350,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes break for authzr in responses: if authzr.body.status != messages.STATUS_VALID: - for chall in authz.body.challenges: + for chall in authzr.body.challenges: if chall.error != None: raise Exception("failed challenge for %s: %s" % (authz.body.identifier.value, chall.error)) From 3e0b3beebd2f3703a6ca0cb0d3b69377c84634ae Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 2 Jan 2018 15:45:59 -0800 Subject: [PATCH 13/14] authz -> authzr --- acme/acme/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index b1703f529..79418513b 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -353,8 +353,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes for chall in authzr.body.challenges: if chall.error != None: raise Exception("failed challenge for %s: %s" % - (authz.body.identifier.value, chall.error)) - raise Exception("failed authorization: %s" % authz.body) + (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: From 5ee065ef204d755c1c7037b29f911932bf423d99 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 3 Jan 2018 08:41:14 -0800 Subject: [PATCH 14/14] Add acme-v2 branches to Travis. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3d41bfa4b..efe97ff7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,6 +67,8 @@ branches: - master - /^\d+\.\d+\.x$/ - /^test-.*$/ + - acme-v2 + - acme-v2-integration # container-based infrastructure sudo: false