Compare commits
109 Commits
remove-ngi
...
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."""
|
||||
import base64
|
||||
import collections
|
||||
import cryptography
|
||||
import datetime
|
||||
from email.utils import parsedate_tz
|
||||
import heapq
|
||||
@@ -119,11 +120,11 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
|
||||
"""
|
||||
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(
|
||||
body=messages.Authorization.from_json(response.json()),
|
||||
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)
|
||||
return authzr
|
||||
|
||||
@@ -233,8 +234,8 @@ class Client(ClientBase):
|
||||
instances of `.DeserializationError` raised in `from_json()`.
|
||||
|
||||
:ivar messages.Directory directory:
|
||||
:ivar key: `.JWK` (private)
|
||||
:ivar alg: `.JWASignature`
|
||||
:ivar key: `josepy.JWK` (private)
|
||||
:ivar alg: `josepy.JWASignature`
|
||||
:ivar bool verify_ssl: Verify SSL certificates?
|
||||
:ivar .ClientNetwork net: Client network. Useful for testing. If not
|
||||
supplied, it will be initialized using `key`, `alg` and
|
||||
@@ -550,7 +551,6 @@ class ClientV2(ClientBase):
|
||||
|
||||
:returns: Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
response = self.net.post(self.directory['newAccount'], new_account,
|
||||
acme_version=2)
|
||||
@@ -560,6 +560,83 @@ class ClientV2(ClientBase):
|
||||
self.net.account = 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
|
||||
"""Wrapper around requests that signs POSTs for authentication.
|
||||
|
||||
@@ -572,10 +649,10 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
"""Initialize.
|
||||
|
||||
:param key: Account private key
|
||||
:param josepy.JWK key: Account private key
|
||||
: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
|
||||
account; may be set later after registering.
|
||||
planning to use .post() with acme_version=2 for anything other than
|
||||
creating a new account; may be set later after registering.
|
||||
:param josepy.JWASignature alg: Algoritm to use in signing JWS.
|
||||
:param bool verify_ssl: Whether to verify certificates on SSL connections.
|
||||
: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``.
|
||||
|
||||
:param .JSONDeSerializable obj:
|
||||
:param josepy.JSONDeSerializable obj:
|
||||
:param str url: The URL to which this object will be POSTed
|
||||
:param bytes nonce:
|
||||
:rtype: `.JWS`
|
||||
:rtype: `josepy.JWS`
|
||||
|
||||
"""
|
||||
jobj = obj.json_dumps(indent=2).encode()
|
||||
|
||||
@@ -83,6 +83,27 @@ class PollError(ClientError):
|
||||
return '{0}(exhausted={1!r}, updated={2!r})'.format(
|
||||
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):
|
||||
"""Error for when the server returns a 409 (Conflict) HTTP status.
|
||||
|
||||
|
||||
@@ -172,8 +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_v2 = 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):
|
||||
@@ -251,7 +252,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:'
|
||||
@@ -483,3 +484,49 @@ class Revocation(jose.JSONObjectWithFields):
|
||||
certificate = jose.Field(
|
||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||
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': {
|
||||
'terms-of-service': 'https://example.com/acme/terms',
|
||||
'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()))
|
||||
|
||||
|
||||
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__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
Reference in New Issue
Block a user