Merge branch 'acme-v2' into v2-orders

This commit is contained in:
Jacob Hoffman-Andrews 2018-01-05 18:07:14 -08:00
commit 89a73d6e26
2 changed files with 55 additions and 30 deletions

View file

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

View file

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