Compare commits
109 Commits
test-pytho
...
v2-orders
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b102913ab | ||
|
|
77f7cf2c2e | ||
|
|
2a430bb7a5 | ||
|
|
0649bbaa8f | ||
|
|
94f0d150d2 | ||
|
|
105ec41257 | ||
|
|
d7c9cbb278 | ||
|
|
9c1c5653db | ||
|
|
9776175556 | ||
|
|
244bb188e2 | ||
|
|
399a2af6bb | ||
|
|
c27bcb83b8 | ||
|
|
06a5b2c838 | ||
|
|
fb88bf5b06 | ||
|
|
5cc4778221 | ||
|
|
924fbdf84e | ||
|
|
f0a9b81cf1 | ||
|
|
89a73d6e26 | ||
|
|
80e2ec993a | ||
|
|
c48450c1dc | ||
|
|
5c5cd489bc | ||
|
|
d17340f23b | ||
|
|
30efbbe457 | ||
|
|
0bca64adf6 | ||
|
|
5ee065ef20 | ||
|
|
3e0b3beebd | ||
|
|
8ec6b7c6e2 | ||
|
|
c0841ca485 | ||
|
|
b42c23d896 | ||
|
|
6cf656a68f | ||
|
|
ac956eb08d | ||
|
|
15a8a1e7d8 | ||
|
|
37dcbebb1c | ||
|
|
584bcec337 | ||
|
|
40e039176e | ||
|
|
c751d8ad72 | ||
|
|
f32aa7921f | ||
|
|
210009bbfe | ||
|
|
540d55c4f8 | ||
|
|
2916ec896c | ||
|
|
d9616c44f4 | ||
|
|
6a8906509f | ||
|
|
abc78641ea | ||
|
|
40392918db | ||
|
|
400a174d5a | ||
|
|
47a96218d6 | ||
|
|
7063f38463 | ||
|
|
28228aae09 | ||
|
|
2517269a2c | ||
|
|
51d3c7f03e | ||
|
|
78bf67aa56 | ||
|
|
36bdc3d8c9 | ||
|
|
cbde2c2ec7 | ||
|
|
b4eb906274 | ||
|
|
4c2c10f388 | ||
|
|
3667c1e7d8 | ||
|
|
fb1c45671d | ||
|
|
e59408ca25 | ||
|
|
1436323371 | ||
|
|
4e7b930d4b | ||
|
|
ffd64adf82 | ||
|
|
7b92d6dc95 | ||
|
|
6a8d78c5a3 | ||
|
|
acaf858fdb | ||
|
|
f009296669 | ||
|
|
04cc1f4fa7 | ||
|
|
63f8dff67f | ||
|
|
1cf5b9f43e | ||
|
|
a1d4f47ccc | ||
|
|
2070bfa433 | ||
|
|
b9d7f5ad59 | ||
|
|
92409279d0 | ||
|
|
b21b3208df | ||
|
|
cdaba6df33 | ||
|
|
e09efb0029 | ||
|
|
d5607a7b34 | ||
|
|
6a8f9c387f | ||
|
|
38982eeedf | ||
|
|
590044ac77 | ||
|
|
307f4e5c42 | ||
|
|
1f979dd37b | ||
|
|
5ae60088df | ||
|
|
324caf2def | ||
|
|
9cec48c711 | ||
|
|
6bdb87d718 | ||
|
|
3c7b59f730 | ||
|
|
db249c9e19 | ||
|
|
d550542a94 | ||
|
|
89ffd76f4b | ||
|
|
52cb9d9421 | ||
|
|
f1bbf87d30 | ||
|
|
d35a23d378 | ||
|
|
34840b7933 | ||
|
|
2ddba14d2e | ||
|
|
44c761cb68 | ||
|
|
23ce0c3129 | ||
|
|
dd52573bcf | ||
|
|
8b1c58bd8e | ||
|
|
eff001547a | ||
|
|
1e112fa936 | ||
|
|
d806714769 | ||
|
|
3cfaafb4c2 | ||
|
|
9ce078bfd1 | ||
|
|
a9e111d5f4 | ||
|
|
bd09d60246 | ||
|
|
d172a142aa | ||
|
|
24fd264c9c | ||
|
|
fd598d8d19 | ||
|
|
31e68137cd |
@@ -1,6 +1,7 @@
|
|||||||
"""ACME client API."""
|
"""ACME client API."""
|
||||||
import base64
|
import base64
|
||||||
import collections
|
import collections
|
||||||
|
import cryptography
|
||||||
import datetime
|
import datetime
|
||||||
from email.utils import parsedate_tz
|
from email.utils import parsedate_tz
|
||||||
import heapq
|
import heapq
|
||||||
@@ -119,11 +120,11 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
|
|||||||
"""
|
"""
|
||||||
return self._send_recv_regr(regr, messages.UpdateRegistration())
|
return self._send_recv_regr(regr, messages.UpdateRegistration())
|
||||||
|
|
||||||
def _authzr_from_response(self, response, identifier, uri=None):
|
def _authzr_from_response(self, response, identifier=None, uri=None):
|
||||||
authzr = messages.AuthorizationResource(
|
authzr = messages.AuthorizationResource(
|
||||||
body=messages.Authorization.from_json(response.json()),
|
body=messages.Authorization.from_json(response.json()),
|
||||||
uri=response.headers.get('Location', uri))
|
uri=response.headers.get('Location', uri))
|
||||||
if authzr.body.identifier != identifier:
|
if identifier is not None and authzr.body.identifier != identifier:
|
||||||
raise errors.UnexpectedUpdate(authzr)
|
raise errors.UnexpectedUpdate(authzr)
|
||||||
return authzr
|
return authzr
|
||||||
|
|
||||||
@@ -233,8 +234,8 @@ class Client(ClientBase):
|
|||||||
instances of `.DeserializationError` raised in `from_json()`.
|
instances of `.DeserializationError` raised in `from_json()`.
|
||||||
|
|
||||||
:ivar messages.Directory directory:
|
:ivar messages.Directory directory:
|
||||||
:ivar key: `.JWK` (private)
|
:ivar key: `josepy.JWK` (private)
|
||||||
:ivar alg: `.JWASignature`
|
:ivar alg: `josepy.JWASignature`
|
||||||
:ivar bool verify_ssl: Verify SSL certificates?
|
:ivar bool verify_ssl: Verify SSL certificates?
|
||||||
:ivar .ClientNetwork net: Client network. Useful for testing. If not
|
:ivar .ClientNetwork net: Client network. Useful for testing. If not
|
||||||
supplied, it will be initialized using `key`, `alg` and
|
supplied, it will be initialized using `key`, `alg` and
|
||||||
@@ -550,7 +551,6 @@ class ClientV2(ClientBase):
|
|||||||
|
|
||||||
:returns: Registration Resource.
|
:returns: Registration Resource.
|
||||||
:rtype: `.RegistrationResource`
|
:rtype: `.RegistrationResource`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
response = self.net.post(self.directory['newAccount'], new_account,
|
response = self.net.post(self.directory['newAccount'], new_account,
|
||||||
acme_version=2)
|
acme_version=2)
|
||||||
@@ -560,6 +560,83 @@ class ClientV2(ClientBase):
|
|||||||
self.net.account = regr
|
self.net.account = regr
|
||||||
return regr
|
return regr
|
||||||
|
|
||||||
|
def new_order(self, csr_pem):
|
||||||
|
"""Request a new Order object from the server.
|
||||||
|
|
||||||
|
:param str csr_pem: A CSR in PEM format.
|
||||||
|
|
||||||
|
:returns: The newly created order.
|
||||||
|
:rtype: OrderResource
|
||||||
|
"""
|
||||||
|
csr = cryptography.x509.load_pem_x509_csr(csr_pem,
|
||||||
|
cryptography.hazmat.backends.default_backend())
|
||||||
|
san_extension = next(ext for ext in csr.extensions
|
||||||
|
if ext.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
||||||
|
dnsNames = san_extension.value.get_values_for_type(cryptography.x509.DNSName)
|
||||||
|
|
||||||
|
identifiers = []
|
||||||
|
for name in dnsNames:
|
||||||
|
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,
|
||||||
|
value=name))
|
||||||
|
order = messages.NewOrder(identifiers=identifiers)
|
||||||
|
response = self.net.post(self.directory['newOrder'], order)
|
||||||
|
body = messages.Order.from_json(response.json())
|
||||||
|
authorizations = []
|
||||||
|
for url in body.authorizations:
|
||||||
|
authorizations.append(self._authzr_from_response(self.net.get(url)))
|
||||||
|
return messages.OrderResource(
|
||||||
|
body=body,
|
||||||
|
uri=response.headers.get('Location', uri),
|
||||||
|
fullchain_pem=fullchain_pem,
|
||||||
|
authorizations=authorizations,
|
||||||
|
csr_pem=csr_pem)
|
||||||
|
|
||||||
|
def poll_and_finalize(self, orderr, deadline=None):
|
||||||
|
if deadline is None:
|
||||||
|
deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)
|
||||||
|
orderr = self.poll_authorizations(orderr, deadline)
|
||||||
|
return self.finalize_order(orderr, deadline)
|
||||||
|
|
||||||
|
def poll_authorizations(self, orderr, deadline):
|
||||||
|
"""Poll Order Resource for status."""
|
||||||
|
responses = []
|
||||||
|
for url in orderr.body.authorizations:
|
||||||
|
while datetime.datetime.now() < deadline:
|
||||||
|
authzr = self._authzr_from_response(self.net.get(url), uri=url)
|
||||||
|
if authzr.body.status != messages.STATUS_PENDING:
|
||||||
|
responses.append(authzr)
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
# If we didn't get a response for every authorization, we fell through
|
||||||
|
# the bottom of the loop due to hitting the deadline.
|
||||||
|
if len(responses) > orderr.body.authorizations:
|
||||||
|
raise TimeoutError()
|
||||||
|
failed = []
|
||||||
|
for authzr in responses:
|
||||||
|
if authzr.body.status != messages.STATUS_VALID:
|
||||||
|
for chall in authzr.body.challenges:
|
||||||
|
if chall.error != None:
|
||||||
|
failed.append(authzr)
|
||||||
|
if len(failed) > 0:
|
||||||
|
raise ValidationError(failed)
|
||||||
|
return orderr.update(authorizations=responses)
|
||||||
|
|
||||||
|
def finalize_order(self, orderr, deadline):
|
||||||
|
csr = OpenSSL.crypto.load_certificate_request(
|
||||||
|
OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem)
|
||||||
|
wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr))
|
||||||
|
self.net.post(latest.body.finalize, wrapped_csr)
|
||||||
|
while datetime.datetime.now() < deadline:
|
||||||
|
time.sleep(1)
|
||||||
|
response = self.net.get(orderr.uri)
|
||||||
|
body = messages.Order.from_json(response.json())
|
||||||
|
if body.error is not None:
|
||||||
|
raise IssuanceError(body.error)
|
||||||
|
if body.certificate is not None:
|
||||||
|
certificate_response = self.net.get(body.certificate).text
|
||||||
|
return orderr.update(fullchain_pem=certificate_response)
|
||||||
|
raise TimeoutError()
|
||||||
|
|
||||||
class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||||
"""Wrapper around requests that signs POSTs for authentication.
|
"""Wrapper around requests that signs POSTs for authentication.
|
||||||
|
|
||||||
@@ -572,10 +649,10 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
"""Initialize.
|
"""Initialize.
|
||||||
|
|
||||||
:param key: Account private key
|
:param josepy.JWK key: Account private key
|
||||||
:param messages.RegistrationResource account: Account object. Required if you are
|
:param messages.RegistrationResource account: Account object. Required if you are
|
||||||
planning to use .post() with acme_version=2 for anything other than creating a new
|
planning to use .post() with acme_version=2 for anything other than
|
||||||
account; may be set later after registering.
|
creating a new account; may be set later after registering.
|
||||||
:param josepy.JWASignature alg: Algoritm to use in signing JWS.
|
:param josepy.JWASignature alg: Algoritm to use in signing JWS.
|
||||||
:param bool verify_ssl: Whether to verify certificates on SSL connections.
|
:param bool verify_ssl: Whether to verify certificates on SSL connections.
|
||||||
:param str user_agent: String to send as User-Agent header.
|
:param str user_agent: String to send as User-Agent header.
|
||||||
@@ -606,10 +683,10 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
.. todo:: Implement ``acmePath``.
|
.. todo:: Implement ``acmePath``.
|
||||||
|
|
||||||
:param .JSONDeSerializable obj:
|
:param josepy.JSONDeSerializable obj:
|
||||||
:param str url: The URL to which this object will be POSTed
|
:param str url: The URL to which this object will be POSTed
|
||||||
:param bytes nonce:
|
:param bytes nonce:
|
||||||
:rtype: `.JWS`
|
:rtype: `josepy.JWS`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
jobj = obj.json_dumps(indent=2).encode()
|
jobj = obj.json_dumps(indent=2).encode()
|
||||||
|
|||||||
@@ -83,6 +83,27 @@ class PollError(ClientError):
|
|||||||
return '{0}(exhausted={1!r}, updated={2!r})'.format(
|
return '{0}(exhausted={1!r}, updated={2!r})'.format(
|
||||||
self.__class__.__name__, self.exhausted, self.updated)
|
self.__class__.__name__, self.exhausted, self.updated)
|
||||||
|
|
||||||
|
class ValidationError(Error):
|
||||||
|
"""Error for authorization failures. Contains a list of authorization
|
||||||
|
resources, each of which is invalid and should have an error field.
|
||||||
|
"""
|
||||||
|
def __init__(self, failed_authzrs):
|
||||||
|
self.failed_authzrs = failed_authzrs
|
||||||
|
super(ClientError, self).__init__()
|
||||||
|
|
||||||
|
class TimeoutError(Error):
|
||||||
|
"""Error for when polling an authorization or an order times out."""
|
||||||
|
|
||||||
|
class IssuanceError(Error):
|
||||||
|
"""Error sent by the server after requesting issuance of a certificate."""
|
||||||
|
|
||||||
|
def __init__(self, error):
|
||||||
|
"""Initialize.
|
||||||
|
|
||||||
|
:param messages.Error error: The error provided by the server.
|
||||||
|
"""
|
||||||
|
self.error = error
|
||||||
|
|
||||||
class ConflictError(ClientError):
|
class ConflictError(ClientError):
|
||||||
"""Error for when the server returns a 409 (Conflict) HTTP status.
|
"""Error for when the server returns a 409 (Conflict) HTTP status.
|
||||||
|
|
||||||
|
|||||||
@@ -172,8 +172,9 @@ class Directory(jose.JSONDeSerializable):
|
|||||||
class Meta(jose.JSONObjectWithFields):
|
class Meta(jose.JSONObjectWithFields):
|
||||||
"""Directory Meta."""
|
"""Directory Meta."""
|
||||||
terms_of_service = jose.Field('terms-of-service', omitempty=True)
|
terms_of_service = jose.Field('terms-of-service', omitempty=True)
|
||||||
|
terms_of_service_v2 = jose.Field('termsOfService', omitempty=True)
|
||||||
website = jose.Field('website', omitempty=True)
|
website = jose.Field('website', omitempty=True)
|
||||||
caa_identities = jose.Field('caa-identities', omitempty=True)
|
caa_identities = jose.Field('caaIdentities', omitempty=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _canon_key(cls, key):
|
def _canon_key(cls, key):
|
||||||
@@ -251,7 +252,7 @@ class Registration(ResourceBody):
|
|||||||
contact = jose.Field('contact', omitempty=True, default=())
|
contact = jose.Field('contact', omitempty=True, default=())
|
||||||
agreement = jose.Field('agreement', omitempty=True)
|
agreement = jose.Field('agreement', omitempty=True)
|
||||||
status = jose.Field('status', 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:'
|
phone_prefix = 'tel:'
|
||||||
email_prefix = 'mailto:'
|
email_prefix = 'mailto:'
|
||||||
@@ -483,3 +484,49 @@ class Revocation(jose.JSONObjectWithFields):
|
|||||||
certificate = jose.Field(
|
certificate = jose.Field(
|
||||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||||
reason = jose.Field('reason')
|
reason = jose.Field('reason')
|
||||||
|
|
||||||
|
|
||||||
|
class Order(ResourceBody):
|
||||||
|
"""Order Resource Body.
|
||||||
|
|
||||||
|
.. note:: Parsing of identifiers on response doesn't work right now; to make
|
||||||
|
it work we would need to set up the equivalent of Identifier.from_json, but
|
||||||
|
for a list.
|
||||||
|
:ivar list of .Identifier: List of identifiers for the certificate.
|
||||||
|
:ivar acme.messages.Status status:
|
||||||
|
:ivar list of str authorizations: URLs of authorizations.
|
||||||
|
:ivar str certificate: URL to download certificate as a fullchain PEM.
|
||||||
|
:ivar str finalize: URL to POST to to request issuance once all
|
||||||
|
authorizations have "valid" status.
|
||||||
|
:ivar datetime.datetime expires: When the order expires.
|
||||||
|
:ivar .Error error: Any error that occurred during finalization, if applicable.
|
||||||
|
"""
|
||||||
|
identifiers = jose.Field('identifiers', omitempty=True)
|
||||||
|
status = jose.Field('status', decoder=Status.from_json,
|
||||||
|
omitempty=True, default=STATUS_PENDING)
|
||||||
|
authorizations = jose.Field('authorizations', omitempty=True)
|
||||||
|
certificate = jose.Field('certificate', omitempty=True)
|
||||||
|
finalize = jose.Field('finalize', omitempty=True)
|
||||||
|
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||||
|
error = jose.Field('error', omitempty=True, decoder=Error.from_json)
|
||||||
|
|
||||||
|
class OrderResource(ResourceWithURI):
|
||||||
|
"""Order Resource.
|
||||||
|
|
||||||
|
:ivar acme.messages.Order body:
|
||||||
|
:ivar str csr_pem: The CSR this Order will be finalized with.
|
||||||
|
:ivar list of acme.messages.AuthorizationResource authorizations:
|
||||||
|
Fully-fetched AuthorizationResource objects.
|
||||||
|
:ivar str fullchain_pem: The fetched contents of the certificate URL
|
||||||
|
produced once the order was finalized, if it's present.
|
||||||
|
"""
|
||||||
|
body = jose.Field('body', decoder=Order.from_json)
|
||||||
|
csr_pem = jose.Field('csr_pem', omitempty=True)
|
||||||
|
authorizations = jose.Field('authorizations')
|
||||||
|
fullchain_pem = jose.Field('fullchain_pem', omitempty=True)
|
||||||
|
|
||||||
|
@Directory.register
|
||||||
|
class NewOrder(Order):
|
||||||
|
"""New order."""
|
||||||
|
resource_type = 'new-order'
|
||||||
|
resource = fields.Resource(resource_type)
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class DirectoryTest(unittest.TestCase):
|
|||||||
'meta': {
|
'meta': {
|
||||||
'terms-of-service': 'https://example.com/acme/terms',
|
'terms-of-service': 'https://example.com/acme/terms',
|
||||||
'website': 'https://www.example.com/',
|
'website': 'https://www.example.com/',
|
||||||
'caa-identities': ['example.com'],
|
'caaIdentities': ['example.com'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -401,5 +401,21 @@ class RevocationTest(unittest.TestCase):
|
|||||||
hash(Revocation.from_json(self.rev.to_json()))
|
hash(Revocation.from_json(self.rev.to_json()))
|
||||||
|
|
||||||
|
|
||||||
|
class OrderResourceTest(unittest.TestCase):
|
||||||
|
"""Tests for acme.messages.OrderResource."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from acme.messages import OrderResource
|
||||||
|
self.regr = OrderResource(
|
||||||
|
body=mock.sentinel.body, uri=mock.sentinel.uri)
|
||||||
|
|
||||||
|
def test_to_partial_json(self):
|
||||||
|
self.assertEqual(self.regr.to_json(), {
|
||||||
|
'body': mock.sentinel.body,
|
||||||
|
'uri': mock.sentinel.uri,
|
||||||
|
'authorizations': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main() # pragma: no cover
|
unittest.main() # pragma: no cover
|
||||||
|
|||||||
Reference in New Issue
Block a user