twisted / txacme
Showing 11 of 41 files from the diff.

@@ -13,7 +13,7 @@
Loading
13 13
14 14
LOG_JWS_SIGN = ActionType(
15 15
    u'txacme:jws:sign',
16 -
    fields(NONCE, key_type=unicode, alg=unicode),
16 +
    fields(NONCE, key_type=unicode, alg=unicode, kid=unicode),
17 17
    fields(),
18 18
    u'Signing a message with JWS')
19 19
@@ -93,27 +93,6 @@
Loading
93 93
                 u'The resulting registration')),
94 94
    u'Registering with an ACME server')
95 95
96 -
LOG_ACME_UPDATE_REGISTRATION = ActionType(
97 -
    u'txacme:acme:client:registration:update',
98 -
    fields(Field(u'registration',
99 -
                 methodcaller('to_json'),
100 -
                 u'An ACME registration'),
101 -
           uri=unicode),
102 -
    fields(Field(u'registration',
103 -
                 methodcaller('to_json'),
104 -
                 u'The updated registration')),
105 -
    u'Updating a registration')
106 -
107 -
LOG_ACME_CREATE_AUTHORIZATION = ActionType(
108 -
    u'txacme:acme:client:authorization:create',
109 -
    fields(Field(u'identifier',
110 -
                 methodcaller('to_json'),
111 -
                 u'An identifier')),
112 -
    fields(Field(u'authorization',
113 -
                 methodcaller('to_json'),
114 -
                 u'The authorization')),
115 -
    u'Creating an authorization')
116 -
117 96
LOG_ACME_ANSWER_CHALLENGE = ActionType(
118 97
    u'txacme:acme:client:challenge:answer',
119 98
    fields(Field(u'challenge_body',
@@ -126,28 +105,3 @@
Loading
126 105
                 methodcaller('to_json'),
127 106
                 u'The updated challenge')),
128 107
    u'Answering an authorization challenge')
129 -
130 -
LOG_ACME_POLL_AUTHORIZATION = ActionType(
131 -
    u'txacme:acme:client:authorization:poll',
132 -
    fields(Field(u'authorization',
133 -
                 methodcaller('to_json'),
134 -
                 u'The authorization resource')),
135 -
    fields(Field(u'authorization',
136 -
                 methodcaller('to_json'),
137 -
                 u'The updated authorization'),
138 -
           Field.for_types(u'retry_after',
139 -
                           [int, float],
140 -
                           u'How long before polling again?')),
141 -
    u'Polling an authorization')
142 -
143 -
LOG_ACME_REQUEST_CERTIFICATE = ActionType(
144 -
    u'txacme:acme:client:certificate:request',
145 -
    fields(),
146 -
    fields(),
147 -
    u'Requesting a certificate')
148 -
149 -
LOG_ACME_FETCH_CHAIN = ActionType(
150 -
    u'txacme:acme:client:certificate:fetch-chain',
151 -
    fields(),
152 -
    fields(),
153 -
    u'Fetching a certificate chain')

@@ -13,20 +13,11 @@
Loading
13 13
from cryptography.hazmat.backends import default_backend
14 14
from cryptography.hazmat.primitives import serialization
15 15
from cryptography.hazmat.primitives.asymmetric import rsa
16 -
from fixtures import Fixture
17 -
from hypothesis import strategies as s
18 -
from hypothesis import assume, example, given, settings
19 -
from testtools import ExpectedException, TestCase
20 -
from testtools.matchers import (
21 -
    AfterPreprocessing, Always, ContainsDict, Equals, Is, IsInstance,
22 -
    MatchesAll, MatchesListwise, MatchesPredicate, MatchesStructure, Mismatch,
23 -
    Never, Not, StartsWith, HasLength)
24 -
from testtools.twistedsupport import failed, succeeded, has_no_result
25 16
from treq.client import HTTPClient
26 17
from treq.testing import RequestSequence as treq_RequestSequence
27 18
from treq.testing import (
28 19
    _SynchronousProducer, RequestTraversalAgent, StringStubbingResource)
29 -
from twisted.internet import reactor
20 +
from twisted.internet import defer, reactor
30 21
from twisted.internet.defer import Deferred, CancelledError, fail, succeed
31 22
from twisted.internet.error import ConnectionClosed
32 23
from twisted.internet.task import Clock
@@ -35,19 +26,22 @@
Loading
35 26
from twisted.web import http, server
36 27
from twisted.web.resource import Resource
37 28
from twisted.web.http_headers import Headers
29 +
from twisted.trial.unittest import TestCase
38 30
from zope.interface import implementer
39 31
40 32
from txacme.client import (
41 33
    _default_client, _find_supported_challenge, _parse_header_links,
42 34
    answer_challenge, AuthorizationFailed, Client, DER_CONTENT_TYPE,
43 -
    fqdn_identifier, JSON_CONTENT_TYPE, JSON_ERROR_CONTENT_TYPE, JWSClient,
44 -
    NoSupportedChallenges, poll_until_valid, ServerError)
35 +
    fqdn_identifier, JSON_CONTENT_TYPE, JOSE_CONTENT_TYPE,
36 +
    JSON_ERROR_CONTENT_TYPE, JWSClient, NoSupportedChallenges, ServerError,
37 +
    get_certificate
38 +
)
45 39
from txacme.interfaces import IResponder
46 40
from txacme.messages import CertificateRequest
47 -
from txacme.test import strategies as ts
48 41
from txacme.testing import NullResponder
49 42
from txacme.util import (
50 -
    csr_for_names, generate_private_key, generate_tls_sni_01_cert)
43 +
    csr_for_names, generate_private_key
44 +
)
51 45
52 46
53 47
def failed_with(matcher):
@@ -89,116 +83,6 @@
Loading
89 83
RSA_KEY_512 = JWKRSA(key=RSA_KEY_512_RAW)
90 84
91 85
92 -
class Nearly(object):
93 -
    """Within a certain threshold."""
94 -
    def __init__(self, expected, epsilon=0.001):
95 -
        self.expected = expected
96 -
        self.epsilon = epsilon
97 -
98 -
    def __str__(self):
99 -
        return 'Nearly(%r, %r)' % (self.expected, self.epsilon)
100 -
101 -
    def match(self, value):
102 -
        if abs(value - self.expected) > self.epsilon:
103 -
            return Mismatch(
104 -
                u'%r more than %r from %r' % (
105 -
                    value, self.epsilon, self.expected))
106 -
107 -
108 -
@attr.s(auto_attribs=True)
109 -
class ConnectionPoolFixture:
110 -
    _deferred: Deferred = attr.Factory(Deferred)
111 -
    _closing: bool = False
112 -
113 -
    def started_closing(self):
114 -
        return self._closing
115 -
116 -
    def finish_closing(self):
117 -
        self._deferred.callback(None)
118 -
119 -
120 -
@attr.s(auto_attribs=True)
121 -
class FakePool:
122 -
    _fixture_pool: ConnectionPoolFixture
123 -
124 -
    def closeCachedConnections(self):  # noqa
125 -
        self._fixture_pool._closing = True
126 -
        return self._fixture_pool._deferred
127 -
128 -
129 -
class ClientFixture(Fixture):
130 -
    """
131 -
    Create a :class:`~txacme.client.Client` for testing.
132 -
    """
133 -
    def __init__(
134 -
        self, sequence, key=None, alg=RS256, use_connection_pool=False
135 -
    ):
136 -
        super(ClientFixture, self).__init__()
137 -
        self._sequence = sequence
138 -
        if isinstance(sequence, treq_RequestSequence):
139 -
            self._agent = RequestTraversalAgent(
140 -
                StringStubbingResource(self._sequence)
141 -
            )
142 -
        else:
143 -
            self._agent = RequestTraversalAgent(sequence)
144 -
        if use_connection_pool:
145 -
            self.pool = ConnectionPoolFixture()
146 -
            self._agent._pool = FakePool(self.pool)
147 -
        self._directory = messages.Directory({
148 -
            messages.NewRegistration:
149 -
            u'https://example.org/acme/new-reg',
150 -
            messages.Revocation:
151 -
            u'https://example.org/acme/revoke-cert',
152 -
            messages.NewAuthorization:
153 -
            u'https://example.org/acme/new-authz',
154 -
            messages.CertificateRequest:
155 -
            u'https://example.org/acme/new-cert',
156 -
            })
157 -
        if key is None:
158 -
            key = JWKRSA(key=generate_private_key('rsa'))
159 -
        self._key = key
160 -
        self._alg = alg
161 -
162 -
    def _setUp(self):  # noqa
163 -
        jws_client = JWSClient(self._agent, self._key, self._alg)
164 -
        jws_client._treq._data_to_body_producer = _SynchronousProducer
165 -
        self.clock = Clock()
166 -
        self.client = Client(
167 -
            self._directory, self.clock, self._key,
168 -
            jws_client=jws_client)
169 -
170 -
    def flush(self):
171 -
        self._agent.flush()
172 -
173 -
174 -
def _nonce_response(url, nonce):
175 -
    """
176 -
    Construct an expected request for an initial nonce check.
177 -
178 -
    :param bytes url: The url being requested.
179 -
    :param bytes nonce: The nonce to return.
180 -
181 -
    :return: A request/response tuple suitable for use with
182 -
        :class:`~treq.testing.RequestSequence`.
183 -
    """
184 -
    return (
185 -
        MatchesListwise([
186 -
            Equals(b'HEAD'),
187 -
            Equals(url),
188 -
            Equals({}),
189 -
            ContainsDict({b'User-Agent':
190 -
                          MatchesListwise([StartsWith(b'txacme/')])}),
191 -
            Equals(b'')]),
192 -
        (http.NOT_ALLOWED,
193 -
         {b'content-type': JSON_CONTENT_TYPE,
194 -
          b'replay-nonce': b64encode(nonce)},
195 -
         b'{}'))
196 -
197 -
198 -
def _json_dumps(j):
199 -
    return json.dumps(j).encode("utf-8")
200 -
201 -
202 86
class RequestSequence(treq_RequestSequence):
203 87
    @contextmanager
204 88
    def consume(self, sync_failure_reporter):
@@ -291,1458 +175,76 @@
Loading
291 175
    """
292 176
    :class:`.Client` provides a client interface for the ACME API.
293 177
    """
178 +
179 +
    @defer.inlineCallbacks
294 180
    def test_directory_url_type(self):
295 181
        """
296 182
        `~txacme.client.Client.from_url` expects a ``twisted.python.url.URL``
297 183
        instance for the ``url`` argument.
298 184
        """
299 -
        with ExpectedException(TypeError):
300 -
            Client.from_url(
185 +
        with self.assertRaises(TypeError):
186 +
            yield Client.from_url(
301 187
                reactor, '/wrong/kind/of/directory', key=RSA_KEY_512)
302 188
303 -
    def test_register_missing_next(self):
304 -
        """
305 -
        If the directory does not return a ``"next"`` link, a
306 -
        :exc:`~acme.errors.ClientError` failure occurs.
307 -
        """
308 -
        sequence = RequestSequence(
309 -
            [_nonce_response(
310 -
                u'https://example.org/acme/new-reg',
311 -
                b'Nonce'),
312 -
             (MatchesListwise([
313 -
                 Equals(b'POST'),
314 -
                 Equals(u'https://example.org/acme/new-reg'),
315 -
                 Equals({}),
316 -
                 Always(),
317 -
                 Always()]),
318 -
              (http.CREATED,
319 -
               {b'content-type': JSON_CONTENT_TYPE,
320 -
                b'replay-nonce': b64encode(b'Nonce2')},
321 -
               b'{}'))],
322 -
            self.expectThat)
323 -
        client = self.useFixture(ClientFixture(sequence)).client
324 -
        with sequence.consume(self.fail):
325 -
            d = client.register()
326 -
        self.expectThat(
327 -
            d, failed_with(MatchesAll(
328 -
                IsInstance(errors.ClientError),
329 -
                AfterPreprocessing(str, Equals('"next" link missing')))))
330 -
331 -
    def test_unexpected_update(self):
332 -
        """
333 -
        If the server does not return the registration we expected, an
334 -
        :exc:`~acme.errors.UnexpectedUpdate` failure occurs.
335 -
        """
336 -
        update = (
337 -
            MatchesListwise([
338 -
                Equals(b'POST'),
339 -
                Equals(u'https://example.org/acme/new-reg'),
340 -
                Equals({}),
341 -
                ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
342 -
                Always()]),
343 -
            (http.CREATED,
344 -
             {b'content-type': JSON_CONTENT_TYPE,
345 -
              b'replay-nonce': b64encode(b'Nonce2'),
346 -
              b'location': b'https://example.org/acme/reg/1',
347 -
              b'link': b','.join([
348 -
                  b'<https://example.org/acme/new-authz>;rel="next"',
349 -
                  b'<https://example.org/acme/recover-reg>;rel="recover"',
350 -
                  b'<https://example.org/acme/terms>;rel="terms-of-service"',
351 -
              ])},
352 -
             _json_dumps({
353 -
                 u'key': {
354 -
                     u'n': u'alQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
355 -
                           u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
356 -
                     u'e': u'AQAB',
357 -
                     u'kty': u'RSA'},
358 -
                 u'contact': [u'mailto:example@example.com'],
359 -
             })))
360 -
        sequence = RequestSequence(
361 -
            [_nonce_response(
362 -
                u'https://example.org/acme/new-reg',
363 -
                b'Nonce'),
364 -
             update,
365 -
             update],
366 -
            self.expectThat)
367 -
        client = self.useFixture(
368 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
369 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
370 -
        reg2 = messages.NewRegistration.from_data(email=u'foo@example.com')
371 -
        with sequence.consume(self.fail):
372 -
            self.assertThat(
373 -
                client.register(reg),
374 -
                failed_with(IsInstance(errors.UnexpectedUpdate)))
375 -
            self.assertThat(
376 -
                client.register(reg2),
377 -
                failed_with(IsInstance(errors.UnexpectedUpdate)))
378 -
        self.expectThat(client.stop(), succeeded(Equals(None)))
379 -
380 -
    def stop_in_progress(self, use_pool=False):
381 -
        requested = []
382 -
383 -
        class NoAnswerResource(Resource):
384 -
            isLeaf = True       # noqa
385 -
386 -
            def render(self, request):
387 -
                requested.append(request.notifyFinish())
388 -
                return server.NOT_DONE_YET
389 -
390 -
        self.client_fixture = self.useFixture(
391 -
            ClientFixture(
392 -
                NoAnswerResource(),
393 -
                key=RSA_KEY_512,
394 -
                use_connection_pool=use_pool,
395 -
            )
396 -
        )
397 -
        client = self.client_fixture.client
398 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
399 -
        register_call = client.register(reg)
400 -
        self.expectThat(requested, HasLength(1))
401 -
        self.expectThat(register_call, has_no_result())
402 -
        self.expectThat(requested[0], has_no_result())
403 -
        stop_deferred = client.stop()
404 -
        self.assertThat(register_call, succeeded(Equals(None)))
405 -
        self.client_fixture.flush()
406 -
        self.assertThat(
407 -
            requested[0],
408 -
            failed_with(IsInstance(ConnectionClosed)),
409 -
        )
410 -
        return stop_deferred
411 -
412 -
    def test_stop_in_progress(self):
413 -
        """
414 -
        If we stop the client while an operation is in progress, it's
415 -
        cancelled.
416 -
        """
417 -
        self.assertThat(self.stop_in_progress(), succeeded(Equals(None)))
418 -
419 -
    def test_stop_in_progress_with_pool(self):
420 -
        """
421 -
        If we stop the client while an operation is in progress, it will stop.
422 -
        """
423 -
        stopped = self.stop_in_progress(True)
424 -
        self.assertThat(stopped, has_no_result())
425 -
        self.assertThat(self.client_fixture.pool.started_closing(),
426 -
                        Equals(True))
427 -
        self.client_fixture.pool.finish_closing()
428 -
        self.assertThat(stopped, succeeded(Equals(None)))
429 -
430 -
    def test_register(self):
431 -
        """
432 -
        If the registration succeeds, the new registration is returned.
433 -
        """
434 -
        sequence = RequestSequence(
435 -
            [_nonce_response(
436 -
                u'https://example.org/acme/new-reg',
437 -
                b'Nonce'),
438 -
             (MatchesListwise([
439 -
                 Equals(b'POST'),
440 -
                 Equals(u'https://example.org/acme/new-reg'),
441 -
                 Equals({}),
442 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
443 -
                 on_jws(Equals({
444 -
                     u'resource': u'new-reg',
445 -
                     u'contact': [u'mailto:example@example.com']}))]),
446 -
              (http.CREATED,
447 -
               {b'content-type': JSON_CONTENT_TYPE,
448 -
                b'replay-nonce': b64encode(b'Nonce2'),
449 -
                b'location': b'https://example.org/acme/reg/1',
450 -
                b'link': b','.join([
451 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
452 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
453 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
454 -
                ])},
455 -
               _json_dumps({
456 -
                   u'key': {
457 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
458 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
459 -
                       u'e': u'AQAB',
460 -
                       u'kty': u'RSA'},
461 -
                   u'contact': [u'mailto:example@example.com'],
462 -
               })))],
463 -
            self.expectThat)
464 -
        client = self.useFixture(
465 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
466 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
467 -
        with sequence.consume(self.fail):
468 -
            d = client.register(reg)
469 -
            self.assertThat(
470 -
                d, succeeded(MatchesStructure(
471 -
                    body=MatchesStructure(
472 -
                        key=Equals(RSA_KEY_512.public_key()),
473 -
                        contact=Equals(reg.contact)),
474 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
475 -
                    new_authzr_uri=Equals(
476 -
                        u'https://example.org/acme/new-authz'),
477 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
478 -
                )))
479 -
480 -
    def test_register_existing(self):
481 -
        """
482 -
        If registration fails due to our key already being registered, the
483 -
        existing registration is returned.
484 -
        """
485 -
        sequence = RequestSequence(
486 -
            [_nonce_response(
487 -
                u'https://example.org/acme/new-reg',
488 -
                b'Nonce'),
489 -
             (MatchesListwise([
490 -
                 Equals(b'POST'),
491 -
                 Equals(u'https://example.org/acme/new-reg'),
492 -
                 Equals({}),
493 -
                 Always(),
494 -
                 on_jws(Equals({
495 -
                     u'resource': u'new-reg',
496 -
                     u'contact': [u'mailto:example@example.com']}))]),
497 -
              (http.CONFLICT,
498 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
499 -
                b'replay-nonce': b64encode(b'Nonce2'),
500 -
                b'location': b'https://example.org/acme/reg/1',
501 -
                },
502 -
               _json_dumps(
503 -
                   {u'status': http.CONFLICT,
504 -
                    u'type': u'urn:acme:error:malformed',
505 -
                    u'detail': u'Registration key is already in use'}
506 -
               ))),
507 -
             (MatchesListwise([
508 -
                 Equals(b'POST'),
509 -
                 Equals(u'https://example.org/acme/reg/1'),
510 -
                 Equals({}),
511 -
                 Always(),
512 -
                 on_jws(Equals({
513 -
                     u'resource': u'reg',
514 -
                     u'contact': [u'mailto:example@example.com']}))]),
515 -
              (http.ACCEPTED,
516 -
               {b'content-type': JSON_CONTENT_TYPE,
517 -
                b'replay-nonce': b64encode(b'Nonce3'),
518 -
                b'link': b','.join([
519 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
520 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
521 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
522 -
                ])},
523 -
               _json_dumps({
524 -
                   u'key': {
525 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
526 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
527 -
                       u'e': u'AQAB',
528 -
                       u'kty': u'RSA'},
529 -
                   u'contact': [u'mailto:example@example.com'],
530 -
                   u'agreement': u'https://example.org/acme/terms',
531 -
               })))],
532 -
            self.expectThat)
533 -
        client = self.useFixture(
534 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
535 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
536 -
        with sequence.consume(self.fail):
537 -
            d = client.register(reg)
538 -
            self.assertThat(
539 -
                d, succeeded(MatchesStructure(
540 -
                    body=MatchesStructure(
541 -
                        key=Equals(RSA_KEY_512.public_key()),
542 -
                        contact=Equals(reg.contact)),
543 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
544 -
                    new_authzr_uri=Equals(
545 -
                        u'https://example.org/acme/new-authz'),
546 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
547 -
                )))
548 -
549 -
    def test_register_existing_update(self):
550 -
        """
551 -
        If registration fails due to our key already being registered, the
552 -
        existing registration is updated.
553 -
        """
554 -
        sequence = RequestSequence(
555 -
            [_nonce_response(
556 -
                u'https://example.org/acme/new-reg',
557 -
                b'Nonce'),
558 -
             (MatchesListwise([
559 -
                 Equals(b'POST'),
560 -
                 Equals(u'https://example.org/acme/new-reg'),
561 -
                 Equals({}),
562 -
                 Always(),
563 -
                 on_jws(Equals({
564 -
                     u'resource': u'new-reg',
565 -
                     u'contact': [u'mailto:example2@example.com']}))]),
566 -
              (http.CONFLICT,
567 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
568 -
                b'replay-nonce': b64encode(b'Nonce2'),
569 -
                b'location': b'https://example.org/acme/reg/1',
570 -
                },
571 -
               _json_dumps(
572 -
                   {u'status': http.CONFLICT,
573 -
                    u'type': u'urn:acme:error:malformed',
574 -
                    u'detail': u'Registration key is already in use'}
575 -
               ))),
576 -
             (MatchesListwise([
577 -
                 Equals(b'POST'),
578 -
                 Equals(u'https://example.org/acme/reg/1'),
579 -
                 Equals({}),
580 -
                 Always(),
581 -
                 on_jws(Equals({
582 -
                     u'resource': u'reg',
583 -
                     u'contact': [u'mailto:example2@example.com']}))]),
584 -
              (http.ACCEPTED,
585 -
               {b'content-type': JSON_CONTENT_TYPE,
586 -
                b'replay-nonce': b64encode(b'Nonce3'),
587 -
                b'link': b','.join([
588 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
589 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
590 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
591 -
                ])},
592 -
               _json_dumps({
593 -
                   u'key': {
594 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
595 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
596 -
                       u'e': u'AQAB',
597 -
                       u'kty': u'RSA'},
598 -
                   u'contact': [u'mailto:example2@example.com'],
599 -
                   u'agreement': u'https://example.org/acme/terms',
600 -
               })))],
601 -
            self.expectThat)
602 -
        client = self.useFixture(
603 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
604 -
        reg = messages.NewRegistration.from_data(email=u'example2@example.com')
605 -
        with sequence.consume(self.fail):
606 -
            d = client.register(reg)
607 -
            self.assertThat(
608 -
                d, succeeded(MatchesStructure(
609 -
                    body=MatchesStructure(
610 -
                        key=Equals(RSA_KEY_512.public_key()),
611 -
                        contact=Equals(reg.contact)),
612 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
613 -
                    new_authzr_uri=Equals(
614 -
                        u'https://example.org/acme/new-authz'),
615 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
616 -
                )))
617 -
618 -
    def test_register_error(self):
619 -
        """
620 -
        If some other error occurs during registration, a
621 -
        :exc:`txacme.client.ServerError` results.
622 -
        """
623 -
        sequence = RequestSequence(
624 -
            [_nonce_response(
625 -
                u'https://example.org/acme/new-reg',
626 -
                b'Nonce'),
627 -
             (MatchesListwise([
628 -
                 Equals(b'POST'),
629 -
                 Equals(u'https://example.org/acme/new-reg'),
630 -
                 Equals({}),
631 -
                 Always(),
632 -
                 on_jws(Equals({
633 -
                     u'resource': u'new-reg',
634 -
                     u'contact': [u'mailto:example@example.com']}))]),
635 -
              (http.SERVICE_UNAVAILABLE,
636 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
637 -
                b'replay-nonce': b64encode(b'Nonce2'),
638 -
                },
639 -
               _json_dumps(
640 -
                   {u'status': http.SERVICE_UNAVAILABLE,
641 -
                    u'type': u'urn:acme:error:rateLimited',
642 -
                    u'detail': u'The request exceeds a rate limit'}
643 -
               )))],
644 -
            self.expectThat)
645 -
        client = self.useFixture(
646 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
647 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
648 -
        with sequence.consume(self.fail):
649 -
            d = client.register(reg)
650 -
            self.assertThat(
651 -
                d, failed_with(MatchesAll(
652 -
                    IsInstance(ServerError),
653 -
                    MatchesStructure(
654 -
                        message=MatchesStructure(
655 -
                            typ=Equals(u'urn:acme:error:rateLimited'),
656 -
                            detail=Equals(u'The request exceeds a rate limit'),
657 -
                            )))))
658 -
659 -
    def test_register_bad_nonce_once(self):
660 -
        """
661 -
        If a badNonce error is received, we clear all old nonces and retry the
662 -
        request once.
663 -
        """
664 -
        sequence = RequestSequence(
665 -
            [(MatchesListwise([
666 -
                 Equals(b'POST'),
667 -
                 Equals(u'https://example.org/acme/new-reg'),
668 -
                 Equals({}),
669 -
                 Always(),
670 -
                 on_jws(Equals({
671 -
                     u'resource': u'new-reg',
672 -
                     u'contact': [u'mailto:example@example.com']}))]),
673 -
              (http.SERVICE_UNAVAILABLE,
674 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
675 -
                b'replay-nonce': b64encode(b'Nonce2'),
676 -
                },
677 -
               _json_dumps(
678 -
                   {u'status': http.SERVICE_UNAVAILABLE,
679 -
                    u'type': u'urn:acme:error:badNonce',
680 -
                    u'detail': u'The client sent a bad nonce'}
681 -
               ))),
682 -
             (MatchesListwise([
683 -
                 Equals(b'POST'),
684 -
                 Equals(u'https://example.org/acme/new-reg'),
685 -
                 Equals({}),
686 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
687 -
                 on_jws(Equals({
688 -
                     u'resource': u'new-reg',
689 -
                     u'contact': [u'mailto:example@example.com'],
690 -
                 }), nonce=b'Nonce2')]),
691 -
              (http.CREATED,
692 -
               {b'content-type': JSON_CONTENT_TYPE,
693 -
                b'replay-nonce': b64encode(b'Nonce3'),
694 -
                b'location': b'https://example.org/acme/reg/1',
695 -
                b'link': b','.join([
696 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
697 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
698 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
699 -
                ])},
700 -
               _json_dumps({
701 -
                   u'key': {
702 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
703 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
704 -
                       u'e': u'AQAB',
705 -
                       u'kty': u'RSA'},
706 -
                   u'contact': [u'mailto:example@example.com'],
707 -
               })))],
708 -
            self.expectThat)
709 -
        client = self.useFixture(
710 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
711 -
        # Stash a few nonces so that we have some to clear on the retry.
712 -
        client._client._nonces.update(
713 -
            [b'OldNonce1', b'OldNonce2', b'OldNonce3', b'OldNonce4'])
714 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
715 -
        with sequence.consume(self.fail):
716 -
            d = client.register(reg)
717 -
            self.assertThat(
718 -
                d, succeeded(MatchesStructure(
719 -
                    body=MatchesStructure(
720 -
                        key=Equals(RSA_KEY_512.public_key()),
721 -
                        contact=Equals(reg.contact)),
722 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
723 -
                    new_authzr_uri=Equals(
724 -
                        u'https://example.org/acme/new-authz'),
725 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
726 -
                )))
727 -
        self.assertThat(client._client._nonces, Equals(set([b'Nonce3'])))
728 -
729 -
    def test_register_bad_nonce_twice(self):
730 -
        """
731 -
        If a badNonce error is received on a retry, fail the request.
732 -
        """
733 -
        sequence = RequestSequence(
734 -
            [_nonce_response(
735 -
                u'https://example.org/acme/new-reg',
736 -
                b'Nonce'),
737 -
             (MatchesListwise([
738 -
                 Equals(b'POST'),
739 -
                 Equals(u'https://example.org/acme/new-reg'),
740 -
                 Equals({}),
741 -
                 Always(),
742 -
                 on_jws(Equals({
743 -
                     u'resource': u'new-reg',
744 -
                     u'contact': [u'mailto:example@example.com']}))]),
745 -
              (http.SERVICE_UNAVAILABLE,
746 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
747 -
                b'replay-nonce': b64encode(b'Nonce2'),
748 -
                },
749 -
               _json_dumps(
750 -
                   {u'status': http.SERVICE_UNAVAILABLE,
751 -
                    u'type': u'urn:acme:error:badNonce',
752 -
                    u'detail': u'The client sent a bad nonce'}
753 -
               ))),
754 -
             (MatchesListwise([
755 -
                 Equals(b'POST'),
756 -
                 Equals(u'https://example.org/acme/new-reg'),
757 -
                 Equals({}),
758 -
                 Always(),
759 -
                 on_jws(Equals({
760 -
                     u'resource': u'new-reg',
761 -
                     u'contact': [u'mailto:example@example.com']}))]),
762 -
              (http.SERVICE_UNAVAILABLE,
763 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
764 -
                b'replay-nonce': b64encode(b'Nonce3'),
765 -
                },
766 -
               _json_dumps(
767 -
                   {u'status': http.SERVICE_UNAVAILABLE,
768 -
                    u'type': u'urn:acme:error:badNonce',
769 -
                    u'detail': u'The client sent a bad nonce'}
770 -
               )))],
771 -
            self.expectThat)
772 -
        client = self.useFixture(
773 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
774 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
775 -
        with sequence.consume(self.fail):
776 -
            d = client.register(reg)
777 -
            self.assertThat(
778 -
                d, failed_with(MatchesAll(
779 -
                    IsInstance(ServerError),
780 -
                    MatchesStructure(
781 -
                        message=MatchesStructure(
782 -
                            typ=Equals(u'urn:acme:error:badNonce'),
783 -
                            detail=Equals(u'The client sent a bad nonce'),
784 -
                            )))))
785 -
786 -
    def test_agree_to_tos(self):
787 -
        """
788 -
        Agreeing to the TOS returns a registration with the agreement updated.
789 -
        """
790 -
        tos = u'https://example.org/acme/terms'
791 -
        sequence = RequestSequence(
792 -
            [_nonce_response(
793 -
                u'https://example.org/acme/reg/1',
794 -
                b'Nonce'),
795 -
             (MatchesListwise([
796 -
                 Equals(b'POST'),
797 -
                 Equals(u'https://example.org/acme/reg/1'),
798 -
                 Equals({}),
799 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
800 -
                 on_jws(ContainsDict({
801 -
                     u'resource': Equals(u'reg'),
802 -
                     u'agreement': Equals(tos)}))]),
803 -
              (http.ACCEPTED,
804 -
               {b'content-type': JSON_CONTENT_TYPE,
805 -
                b'replay-nonce': b64encode(b'Nonce2'),
806 -
                b'link': b','.join([
807 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
808 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
809 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
810 -
                ])},
811 -
               _json_dumps({
812 -
                   u'key': {
813 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
814 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
815 -
                       u'e': u'AQAB',
816 -
                       u'kty': u'RSA'},
817 -
                   u'contact': [u'mailto:example@example.com'],
818 -
                   u'agreement': tos,
819 -
               })))],
820 -
            self.expectThat)
821 -
        client = self.useFixture(
822 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
823 -
        reg = messages.RegistrationResource(
824 -
            body=messages.Registration(
825 -
                contact=(u'mailto:example@example.com',),
826 -
                key=RSA_KEY_512.public_key()),
827 -
            uri=u'https://example.org/acme/reg/1',
828 -
            new_authzr_uri=u'https://example.org/acme/new-authz',
829 -
            terms_of_service=tos)
830 -
        with sequence.consume(self.fail):
831 -
            d = client.agree_to_tos(reg)
832 -
            self.assertThat(
833 -
                d, succeeded(MatchesStructure(
834 -
                    body=MatchesStructure(
835 -
                        key=Equals(RSA_KEY_512.public_key()),
836 -
                        contact=Equals(reg.body.contact),
837 -
                        agreement=Equals(tos)),
838 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
839 -
                    new_authzr_uri=Equals(
840 -
                        u'https://example.org/acme/new-authz'),
841 -
                    terms_of_service=Equals(tos),
842 -
                )))
843 -
844 -
    def test_from_directory(self):
845 -
        """
846 -
        :func:`~txacme.client.Client.from_url` constructs a client with a
847 -
        directory retrieved from the given URL.
848 -
        """
849 -
        new_reg = u'https://example.org/acme/new-reg'
850 -
        sequence = RequestSequence(
851 -
            [(MatchesListwise([
852 -
                Equals(b'GET'),
853 -
                Equals(u'https://example.org/acme/'),
854 -
                Always(),
855 -
                Always(),
856 -
                Always()]),
857 -
             (http.OK,
858 -
              {b'content-type': JSON_CONTENT_TYPE,
859 -
               b'replay-nonce': b64encode(b'Nonce')},
860 -
              _json_dumps({
861 -
                  u'new-reg': new_reg,
862 -
                  u'revoke-cert': u'https://example.org/acme/revoke-cert',
863 -
                  u'new-authz': u'https://example.org/acme/new-authz',
864 -
              })))],
865 -
            self.expectThat)
866 -
        agent = RequestTraversalAgent(
867 -
            StringStubbingResource(sequence))
868 -
        jws_client = JWSClient(agent, key=RSA_KEY_512, alg=RS256)
869 -
        jws_client._treq._data_to_body_producer = _SynchronousProducer
870 -
        with sequence.consume(self.fail):
871 -
            d = Client.from_url(
872 -
                reactor, URL.fromText(u'https://example.org/acme/'),
873 -
                key=RSA_KEY_512, alg=RS256,
874 -
                jws_client=jws_client)
875 -
            self.assertThat(
876 -
                d,
877 -
                succeeded(
878 -
                    MatchesAll(
879 -
                        AfterPreprocessing(
880 -
                            lambda client:
881 -
                            client.directory[messages.NewRegistration()],
882 -
                            Equals(new_reg)))))
883 -
884 -
    def test_default_client(self):
885 -
        """
886 -
        ``~txacme.client._default_client`` constructs a client if one was not
887 -
        provided.
888 -
        """
889 -
        reactor = MemoryReactor()
890 -
        client = _default_client(None, reactor, RSA_KEY_512, RS384)
891 -
        self.assertThat(client, IsInstance(JWSClient))
892 -
        # We should probably assert some stuff about the treq.HTTPClient, but
893 -
        # it's hard without doing awful mock stuff.
894 -
895 -
    def test_request_challenges(self):
896 -
        """
897 -
        :meth:`~txacme.client.Client.request_challenges` creates a new
898 -
        authorization, and returns the authorization resource with a list of
899 -
        possible challenges to proceed with.
900 -
        """
901 -
        name = u'example.com'
902 -
        identifier_json = {u'type': u'dns',
903 -
                           u'value': name}
904 -
        identifier = messages.Identifier.from_json(identifier_json)
905 -
        challenges = [
906 -
            {u'type': u'http-01',
907 -
             u'uri': u'https://example.org/acme/authz/1/0',
908 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
909 -
            {u'type': u'dns',
910 -
             u'uri': u'https://example.org/acme/authz/1/1',
911 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
912 -
            ]
913 -
        sequence = RequestSequence(
914 -
            [_nonce_response(
915 -
                u'https://example.org/acme/new-authz',
916 -
                b'Nonce'),
917 -
             (MatchesListwise([
918 -
                 Equals(b'POST'),
919 -
                 Equals(u'https://example.org/acme/new-authz'),
920 -
                 Equals({}),
921 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
922 -
                 on_jws(Equals({
923 -
                     u'resource': u'new-authz',
924 -
                     u'identifier': identifier_json,
925 -
                     }))]),
926 -
              (http.CREATED,
927 -
               {b'content-type': JSON_CONTENT_TYPE,
928 -
                b'replay-nonce': b64encode(b'Nonce2'),
929 -
                b'location': b'https://example.org/acme/authz/1',
930 -
                b'link': b'<https://example.org/acme/new-cert>;rel="next"',
931 -
                },
932 -
               _json_dumps({
933 -
                   u'status': u'pending',
934 -
                   u'identifier': identifier_json,
935 -
                   u'challenges': challenges,
936 -
                   u'combinations': [[0], [1]],
937 -
               })))],
938 -
            self.expectThat)
939 -
        client = self.useFixture(
940 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
941 -
        with sequence.consume(self.fail):
942 -
            self.assertThat(
943 -
                client.request_challenges(identifier),
944 -
                succeeded(MatchesStructure(
945 -
                    body=MatchesStructure(
946 -
                        identifier=Equals(identifier),
947 -
                        challenges=Equals(
948 -
                            tuple(map(
949 -
                                messages.ChallengeBody.from_json,
950 -
                                challenges))),
951 -
                        combinations=Equals(((0,), (1,))),
952 -
                        status=Equals(messages.STATUS_PENDING)),
953 -
                    new_cert_uri=Equals(
954 -
                        u'https://example.org/acme/new-cert'),
955 -
                )))
956 -
957 -
    @example(http.CREATED, http.FOUND)
958 -
    @given(s.sampled_from(sorted(http.RESPONSES)),
959 -
           s.sampled_from(sorted(http.RESPONSES)))
960 -
    def test_expect_response_wrong_code(self, expected, actual):
961 -
        """
962 -
        ``_expect_response`` raises `~acme.errors.ClientError` if the response
963 -
        code does not match the expected code.
964 -
        """
965 -
        assume(expected != actual)
966 -
        response = TestResponse(code=actual)
967 -
        with ExpectedException(errors.ClientError):
968 -
            Client._expect_response(response, expected)
969 -
970 -
    def test_authorization_missing_link(self):
971 -
        """
972 -
        ``_parse_authorization`` raises `~acme.errors.ClientError` if the
973 -
        ``"next"`` link is missing.
974 -
        """
975 -
        response = TestResponse()
976 -
        with ExpectedException(errors.ClientError, '"next" link missing'):
977 -
            Client._parse_authorization(response)
978 -
979 -
    def test_authorization_unexpected_identifier(self):
980 -
        """
981 -
        ``_check_authorization`` raises `~acme.errors.UnexpectedUpdate` if the
982 -
        return identifier doesn't match.
983 -
        """
984 -
        with ExpectedException(errors.UnexpectedUpdate):
985 -
            Client._check_authorization(
986 -
                messages.AuthorizationResource(
987 -
                    body=messages.Authorization()),
988 -
                messages.Identifier(
989 -
                    typ=messages.IDENTIFIER_FQDN, value=u'example.org'))
990 -
991 -
    @example(u'example.com')
992 -
    @given(ts.dns_names())
993 -
    def test_fqdn_identifier(self, name):
189 +
    def test_fqdn_identifier(self):
994 190
        """
995 191
        `~txacme.client.fqdn_identifier` constructs an
996 192
        `~acme.messages.Identifier` of the right type.
997 193
        """
998 -
        self.assertThat(
999 -
            fqdn_identifier(name),
1000 -
            MatchesStructure(
1001 -
                typ=Equals(messages.IDENTIFIER_FQDN),
1002 -
                value=Equals(name)))
1003 -
1004 -
    def test_answer_challenge(self):
1005 -
        """
1006 -
        `~txacme.client.Client.answer_challenge` responds to a challenge and
1007 -
        returns the updated challenge.
1008 -
        """
1009 -
        key_authorization = u'blahblahblah'
1010 -
        uri = u'https://example.org/acme/authz/1/0'
1011 -
        sequence = RequestSequence(
1012 -
            [_nonce_response(
1013 -
                u'https://example.org/acme/authz/1/0',
1014 -
                b'Nonce'),
1015 -
             (MatchesListwise([
1016 -
                 Equals(b'POST'),
1017 -
                 Equals(uri),
1018 -
                 Equals({}),
1019 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
1020 -
                 on_jws(Equals({
1021 -
                     u'resource': u'challenge',
1022 -
                     u'type': u'http-01',
1023 -
                     }))]),
1024 -
              (http.OK,
1025 -
               {b'content-type': JSON_CONTENT_TYPE,
1026 -
                b'replay-nonce': b64encode(b'Nonce2'),
1027 -
                b'link': b'<https://example.org/acme/authz/1>;rel="up"',
1028 -
                },
1029 -
               _json_dumps({
1030 -
                   u'uri': uri,
1031 -
                   u'type': u'http-01',
1032 -
                   u'status': u'processing',
1033 -
                   u'token': u'DGyRejmCefe7v4NfDGDKfA',
1034 -
               })))],
1035 -
            self.expectThat)
1036 -
        client = self.useFixture(
1037 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1038 -
        with sequence.consume(self.fail):
1039 -
            self.assertThat(
1040 -
                client.answer_challenge(
1041 -
                    messages.ChallengeBody(
1042 -
                        uri=uri,
1043 -
                        chall=challenges.HTTP01(token=b'blahblah'),
1044 -
                        status=messages.STATUS_PENDING),
1045 -
                    challenges.HTTP01Response(
1046 -
                        key_authorization=key_authorization)),
1047 -
                succeeded(MatchesStructure(
1048 -
                    body=MatchesStructure(),
1049 -
                    authzr_uri=Equals(
1050 -
                        u'https://example.org/acme/authz/1'),
1051 -
                )))
1052 -
1053 -
    def test_challenge_missing_link(self):
1054 -
        """
1055 -
        ``_parse_challenge`` raises `~acme.errors.ClientError` if the ``"up"``
1056 -
        link is missing.
1057 -
        """
1058 -
        response = TestResponse()
1059 -
        with ExpectedException(errors.ClientError, '"up" link missing'):
1060 -
            Client._parse_challenge(response)
194 +
        name = u'example.com'
195 +
        result = fqdn_identifier(name)
196 +
        self.assertEqual(messages.IDENTIFIER_FQDN, result.typ)
197 +
        self.assertEqual(name, result.value)
1061 198
1062 -
    @example(URL.fromText(u'https://example.org/'),
1063 -
             URL.fromText(u'https://example.com/'))
1064 -
    @given(ts.urls(), ts.urls())
1065 -
    def test_challenge_unexpected_uri(self, url1, url2):
199 +
    def test_challenge_unexpected_uri(self):
1066 200
        """
1067 201
        ``_check_challenge`` raises `~acme.errors.UnexpectedUpdate` if the
1068 202
        challenge does not have the expected URI.
1069 203
        """
1070 -
        url1 = url1.asURI().asText()
1071 -
        url2 = url2.asURI().asText()
1072 -
        assume(url1 != url2)
1073 -
        with ExpectedException(errors.UnexpectedUpdate):
204 +
        # Crazy dance that was used in previous test.
205 +
        url1 = URL.fromText(u'https://example.org/').asURI().asText()
206 +
        url2 = URL.fromText(u'https://example.com/').asURI().asText()
207 +
208 +
        with self.assertRaises(errors.UnexpectedUpdate):
1074 209
            Client._check_challenge(
1075 -
                messages.ChallengeResource(
210 +
                challenge=messages.ChallengeResource(
1076 211
                    body=messages.ChallengeBody(chall=None, uri=url1)),
1077 -
                messages.ChallengeBody(chall=None, uri=url2))
1078 -
1079 -
    @example(now=1459184402., name=u'example.com', retry_after=60,
1080 -
             date_string=False)
1081 -
    @example(now=1459184402., name=u'example.org', retry_after=60,
1082 -
             date_string=True)
1083 -
    @given(now=s.floats(min_value=0., max_value=2147483648.),
1084 -
           name=ts.dns_names(),
1085 -
           retry_after=s.none() | s.integers(min_value=0, max_value=1000),
1086 -
           date_string=s.booleans())
1087 -
    def test_poll(self, now, name, retry_after, date_string):
1088 -
        """
1089 -
        `~txacme.client.Client.poll` retrieves the latest state of an
1090 -
        authorization resource, as well as the minimum time to wait before
1091 -
        polling the state again.
1092 -
        """
1093 -
        if retry_after is None:
1094 -
            retry_after_encoded = None
1095 -
            retry_after = 5
1096 -
        elif date_string:
1097 -
            retry_after_encoded = http.datetimeToString(retry_after + now)
1098 -
        else:
1099 -
            retry_after_encoded = u'{}'.format(retry_after).encode('ascii')
1100 -
        identifier_json = {u'type': u'dns',
1101 -
                           u'value': name}
1102 -
        identifier = messages.Identifier.from_json(identifier_json)
1103 -
        challenges = [
1104 -
            {u'type': u'http-01',
1105 -
             u'status': u'invalid',
1106 -
             u'uri': u'https://example.org/acme/authz/1/0',
1107 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1108 -
            {u'type': u'dns',
1109 -
             u'status': u'pending',
1110 -
             u'uri': u'https://example.org/acme/authz/1/1',
1111 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1112 -
            ]
1113 -
        authzr = messages.AuthorizationResource(
1114 -
            uri=u'https://example.org/acme/authz/1',
1115 -
            body=messages.Authorization(
1116 -
                identifier=identifier))
1117 -
        response_headers = {
1118 -
            b'content-type': JSON_CONTENT_TYPE,
1119 -
            b'replay-nonce': b64encode(b'Nonce2'),
1120 -
            b'location': b'https://example.org/acme/authz/1',
1121 -
            b'link': b'<https://example.org/acme/new-cert>;rel="next"',
1122 -
            }
1123 -
        if retry_after_encoded is not None:
1124 -
            response_headers[b'retry-after'] = retry_after_encoded
1125 -
        sequence = RequestSequence(
1126 -
            [(MatchesListwise([
1127 -
                Equals(b'GET'),
1128 -
                Equals(u'https://example.org/acme/authz/1'),
1129 -
                Equals({}),
1130 -
                Always(),
1131 -
                Always()]),
1132 -
              (http.OK,
1133 -
               response_headers,
1134 -
               _json_dumps({
1135 -
                   u'status': u'invalid',
1136 -
                   u'identifier': identifier_json,
1137 -
                   u'challenges': challenges,
1138 -
                   u'combinations': [[0], [1]],
1139 -
               })))],
1140 -
            self.expectThat)
1141 -
        fixture = self.useFixture(
1142 -
            ClientFixture(sequence, key=RSA_KEY_512))
1143 -
        fixture.clock.rightNow = now
1144 -
        client = fixture.client
1145 -
        with sequence.consume(self.fail):
1146 -
            self.assertThat(
1147 -
                client.poll(authzr),
1148 -
                succeeded(MatchesListwise([
1149 -
                    MatchesStructure(
1150 -
                        body=MatchesStructure(
1151 -
                            identifier=Equals(identifier),
1152 -
                            challenges=Equals(
1153 -
                                tuple(map(
1154 -
                                    messages.ChallengeBody.from_json,
1155 -
                                    challenges))),
1156 -
                            combinations=Equals(((0,), (1,))),
1157 -
                            status=Equals(messages.STATUS_INVALID)),
1158 -
                        new_cert_uri=Equals(
1159 -
                            u'https://example.org/acme/new-cert')),
1160 -
                    Nearly(retry_after, 1.0),
1161 -
                ])))
1162 -
1163 -
    def test_tls_sni_01_no_singleton(self):
1164 -
        """
1165 -
        If a suitable singleton challenge is not found,
1166 -
        `.NoSupportedChallenges` is raised.
1167 -
        """
1168 -
        challs = [
1169 -
            {u'type': u'http-01',
1170 -
             u'uri': u'https://example.org/acme/authz/1/0',
1171 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1172 -
            {u'type': u'dns',
1173 -
             u'uri': u'https://example.org/acme/authz/1/1',
1174 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1175 -
            {u'type': u'tls-sni-01',
1176 -
             u'uri': u'https://example.org/acme/authz/1/2',
1177 -
             u'token': u'f8IfXqddYr8IJqYHSH6NpA'},
1178 -
            ]
1179 -
        combinations = ((0, 2), (1, 2))
1180 -
        authzr = messages.AuthorizationResource(
1181 -
            body=messages.Authorization(
1182 -
                challenges=list(map(
1183 -
                    messages.ChallengeBody.from_json,
1184 -
                    challs)),
1185 -
                combinations=combinations))
1186 -
        with ExpectedException(NoSupportedChallenges):
1187 -
            _find_supported_challenge(
1188 -
                authzr, [NullResponder(challenges.TLSSNI01.typ)])
1189 -
1190 -
    def test_no_tls_sni_01(self):
1191 -
        """
1192 -
        If no tls-sni-01 challenges are available, `.NoSupportedChallenges` is
1193 -
        raised.
1194 -
        """
1195 -
        challs = [
1196 -
            {u'type': u'http-01',
1197 -
             u'uri': u'https://example.org/acme/authz/1/0',
1198 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1199 -
            {u'type': u'dns',
1200 -
             u'uri': u'https://example.org/acme/authz/1/1',
1201 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1202 -
            {u'type': u'tls-sni-01',
1203 -
             u'uri': u'https://example.org/acme/authz/1/2',
1204 -
             u'token': u'f8IfXqddYr8IJqYHSH6NpA'},
1205 -
            ]
1206 -
        combinations = ((0,), (1,))
1207 -
        authzr = messages.AuthorizationResource(
1208 -
            body=messages.Authorization(
1209 -
                challenges=list(map(
1210 -
                    messages.ChallengeBody.from_json,
1211 -
                    challs)),
1212 -
                combinations=combinations))
1213 -
        with ExpectedException(NoSupportedChallenges):
1214 -
            _find_supported_challenge(
1215 -
                authzr, [NullResponder(challenges.TLSSNI01.typ)])
1216 -
1217 -
    def test_only_tls_sni_01(self):
1218 -
        """
1219 -
        If a singleton tls-sni-01 challenge is available, it is returned.
1220 -
        """
1221 -
        challs = list(map(
1222 -
            messages.ChallengeBody.from_json,
1223 -
            [{u'type': u'http-01',
1224 -
              u'uri': u'https://example.org/acme/authz/1/0',
1225 -
              u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1226 -
             {u'type': u'dns',
1227 -
              u'uri': u'https://example.org/acme/authz/1/1',
1228 -
              u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1229 -
             {u'type': u'tls-sni-01',
1230 -
              u'uri': u'https://example.org/acme/authz/1/2',
1231 -
              u'token': u'f8IfXqddYr8IJqYHSH6NpA'},
1232 -
             ]))
1233 -
        combinations = ((0,), (1,), (2,))
1234 -
        authzr = messages.AuthorizationResource(
1235 -
            body=messages.Authorization(
1236 -
                challenges=challs,
1237 -
                combinations=combinations))
1238 -
        responder = NullResponder(challenges.TLSSNI01.typ)
1239 -
        self.assertThat(
1240 -
            _find_supported_challenge(authzr, [responder]),
1241 -
            MatchesListwise([
1242 -
                Is(responder),
1243 -
                MatchesAll(
1244 -
                    IsInstance(messages.ChallengeBody),
1245 -
                    MatchesStructure(
1246 -
                        chall=IsInstance(challenges.TLSSNI01)))]))
1247 -
1248 -
    def test_answer_challenge_function(self):
1249 -
        """
1250 -
        The challenge is found in the responder after invoking
1251 -
        `~txacme.client.answer_challenge`.
1252 -
        """
1253 -
        recorded_challenges = set()
1254 -
        responder = RecordingResponder(recorded_challenges, u'tls-sni-01')
1255 -
        uri = u'https://example.org/acme/authz/1/1'
1256 -
        challb = messages.ChallengeBody.from_json({
1257 -
            u'uri': uri,
1258 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1259 -
            u'type': u'tls-sni-01',
1260 -
            u'status': u'pending'})
1261 -
        identifier_json = {u'type': u'dns',
1262 -
                           u'value': u'example.com'}
1263 -
        identifier = messages.Identifier.from_json(identifier_json)
1264 -
        authzr = messages.AuthorizationResource(
1265 -
            body=messages.Authorization(
1266 -
                identifier=identifier,
1267 -
                challenges=[challb],
1268 -
                combinations=[[0]]))
1269 -
        sequence = RequestSequence(
1270 -
            [_nonce_response(
1271 -
                u'https://example.org/acme/authz/1/1',
1272 -
                b'Nonce'),
1273 -
             (MatchesListwise([
1274 -
                 Equals(b'POST'),
1275 -
                 Equals(uri),
1276 -
                 Equals({}),
1277 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
1278 -
                 on_jws(Equals({
1279 -
                     u'resource': u'challenge',
1280 -
                     u'type': u'tls-sni-01',
1281 -
                     }))]),
1282 -
              (http.OK,
1283 -
               {b'content-type': JSON_CONTENT_TYPE,
1284 -
                b'replay-nonce': b64encode(b'Nonce2'),
1285 -
                b'link': b'<https://example.org/acme/authz/1>;rel="up"',
1286 -
                },
1287 -
               _json_dumps({
1288 -
                   u'uri': uri,
1289 -
                   u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1290 -
                   u'type': u'tls-sni-01',
1291 -
                   u'status': u'processing',
1292 -
               })))],
1293 -
            self.expectThat)
1294 -
        client = self.useFixture(
1295 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1296 -
        with sequence.consume(self.fail):
1297 -
            d = answer_challenge(authzr, client, [responder])
1298 -
            self.assertThat(d, succeeded(Always()))
1299 -
            stop_responding = d.result
1300 -
            self.assertThat(
1301 -
                recorded_challenges,
1302 -
                MatchesListwise([
1303 -
                    IsInstance(challenges.TLSSNI01)
1304 -
                ]))
1305 -
            self.assertThat(
1306 -
                stop_responding(),
1307 -
                succeeded(Always()))
1308 -
            self.assertThat(recorded_challenges, Equals(set()))
1309 -
1310 -
    def _make_poll_response(self, uri, identifier_json):
1311 -
        """
1312 -
        Return a factory for a poll response.
1313 -
        """
1314 -
        def rr(status, error=None):
1315 -
            chall = {
1316 -
                u'type': u'tls-sni-01',
1317 -
                u'status': status,
1318 -
                u'uri': uri + u'/0',
1319 -
                u'token': u'IlirfxKKXAsHtmzK29Pj8A'}
1320 -
            if error is not None:
1321 -
                chall[u'error'] = error
1322 -
            return (
1323 -
                MatchesListwise([
1324 -
                    Equals(b'GET'),
1325 -
                    Equals(uri),
1326 -
                    Equals({}),
1327 -
                    Always(),
1328 -
                    Always()]),
1329 -
                (http.ACCEPTED,
1330 -
                 {b'content-type': JSON_CONTENT_TYPE,
1331 -
                  b'replay-nonce': b64encode(b'nonce2'),
1332 -
                  b'location': uri.encode('ascii'),
1333 -
                  b'link': b'<https://example.org/acme/new-cert>;rel="next"'},
1334 -
                 _json_dumps({
1335 -
                     u'status': status,
1336 -
                     u'identifier': identifier_json,
1337 -
                     u'challenges': [chall],
1338 -
                     u'combinations': [[0]],
1339 -
                 })))
1340 -
        return rr
1341 -
1342 -
    @example(name=u'example.com')
1343 -
    @given(name=ts.dns_names())
1344 -
    def test_poll_timeout(self, name):
1345 -
        """
1346 -
        If the timeout is exceeded during polling, `.poll_until_valid` will
1347 -
        fail with ``CancelledError``.
1348 -
        """
1349 -
        identifier_json = {u'type': u'dns', u'value': name}
1350 -
        uri = u'https://example.org/acme/authz/1'
1351 -
        rr = self._make_poll_response(uri, identifier_json)
1352 -
        sequence = RequestSequence(
1353 -
            [rr(u'pending'),
1354 -
             rr(u'pending'),
1355 -
             rr(u'pending'),
1356 -
             ], self.expectThat)
1357 -
        clock = Clock()
1358 -
        challb = messages.ChallengeBody.from_json({
1359 -
            u'uri': uri + u'/0',
1360 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1361 -
            u'type': u'tls-sni-01',
1362 -
            u'status': u'pending'})
1363 -
        authzr = messages.AuthorizationResource(
1364 -
            uri=uri,
1365 -
            body=messages.Authorization(
1366 -
                identifier=messages.Identifier.from_json(identifier_json),
1367 -
                challenges=[challb],
1368 -
                combinations=[[0]]))
1369 -
        client = self.useFixture(
1370 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1371 -
        with sequence.consume(self.fail):
1372 -
            d = poll_until_valid(authzr, clock, client, timeout=14.)
1373 -
            clock.pump([5, 5, 5])
1374 -
            self.assertThat(
1375 -
                d,
1376 -
                failed_with(IsInstance(CancelledError)))
1377 -
1378 -
    @example(name=u'example.com')
1379 -
    @given(name=ts.dns_names())
1380 -
    def test_poll_invalid(self, name):
1381 -
        """
1382 -
        If the authorization enters an invalid state while polling,
1383 -
        `.poll_until_valid` will fail with `.AuthorizationFailed`.
1384 -
        """
1385 -
        identifier_json = {u'type': u'dns', u'value': name}
1386 -
        uri = u'https://example.org/acme/authz/1'
1387 -
        rr = self._make_poll_response(uri, identifier_json)
1388 -
        sequence = RequestSequence(
1389 -
            [rr(u'pending'),
1390 -
             rr(u'invalid', {
1391 -
                 u'type': u'urn:acme:error:connection',
1392 -
                 u'detail': u'Failed to connect'}),
1393 -
             ], self.expectThat)
1394 -
        clock = Clock()
1395 -
        challb = messages.ChallengeBody.from_json({
1396 -
            u'uri': uri + u'/0',
1397 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1398 -
            u'type': u'tls-sni-01',
1399 -
            u'status': u'pending',
1400 -
            })
1401 -
        authzr = messages.AuthorizationResource(
1402 -
            uri=uri,
1403 -
            body=messages.Authorization(
1404 -
                identifier=messages.Identifier.from_json(identifier_json),
1405 -
                challenges=[challb],
1406 -
                combinations=[[0]]))
1407 -
        client = self.useFixture(
1408 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1409 -
        with sequence.consume(self.fail):
1410 -
            d = poll_until_valid(authzr, clock, client, timeout=14.)
1411 -
            clock.pump([5, 5])
1412 -
            self.assertThat(
1413 -
                d,
1414 -
                failed_with(MatchesAll(
1415 -
                    IsInstance(AuthorizationFailed),
1416 -
                    MatchesStructure(
1417 -
                        status=Equals(messages.STATUS_INVALID),
1418 -
                        errors=Equals([
1419 -
                            messages.Error(
1420 -
                                typ=u'urn:acme:error:connection',
1421 -
                                detail=u'Failed to connect',
1422 -
                                title=None)])),
1423 -
                    AfterPreprocessing(
1424 -
                        repr,
1425 -
                        StartsWith(u'AuthorizationFailed(<Status(invalid)')))))
1426 -
1427 -
    @example(name=u'example.com')
1428 -
    @given(name=ts.dns_names())
1429 -
    def test_poll_valid(self, name):
1430 -
        """
1431 -
        If the authorization enters a valid state while polling,
1432 -
        `.poll_until_valid` will fire with the updated authorization.
1433 -
        """
1434 -
        identifier_json = {u'type': u'dns', u'value': name}
1435 -
        uri = u'https://example.org/acme/authz/1'
1436 -
        rr = self._make_poll_response(uri, identifier_json)
1437 -
        sequence = RequestSequence(
1438 -
            [rr(u'pending'),
1439 -
             rr(u'valid'),
1440 -
             ], self.expectThat)
1441 -
        clock = Clock()
1442 -
        challb = messages.ChallengeBody.from_json({
1443 -
            u'uri': uri + u'/0',
1444 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1445 -
            u'type': u'tls-sni-01',
1446 -
            u'status': u'pending',
1447 -
            })
1448 -
        authzr = messages.AuthorizationResource(
1449 -
            uri=uri,
1450 -
            body=messages.Authorization(
1451 -
                identifier=messages.Identifier.from_json(identifier_json),
1452 -
                challenges=[challb],
1453 -
                combinations=[[0]]))
1454 -
        client = self.useFixture(
1455 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1456 -
        with sequence.consume(self.fail):
1457 -
            d = poll_until_valid(authzr, clock, client, timeout=14.)
1458 -
            clock.pump([5, 5])
1459 -
            self.assertThat(
1460 -
                d,
1461 -
                succeeded(IsInstance(messages.AuthorizationResource)))
1462 -
1463 -
    @example(name=u'example.com',
1464 -
             issuer_url=URL.fromText(u'https://example.org/acme/ca-cert'))
1465 -
    @given(name=ts.dns_names(),
1466 -
           issuer_url=ts.urls())
1467 -
    def test_request_issuance(self, name, issuer_url):
1468 -
        """
1469 -
        If issuing is successful, a certificate resource is returned.
1470 -
        """
1471 -
        assume(len(name) <= 64)
1472 -
        cert_request = CertificateRequest(
1473 -
            csr=csr_for_names([name], RSA_KEY_512_RAW))
1474 -
        cert, _ = generate_tls_sni_01_cert(
1475 -
            name, _generate_private_key=lambda _: RSA_KEY_512_RAW)
1476 -
        cert_bytes = cert.public_bytes(serialization.Encoding.DER)
1477 -
        sequence = RequestSequence([
1478 -
            _nonce_response(u'https://example.org/acme/new-cert', b'nonce'),
1479 -
            (MatchesListwise([
1480 -
                Equals(b'POST'),
1481 -
                Equals(u'https://example.org/acme/new-cert'),
1482 -
                Equals({}),
1483 -
                ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
1484 -
                on_jws(AfterPreprocessing(
1485 -
                    CertificateRequest.from_json,
1486 -
                    Equals(cert_request)))]),
1487 -
             (http.CREATED,
1488 -
              {b'content-type': DER_CONTENT_TYPE,
1489 -
               b'replay-nonce': b64encode(b'nonce2'),
1490 -
               b'location': b'https://example.org/acme/cert/asdf',
1491 -
               b'link': u'<{!s}>;rel="up"'.format(
1492 -
                   issuer_url.asURI().asText()).encode('utf-8')},
1493 -
              cert_bytes)),
1494 -
        ], self.expectThat)
1495 -
        client = self.useFixture(
1496 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1497 -
        with sequence.consume(self.fail):
1498 -
            self.assertThat(
1499 -
                client.request_issuance(
1500 -
                    CertificateRequest(
1501 -
                        csr=csr_for_names([name], RSA_KEY_512_RAW))),
1502 -
                succeeded(MatchesStructure(
1503 -
                    body=Equals(cert_bytes))))
1504 -
1505 -
    def test_fetch_chain_empty(self):
1506 -
        """
1507 -
        If a certificate has no issuer link, `.Client.fetch_chain` returns an
1508 -
        empty chain.
1509 -
        """
1510 -
        cert = messages.CertificateResource(cert_chain_uri=None)
1511 -
        sequence = RequestSequence([], self.expectThat)
1512 -
        client = self.useFixture(
1513 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1514 -
        with sequence.consume(self.fail):
1515 -
            self.assertThat(
1516 -
                client.fetch_chain(cert),
1517 -
                succeeded(Equals([])))
1518 -
1519 -
    def _make_cert_sequence(self, cert_urls):
1520 -
        """
1521 -
        Build a sequence for fetching a list of certificates.
1522 -
        """
1523 -
        return RequestSequence([
1524 -
            (MatchesListwise([
1525 -
                Equals(b'GET'),
1526 -
                Equals(url),
1527 -
                Equals({}),
1528 -
                ContainsDict({b'Accept': Equals([DER_CONTENT_TYPE])}),
1529 -
                Always()]),
1530 -
             (http.OK,
1531 -
              {b'content-type': DER_CONTENT_TYPE,
1532 -
               b'location': url.encode('utf-8'),
1533 -
               b'link':
1534 -
               u'<{!s}>;rel="up"'.format(
1535 -
                   issuer_url).encode('utf-8')
1536 -
               if issuer_url is not None else b''},
1537 -
              b''))
1538 -
            for url, issuer_url
1539 -
            in cert_urls
1540 -
            ], self.expectThat)
1541 -
1542 -
    @settings(deadline=None)
1543 -
    @example([u'http://example.com/1', u'http://example.com/2'])
1544 -
    @given(s.lists(s.integers()
1545 -
                   .map(lambda n: u'http://example.com/{}'.format(n)),
1546 -
                   min_size=1, max_size=10))
1547 -
    def test_fetch_chain_okay(self, cert_urls):
1548 -
        """
1549 -
        A certificate chain that is shorter than the max length is returned.
1550 -
        """
1551 -
        cert = messages.CertificateResource(
1552 -
            uri=u'http://example.com/',
1553 -
            cert_chain_uri=cert_urls[0])
1554 -
        urls = list(zip(cert_urls, cert_urls[1:] + [None]))
1555 -
        sequence = self._make_cert_sequence(urls)
1556 -
        client = self.useFixture(
1557 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1558 -
        with sequence.consume(self.fail):
1559 -
            self.assertThat(
1560 -
                client.fetch_chain(cert),
1561 -
                succeeded(
1562 -
                    MatchesListwise([
1563 -
                        MatchesStructure(
1564 -
                            uri=Equals(url),
1565 -
                            cert_chain_uri=Equals(issuer_url))
1566 -
                        for url, issuer_url in urls])))
1567 -
1568 -
    @settings(deadline=None)
1569 -
    @example([u'http://example.com/{}'.format(n) for n in range(20)])
1570 -
    @given(s.lists(s.integers()
1571 -
                   .map(lambda n: u'http://example.com/{}'.format(n)),
1572 -
                   min_size=11))
1573 -
    def test_fetch_chain_too_long(self, cert_urls):
1574 -
        """
1575 -
        A certificate chain that is too long fails with
1576 -
        `~acme.errors.ClientError`.
1577 -
        """
1578 -
        cert = messages.CertificateResource(
1579 -
            uri=u'http://example.com/',
1580 -
            cert_chain_uri=cert_urls[0])
1581 -
        sequence = self._make_cert_sequence(
1582 -
            list(zip(cert_urls, cert_urls[1:]))[:10])
1583 -
        client = self.useFixture(
1584 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1585 -
        with sequence.consume(self.fail):
1586 -
            self.assertThat(
1587 -
                client.fetch_chain(cert),
1588 -
                failed_with(IsInstance(errors.ClientError)))
212 +
                challenge_body=messages.ChallengeBody(chall=None, uri=url2),
213 +
                )
1589 214
1590 215
1591 216
class JWSClientTests(TestCase):
1592 217
    """
1593 218
    :class:`.JWSClient` implements JWS-signed requests over HTTP.
1594 219
    """
1595 -
    def test_check_invalid_json(self):
1596 -
        """
1597 -
        If a JSON response is expected, but a response is received with a
1598 -
        non-JSON Content-Type, :exc:`~acme.errors.ClientError` is raised.
1599 -
        """
1600 -
        self.assertThat(
1601 -
            JWSClient._check_response(
1602 -
                TestResponse(content_type=b'application/octet-stream')),
1603 -
            failed_with(IsInstance(errors.ClientError)))
1604 -
1605 -
    def test_check_invalid_error_type(self):
1606 -
        """
1607 -
        If an error response is received with a non-JSON-problem Content-Type,
1608 -
        :exc:`~acme.errors.ClientError` is raised.
1609 -
        """
1610 -
        self.assertThat(
1611 -
            JWSClient._check_response(
1612 -
                TestResponse(
1613 -
                    code=http.FORBIDDEN,
1614 -
                    content_type=b'application/octet-stream')),
1615 -
            failed_with(IsInstance(errors.ClientError)))
1616 -
220 +
    @defer.inlineCallbacks
1617 221
    def test_check_invalid_error(self):
1618 222
        """
1619 223
        If an error response is received but cannot be parsed,
1620 224
        :exc:`~acme.errors.ServerError` is raised.
1621 225
        """
1622 -
        self.assertThat(
1623 -
            JWSClient._check_response(
1624 -
                TestResponse(
1625 -
                    code=http.FORBIDDEN,
1626 -
                    content_type=JSON_ERROR_CONTENT_TYPE)),
1627 -
            failed_with(IsInstance(ServerError)))
226 +
        response = TestResponse(
227 +
            code=http.FORBIDDEN,
228 +
            content_type=JSON_ERROR_CONTENT_TYPE)
229 +
230 +
        with self.assertRaises(ServerError):
231 +
            yield JWSClient._check_response(response)
1628 232
233 +
    @defer.inlineCallbacks
1629 234
    def test_check_valid_error(self):
1630 235
        """
1631 236
        If an error response is received but cannot be parsed,
1632 237
        :exc:`~acme.errors.ClientError` is raised.
1633 238
        """
1634 -
        self.assertThat(
1635 -
            JWSClient._check_response(
1636 -
                TestResponse(
1637 -
                    code=http.FORBIDDEN,
1638 -
                    content_type=JSON_ERROR_CONTENT_TYPE,
1639 -
                    json=lambda: succeed({
1640 -
                        u'type': u'unauthorized',
1641 -
                        u'detail': u'blah blah blah'}))),
1642 -
            failed_with(
1643 -
                MatchesAll(
1644 -
                    IsInstance(ServerError),
1645 -
                    AfterPreprocessing(repr, StartsWith('ServerError')))))
1646 -
1647 -
    def test_check_expected_bad_json(self):
1648 -
        """
1649 -
        If a JSON response was expected, but could not be parsed,
1650 -
        :exc:`~acme.errors.ClientError` is raised.
1651 -
        """
1652 -
        self.assertThat(
1653 -
            JWSClient._check_response(
1654 -
                TestResponse(json=lambda: fail(ValueError()))),
1655 -
            failed_with(IsInstance(errors.ClientError)))
1656 -
1657 -
    def test_missing_nonce(self):
1658 -
        """
1659 -
        If the response from the server does not have a nonce,
1660 -
        :exc:`~acme.errors.MissingNonce` is raised.
1661 -
        """
1662 -
        client = JWSClient(None, None, None)
1663 -
        with ExpectedException(errors.MissingNonce):
1664 -
            client._add_nonce(TestResponse())
1665 -
1666 -
    def test_bad_nonce(self):
1667 -
        """
1668 -
        If the response from the server has an unparseable nonce,
1669 -
        :exc:`~acme.errors.BadNonce` is raised.
1670 -
        """
1671 -
        client = JWSClient(None, None, None)
1672 -
        with ExpectedException(errors.BadNonce):
1673 -
            client._add_nonce(TestResponse(nonce=b'a!_'))
1674 -
1675 -
    def test_already_nonce(self):
1676 -
        """
1677 -
        No request is made if we already have a nonce.
1678 -
        """
1679 -
        client = JWSClient(None, None, None)
1680 -
        client._nonces.add(u'nonce')
1681 -
        self.assertThat(client._get_nonce(b''), succeeded(Equals(u'nonce')))
1682 -
1683 -
1684 -
class ExtraCoverageTests(TestCase):
1685 -
    """
1686 -
    Tests to get coverage on some test helpers that we don't really want to
1687 -
    maintain ourselves.
1688 -
    """
1689 -
    def test_always_never(self):
1690 -
        self.assertThat(Always(), AfterPreprocessing(str, Equals('Always()')))
1691 -
        self.assertThat(Never(), AfterPreprocessing(str, Equals('Never()')))
1692 -
        self.assertThat(None, Not(Never()))
1693 -
        self.assertThat(
1694 -
            Nearly(1.0, 2.0),
1695 -
            AfterPreprocessing(str, Equals('Nearly(1.0, 2.0)')))
1696 -
        self.assertThat(2.0, Not(Nearly(1.0)))
1697 -
1698 -
    def test_unexpected_number_of_request_causes_failure(self):
1699 -
        """
1700 -
        If there are no more expected requests, making a request causes a
1701 -
        failure.
1702 -
        """
1703 -
        async_failures = []
1704 -
        sequence = RequestSequence(
1705 -
            [],
1706 -
            async_failure_reporter=lambda *a: async_failures.append(a))
1707 -
        client = HTTPClient(
1708 -
            agent=RequestTraversalAgent(
1709 -
                StringStubbingResource(sequence)),
1710 -
            data_to_body_producer=_SynchronousProducer)
1711 -
        d = client.get('https://anything', data=b'what', headers={b'1': b'1'})
1712 -
        self.assertThat(
1713 -
            d,
1714 -
            succeeded(MatchesStructure(code=Equals(500))))
1715 -
        self.assertEqual(1, len(async_failures))
1716 -
        self.assertIn("No more requests expected, but request",
1717 -
                      async_failures[0][2])
1718 -
1719 -
        # the expected requests have all been made
1720 -
        self.assertTrue(sequence.consumed())
1721 -
1722 -
    def test_consume_context_manager_fails_on_remaining_requests(self):
1723 -
        """
1724 -
        If the ``consume`` context manager is used, if there are any remaining
1725 -
        expecting requests, the test case will be failed.
1726 -
        """
1727 -
        sequence = RequestSequence(
1728 -
            [(Always(), (418, {}, b'body'))] * 2,
1729 -
            async_failure_reporter=self.assertThat)
1730 -
        client = HTTPClient(
1731 -
            agent=RequestTraversalAgent(
1732 -
                StringStubbingResource(sequence)),
1733 -
            data_to_body_producer=_SynchronousProducer)
1734 -
1735 -
        consume_failures = []
1736 -
        with sequence.consume(sync_failure_reporter=consume_failures.append):
1737 -
            self.assertThat(
1738 -
                client.get('https://anything', data=b'what',
1739 -
                           headers={b'1': b'1'}),
1740 -
                succeeded(Always()))
239 +
        response = TestResponse(
240 +
            code=http.FORBIDDEN,
241 +
            content_type=JSON_ERROR_CONTENT_TYPE,
242 +
            json=lambda: succeed({
243 +
                u'type': u'unauthorized',
244 +
                u'detail': u'blah blah blah'}))
1741 245
1742 -
        self.assertEqual(1, len(consume_failures))
1743 -
        self.assertIn(
1744 -
            "Not all expected requests were made.  Still expecting:",
1745 -
            consume_failures[0])
246 +
        with self.assertRaises(ServerError):
247 +
            yield JWSClient._check_response(response)
1746 248
1747 249
1748 250
class LinkParsingTests(TestCase):
@@ -1757,18 +259,18 @@
Loading
1757 259
        """
1758 260
        The first example from the RFC.
1759 261
        """
1760 -
        self.assertThat(
1761 -
            _parse_header_links(
1762 -
                TestResponse(
1763 -
                    links=[b'<http://example.com/TheBook/chapter2>; '
1764 -
                           b'rel="previous"; '
1765 -
                           b'title="previous chapter"'])),
1766 -
            Equals({
1767 -
                u'previous':
1768 -
                {u'rel': u'previous',
1769 -
                 u'title': u'previous chapter',
1770 -
                 u'url': u'http://example.com/TheBook/chapter2'}
1771 -
            }))
262 +
        response = TestResponse(links=[
263 +
            b'<http://example.com/TheBook/chapter2>; '
264 +
           b'rel="previous"; '
265 +
           b'title="previous chapter"'])
266 +
        result = _parse_header_links(response)
267 +
        self.assertEqual({
268 +
            u'previous':
269 +
            {u'rel': u'previous',
270 +
             u'title': u'previous chapter',
271 +
             u'url': u'http://example.com/TheBook/chapter2'}
272 +
            },
273 +
            result)
1772 274
1773 275
1774 276
__all__ = ['ClientTests', 'ExtraCoverageTests', 'LinkParsingTests']

@@ -2,11 +2,11 @@
Loading
2 2
3 3
4 4
LETSENCRYPT_DIRECTORY = URL.fromText(
5 -
    u'https://acme-v01.api.letsencrypt.org/directory')
5 +
    u'https://acme-v02.api.letsencrypt.org/directory')
6 6
7 7
8 8
LETSENCRYPT_STAGING_DIRECTORY = URL.fromText(
9 -
    u'https://acme-staging.api.letsencrypt.org/directory')
9 +
    u'https://acme-staging-v02.api.letsencrypt.org/directory')
10 10
11 11
12 12
__all__ = ['LETSENCRYPT_DIRECTORY', 'LETSENCRYPT_STAGING_DIRECTORY']

@@ -1,20 +1,101 @@
Loading
1 1
"""
2 2
ACME client API (like :mod:`acme.client`) implementation for Twisted.
3 +
4 +
Extracted from RFC 8555
5 +
6 +
                              directory
7 +
                                  |
8 +
                                  +--> newNonce
9 +
                                  |
10 +
      +----------+----------+-----+-----+------------+
11 +
      |          |          |           |            |
12 +
      |          |          |           |            |
13 +
      V          V          V           V            V
14 +
 newAccount   newAuthz   newOrder   revokeCert   keyChange
15 +
      |          |          |
16 +
      |          |          |
17 +
      V          |          V
18 +
   account       |        order --+--> finalize
19 +
                 |          |     |
20 +
                 |          |     +--> cert
21 +
                 |          V
22 +
                 +---> authorization
23 +
                           | ^
24 +
                           | | "up"
25 +
                           V |
26 +
                         challenge
27 +
28 +
                 ACME Resources and Relationships
29 +
30 +
   The following table illustrates a typical sequence of requests
31 +
   required to establish a new account with the server, prove control of
32 +
   an identifier, issue a certificate, and fetch an updated certificate
33 +
   some time after issuance.  The "->" is a mnemonic for a Location
34 +
   header field pointing to a created resource.
35 +
36 +
   +-------------------+--------------------------------+--------------+
37 +
   | Action            | Request                        | Response     |
38 +
   +-------------------+--------------------------------+--------------+
39 +
   |1.Get directory     | GET  directory                 | 200          |
40 +
   |                   |                                |              |
41 +
   |2.Get nonce         | HEAD newNonce                  | 200          |
42 +
   |                   |                                |              |
43 +
   |3.Create account    | POST newAccount                | 201 ->       |
44 +
   |                   |                                | account      |
45 +
   |                   |                                |              |
46 +
   |4.Submit order      | POST newOrder                  | 201 -> order |
47 +
   |                   |                                |              |
48 +
   |5.Fetch challenges  | POST-as-GET order's            | 200          |
49 +
   |                   | authorization urls             |              |
50 +
   |                   |                                |              |
51 +
   |6.Respond to        | POST authorization challenge   | 200          |
52 +
   | challenges        | urls                           |              |
53 +
   |                   |                                |              |
54 +
   |7.Poll for status   | POST-as-GET order              | 200          |
55 +
   |                   |                                |              |
56 +
   |8.Finalize order    | POST order's finalize url      | 200          |
57 +
   |                   |                                |              |
58 +
   |9.Poll for status   | POST-as-GET order              | 200          |
59 +
   |                   |                                |              |
60 +
   |10.Download          | POST-as-GET order's            | 200          |
61 +
   | certificate       | certificate url                |              |
62 +
   +-------------------+--------------------------------+--------------+
63 +
64 +
1. client = Client.from_url(DIRECTORY_URL)
65 +
2. done as part of Client.from_url() call and automatically for each request
66 +
3. client.start() - creates or updates an account.
67 +
4. order = client.submit_order(new_cert_key, [list,domains])
68 +
5. list(order.authorizations) - fetch done as part of client.submit_order()
69 +
6. client.check_authoriztion(order.authorizations[0]) and for each
70 +
   authorization
71 +
7. poll as part of answer_challenge
72 +
8. client.finalize(order)
73 +
9. client.check_order(order)
74 +
10.
75 +
3 76
"""
4 77
import re
5 -
import time
6 78
7 79
from acme import errors, messages
80 +
from acme.crypto_util import make_csr
8 81
from acme.jws import JWS, Header
9 -
from acme.messages import STATUS_PENDING, STATUS_PROCESSING, STATUS_VALID
82 +
from acme.messages import (
83 +
    STATUS_PENDING,
84 +
    STATUS_VALID,
85 +
    STATUS_INVALID,
86 +
    )
10 87
88 +
import josepy as jose
11 89
from josepy.jwa import RS256
12 90
from josepy.errors import DeserializationError
13 91
92 +
import OpenSSL
93 +
from cryptography.hazmat.primitives import serialization
94 +
14 95
from eliot.twisted import DeferredContext
15 96
from treq import json_content
16 97
from treq.client import HTTPClient
17 -
from twisted.internet.defer import maybeDeferred, succeed
98 +
from twisted.internet import defer
18 99
from twisted.internet.task import deferLater
19 100
from twisted.web import http
20 101
from twisted.web.client import Agent, HTTPConnectionPool
@@ -22,21 +103,25 @@
Loading
22 103
23 104
from txacme import __version__
24 105
from txacme.logging import (
25 -
    LOG_ACME_ANSWER_CHALLENGE, LOG_ACME_CONSUME_DIRECTORY,
26 -
    LOG_ACME_CREATE_AUTHORIZATION, LOG_ACME_FETCH_CHAIN,
27 -
    LOG_ACME_POLL_AUTHORIZATION, LOG_ACME_REGISTER,
28 -
    LOG_ACME_REQUEST_CERTIFICATE, LOG_ACME_UPDATE_REGISTRATION,
29 -
    LOG_HTTP_PARSE_LINKS, LOG_JWS_ADD_NONCE, LOG_JWS_CHECK_RESPONSE,
30 -
    LOG_JWS_GET, LOG_JWS_GET_NONCE, LOG_JWS_HEAD, LOG_JWS_POST,
31 -
    LOG_JWS_REQUEST, LOG_JWS_SIGN)
106 +
    LOG_ACME_ANSWER_CHALLENGE,
107 +
    LOG_ACME_CONSUME_DIRECTORY,
108 +
    LOG_ACME_REGISTER,
109 +
    LOG_HTTP_PARSE_LINKS,
110 +
    LOG_JWS_ADD_NONCE,
111 +
    LOG_JWS_CHECK_RESPONSE,
112 +
    LOG_JWS_GET,
113 +
    LOG_JWS_GET_NONCE,
114 +
    LOG_JWS_HEAD,
115 +
    LOG_JWS_POST,
116 +
    LOG_JWS_REQUEST,
117 +
    LOG_JWS_SIGN,
118 +
    )
32 119
from txacme.util import check_directory_url_type, tap
33 120
34 -
35 121
_DEFAULT_TIMEOUT = 40
36 122
37 123
38 124
# Borrowed from requests, with modifications.
39 -
40 125
def _parse_header_links(response):
41 126
    """
42 127
    Parse the links from a Link: header field.
@@ -72,15 +157,22 @@
Loading
72 157
        return links
73 158
74 159
75 -
def _default_client(jws_client, reactor, key, alg):
160 +
def _default_client(jws_client, reactor, key, alg, directory, timeout):
76 161
    """
77 162
    Make a client if we didn't get one.
78 163
    """
79 164
    if jws_client is None:
80 165
        pool = HTTPConnectionPool(reactor)
81 166
        agent = Agent(reactor, pool=pool)
82 -
        jws_client = JWSClient(agent, key, alg)
83 -
    return jws_client
167 +
        jws_d = JWSClient.from_directory(agent, key, alg, directory)
168 +
    else:
169 +
        jws_d = defer.succeed(jws_client)
170 +
171 +
    def set_timeout(jws_client):
172 +
        jws_client.timeout = timeout
173 +
        return jws_client
174 +
175 +
    return jws_d.addCallback(set_timeout)
84 176
85 177
86 178
def fqdn_identifier(fqdn):
@@ -98,21 +190,42 @@
Loading
98 190
        typ=messages.IDENTIFIER_FQDN, value=fqdn)
99 191
100 192
193 +
@messages.Directory.register
194 +
class Finalize(jose.JSONObjectWithFields):
195 +
    """
196 +
    ACME order finalize request.
197 +
198 +
    This is here as acme.messages.CertificateRequest does not work with
199 +
    pebble in --strict mode.
200 +
201 +
    :ivar josepy.util.ComparableX509 csr:
202 +
        `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
203 +
    """
204 +
    resource_type = 'finalize'
205 +
    csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
206 +
207 +
101 208
class Client(object):
102 209
    """
103 210
    ACME client interface.
211 +
212 +
    The current implementation does not support multiple parallel requests.
213 +
    This is due to the nonce handling.
214 +
215 +
    Should be initialized with 'Client.from_url'.
104 216
    """
105 217
    def __init__(self, directory, reactor, key, jws_client):
106 218
        self._client = jws_client
107 219
        self._clock = reactor
108 220
        self.directory = directory
109 221
        self.key = key
222 +
        self._kid = None
110 223
111 224
    @classmethod
112 225
    def from_url(
113 226
        cls, reactor, url, key, alg=RS256,
114 227
        jws_client=None, timeout=_DEFAULT_TIMEOUT,
115 -
            ):
228 +
    ):
116 229
        """
117 230
        Construct a client from an ACME directory at a given URL.
118 231
@@ -137,16 +250,17 @@
Loading
137 250
            url=url, key_type=key.typ, alg=alg.name)
138 251
        with action.context():
139 252
            check_directory_url_type(url)
140 -
            jws_client = _default_client(jws_client, reactor, key, alg)
141 -
            jws_client.timeout = timeout
253 +
            directory = url.asText()
142 254
            return (
143 -
                DeferredContext(jws_client.get(url.asText()))
144 -
                .addCallback(json_content)
145 -
                .addCallback(messages.Directory.from_json)
255 +
                DeferredContext(jws_client=_default_client(
256 +
                    jws_client, reactor, key, alg, directory, timeout
257 +
                ))
146 258
                .addCallback(
147 -
                    tap(lambda d: action.add_success_fields(directory=d)))
148 -
                .addCallback(cls, reactor, key, jws_client)
149 -
                .addActionFinish())
259 +
                    tap(lambda jws_client:
260 +
                        action.add_success_fields(directory=directory)))
261 +
                .addCallback(lambda jws_client: cls(reactor, key, jws_client))
262 +
                .addActionFinish()
263 +
            )
150 264
151 265
    def stop(self):
152 266
        """
@@ -158,29 +272,45 @@
Loading
158 272
        """
159 273
        return self._client.stop()
160 274
161 -
    def register(self, new_reg=None):
275 +
    def register(self, email=None):
162 276
        """
163 -
        Create a new registration with the ACME server.
277 +
        Create a new registration with the ACME server or update
278 +
        an existing account.
279 +
280 +
        It should be called before doing any ACME requests.
164 281
165 -
        :param ~acme.messages.NewRegistration new_reg: The registration message
166 -
            to use, or ``None`` to construct one.
282 +
        :param str: Comma separated contact emails used by the account.
167 283
168 284
        :return: The registration resource.
169 285
        :rtype: Deferred[`~acme.messages.RegistrationResource`]
170 286
        """
171 -
        if new_reg is None:
172 -
            new_reg = messages.NewRegistration()
287 +
        uri = self.directory.newAccount
288 +
        new_reg = messages.Registration.from_data(
289 +
            email=email,
290 +
            terms_of_service_agreed=True,
291 +
            )
173 292
        action = LOG_ACME_REGISTER(registration=new_reg)
174 293
        with action.context():
175 294
            return (
176 295
                DeferredContext(
177 -
                    self.update_registration(
178 -
                        new_reg, uri=self.directory[new_reg]))
179 -
                .addErrback(self._maybe_registered, new_reg)
296 +
                    self._client.post(uri, new_reg))
297 +
                .addCallback(self._cb_check_existing_account, new_reg)
298 +
                .addCallback(self._cb_check_registration)
180 299
                .addCallback(
181 300
                    tap(lambda r: action.add_success_fields(registration=r)))
182 301
                .addActionFinish())
183 302
303 +
    def stop(self):
304 +
        """
305 +
        Stops the client operation.
306 +
307 +
        This cancels pending operations and does cleanup.
308 +
309 +
        :return: When operation is done.
310 +
        :rtype: Deferred[None]
311 +
        """
312 +
        return self._client.stop()
313 +
184 314
    @classmethod
185 315
    def _maybe_location(cls, response, uri=None):
186 316
        """
@@ -191,167 +321,129 @@
Loading
191 321
            return location.decode('ascii')
192 322
        return uri
193 323
194 -
    def _maybe_registered(self, failure, new_reg):
324 +
    def _cb_check_existing_account(self, response, request):
195 325
        """
196 -
        If the registration already exists, we should just load it.
326 +
        Get the response from the account registration and see if the
327 +
        account is already registered and do an update in that case.
197 328
        """
198 -
        failure.trap(ServerError)
199 -
        response = failure.value.response
200 -
        if response.code == http.CONFLICT:
201 -
            reg = new_reg.update(
202 -
                resource=messages.UpdateRegistration.resource_type)
329 +
        if response.code == 200 and request.contact:
330 +
            # Account already exists and we email address to update.
331 +
            # I don't know how to remove a contact.
203 332
            uri = self._maybe_location(response)
204 -
            return self.update_registration(reg, uri=uri)
205 -
        return failure
333 +
            deferred = self._client.post(uri, request, kid=uri)
334 +
            deferred.addCallback(self._cb_parse_registration_response, uri=uri)
335 +
            return deferred
206 336
207 -
    def agree_to_tos(self, regr):
208 -
        """
209 -
        Accept the terms-of-service for a registration.
210 -
211 -
        :param ~acme.messages.RegistrationResource regr: The registration to
212 -
            update.
213 -
214 -
        :return: The updated registration resource.
215 -
        :rtype: Deferred[`~acme.messages.RegistrationResource`]
216 -
        """
217 -
        return self.update_registration(
218 -
            regr.update(
219 -
                body=regr.body.update(
220 -
                    agreement=regr.terms_of_service)))
221 -
222 -
    def update_registration(self, regr, uri=None):
223 -
        """
224 -
        Submit a registration to the server to update it.
225 -
226 -
        :param ~acme.messages.RegistrationResource regr: The registration to
227 -
            update.  Can be a :class:`~acme.messages.NewRegistration` instead,
228 -
            in order to create a new registration.
229 -
        :param str uri: The url to submit to.  Must be
230 -
            specified if a :class:`~acme.messages.NewRegistration` is provided.
231 -
232 -
        :return: The updated registration resource.
233 -
        :rtype: Deferred[`~acme.messages.RegistrationResource`]
234 -
        """
235 -
        if uri is None:
236 -
            uri = regr.uri
237 -
        if isinstance(regr, messages.RegistrationResource):
238 -
            message = messages.UpdateRegistration(**dict(regr.body))
239 -
        else:
240 -
            message = regr
241 -
        action = LOG_ACME_UPDATE_REGISTRATION(uri=uri, registration=message)
242 -
        with action.context():
243 -
            return (
244 -
                DeferredContext(self._client.post(uri, message))
245 -
                .addCallback(self._parse_regr_response, uri=uri)
246 -
                .addCallback(self._check_regr, regr)
247 -
                .addCallback(
248 -
                    tap(lambda r: action.add_success_fields(registration=r)))
249 -
                .addActionFinish())
337 +
        return self._cb_parse_registration_response(response)
250 338
251 -
    def _parse_regr_response(self, response, uri=None, new_authzr_uri=None,
252 -
                             terms_of_service=None):
339 +
    def _cb_parse_registration_response(self, response, uri=None):
253 340
        """
254 -
        Parse a registration response from the server.
341 +
        Parse a new or update registration response from the server.
255 342
        """
256 343
        links = _parse_header_links(response)
344 +
        terms_of_service = None
257 345
        if u'terms-of-service' in links:
258 346
            terms_of_service = links[u'terms-of-service'][u'url']
259 -
        if u'next' in links:
260 -
            new_authzr_uri = links[u'next'][u'url']
261 -
        if new_authzr_uri is None:
262 -
            raise errors.ClientError('"next" link missing')
263 347
        return (
264 348
            response.json()
265 349
            .addCallback(
266 350
                lambda body:
267 351
                messages.RegistrationResource(
268 352
                    body=messages.Registration.from_json(body),
269 -
                    uri=self._maybe_location(response, uri=uri),
270 -
                    new_authzr_uri=new_authzr_uri,
353 +
                    uri=self._maybe_location(response, uri),
271 354
                    terms_of_service=terms_of_service))
272 355
            )
273 356
274 -
    def _check_regr(self, regr, new_reg):
357 +
    def _cb_check_registration(self, regr):
275 358
        """
276 359
        Check that a registration response contains the registration we were
277 360
        expecting.
278 361
        """
279 -
        body = getattr(new_reg, 'body', new_reg)
280 -
        for k, v in body.items():
281 -
            if k == 'resource' or not v:
282 -
                continue
283 -
            if regr.body[k] != v:
284 -
                raise errors.UnexpectedUpdate(regr)
285 362
        if regr.body.key != self.key.public_key():
363 +
            # This is a response for another key.
286 364
            raise errors.UnexpectedUpdate(regr)
287 -
        return regr
288 365
289 -
    def request_challenges(self, identifier):
290 -
        """
291 -
        Create a new authorization.
366 +
        if regr.body.status != 'valid':
367 +
            raise errors.UnexpectedUpdate(regr)
292 368
293 -
        :param ~acme.messages.Identifier identifier: The identifier to
294 -
            authorize.
369 +
        self._client.kid = regr.uri
295 370
296 -
        :return: The new authorization resource.
297 -
        :rtype: Deferred[`~acme.messages.AuthorizationResource`]
298 -
        """
299 -
        action = LOG_ACME_CREATE_AUTHORIZATION(identifier=identifier)
300 -
        with action.context():
301 -
            message = messages.NewAuthorization(identifier=identifier)
302 -
            return (
303 -
                DeferredContext(
304 -
                    self._client.post(self.directory[message], message))
305 -
                .addCallback(self._expect_response, http.CREATED)
306 -
                .addCallback(self._parse_authorization)
307 -
                .addCallback(self._check_authorization, identifier)
308 -
                .addCallback(
309 -
                    tap(lambda a: action.add_success_fields(authorization=a)))
310 -
                .addActionFinish())
371 +
        return regr
311 372
312 -
    @classmethod
313 -
    def _expect_response(cls, response, code):
314 -
        """
315 -
        Ensure we got the expected response code.
373 +
    @defer.inlineCallbacks
374 +
    def submit_order(self, key, names):
316 375
        """
317 -
        if response.code != code:
318 -
            raise errors.ClientError(
319 -
                'Expected {!r} response but got {!r}'.format(
320 -
                    code, response.code))
321 -
        return response
376 +
        Create a new order and return the OrderResource for that order with
377 +
        all the authorizations resolved.
322 378
323 -
    @classmethod
324 -
    def _parse_authorization(cls, response, uri=None):
325 -
        """
326 -
        Parse an authorization resource.
379 +
        It will automatically create a new private key and CSR for the
380 +
        domain 'names'.
381 +
382 +
        :param key: Key for the future certificate.
383 +
        :param list of str names: Sequence of DNS names for which to request
384 +
            a new certificate.
385 +
386 +
        :return: The new authorization resource.
387 +
        :rtype: Deferred[`~acme.messages.Order`]
327 388
        """
328 -
        links = _parse_header_links(response)
329 -
        try:
330 -
            new_cert_uri = links[u'next'][u'url']
331 -
        except KeyError:
332 -
            raise errors.ClientError('"next" link missing')
333 -
        return (
334 -
            response.json()
335 -
            .addCallback(
336 -
                lambda body: messages.AuthorizationResource(
389 +
        # certbot helper API needs PEM.
390 +
        pem_key = key.private_bytes(
391 +
            encoding=serialization.Encoding.PEM,
392 +
            format=serialization.PrivateFormat.PKCS8,
393 +
            encryption_algorithm=serialization.NoEncryption(),
394 +
            )
395 +
        csr_pem = make_csr(pem_key, names)
396 +
        identifiers = [fqdn_identifier(name) for name in names]
397 +
398 +
        message = messages.NewOrder(identifiers=identifiers)
399 +
        response = yield self._client.post(self.directory.newOrder, message)
400 +
        self._expect_response(response, [http.CREATED])
401 +
402 +
        order_uri = self._maybe_location(response)
403 +
404 +
        authorizations = []
405 +
        order_body = yield response.json()
406 +
        for uri in order_body['authorizations']:
407 +
            # We do a POST-as-GET
408 +
            respose = yield self._client.post(uri, obj=None)
409 +
            self._expect_response(response, [http.CREATED])
410 +
            body = yield respose.json()
411 +
            authorizations.append(
412 +
                messages.AuthorizationResource(
337 413
                    body=messages.Authorization.from_json(body),
338 -
                    uri=cls._maybe_location(response, uri=uri),
339 -
                    new_cert_uri=new_cert_uri))
414 +
                    uri=uri,
415 +
                    ))
416 +
417 +
        order = messages.OrderResource(
418 +
            body=messages.Order.from_json(order_body),
419 +
            uri=order_uri,
420 +
            authorizations=authorizations,
421 +
            csr_pem=csr_pem,
340 422
            )
341 423
424 +
        # TODO: Not sure if all these sanity checks are required.
425 +
        for identifier in order.body.identifiers:
426 +
            if identifier not in identifiers:
427 +
                raise errors.UnexpectedUpdate(order)
428 +
        defer.returnValue(order)
429 +
342 430
    @classmethod
343 -
    def _check_authorization(cls, authzr, identifier):
431 +
    def _expect_response(cls, response, codes):
344 432
        """
345 -
        Check that the authorization we got is the one we expected.
433 +
        Ensure we got one of the expected response codes`.
346 434
        """
347 -
        if authzr.body.identifier != identifier:
348 -
            raise errors.UnexpectedUpdate(authzr)
349 -
        return authzr
435 +
        if response.code not in codes:
436 +
            return _fail_and_consume(response, errors.ClientError(
437 +
                'Expected {!r} response but got {!r}'.format(
438 +
                    codes, response.code)))
439 +
        return response
350 440
351 441
    def answer_challenge(self, challenge_body, response):
352 442
        """
353 443
        Respond to an authorization challenge.
354 444
445 +
        This send a POST with the empty object '{}' as the payload.
446 +
355 447
        :param ~acme.messages.ChallengeBody challenge_body: The challenge being
356 448
            responded to.
357 449
        :param ~acme.challenges.ChallengeResponse response: The response to the
@@ -362,10 +454,16 @@
Loading
362 454
        """
363 455
        action = LOG_ACME_ANSWER_CHALLENGE(
364 456
            challenge_body=challenge_body, response=response)
457 +
458 +
        if challenge_body.status != STATUS_PENDING:
459 +
            # We already have an answer.
460 +
            return challenge_body
461 +
365 462
        with action.context():
366 463
            return (
367 464
                DeferredContext(
368 -
                    self._client.post(challenge_body.uri, response))
465 +
                    self._client.post(
466 +
                        challenge_body.uri, jose.JSONObjectWithFields()))
369 467
                .addCallback(self._parse_challenge)
370 468
                .addCallback(self._check_challenge, challenge_body)
371 469
                .addCallback(
@@ -374,6 +472,7 @@
Loading
374 472
                .addActionFinish())
375 473
376 474
    @classmethod
475 +
    @defer.inlineCallbacks
377 476
    def _parse_challenge(cls, response):
378 477
        """
379 478
        Parse a challenge resource.
@@ -382,14 +481,14 @@
Loading
382 481
        try:
383 482
            authzr_uri = links['up']['url']
384 483
        except KeyError:
385 -
            raise errors.ClientError('"up" link missing')
386 -
        return (
387 -
            response.json()
388 -
            .addCallback(
389 -
                lambda body: messages.ChallengeResource(
390 -
                    authzr_uri=authzr_uri,
391 -
                    body=messages.ChallengeBody.from_json(body)))
392 -
            )
484 +
            yield _fail_and_consume(
485 +
                response, errors.ClientError('"up" link missing'))
486 +
487 +
        body = yield response.json()
488 +
        defer.returnValue(messages.ChallengeResource(
489 +
            authzr_uri=authzr_uri,
490 +
            body=messages.ChallengeBody.from_json(body),
491 +
            ))
393 492
394 493
    @classmethod
395 494
    def _check_challenge(cls, challenge, challenge_body):
@@ -400,76 +499,64 @@
Loading
400 499
            raise errors.UnexpectedUpdate(challenge.uri)
401 500
        return challenge
402 501
403 -
    def poll(self, authzr):
502 +
    def check_authorization(self, authzz):
404 503
        """
405 -
        Update an authorization from the server (usually to check its status).
504 +
        Check the status of the authorization.
505 +
506 +
        Return an updated message.AuthorizationResource.
406 507
        """
407 -
        action = LOG_ACME_POLL_AUTHORIZATION(authorization=authzr)
408 -
        with action.context():
409 -
            return (
410 -
                DeferredContext(self._client.get(authzr.uri))
411 -
                # Spec says we should get 202 while pending, Boulder actually
412 -
                # sends us 200 always, so just don't check.
413 -
                # .addCallback(self._expect_response, http.ACCEPTED)
414 -
                .addCallback(
415 -
                    lambda res:
416 -
                    self._parse_authorization(res, uri=authzr.uri)
417 -
                    .addCallback(
418 -
                        self._check_authorization, authzr.body.identifier)
419 -
                    .addCallback(
420 -
                        lambda authzr:
421 -
                        (authzr,
422 -
                         self.retry_after(res, _now=self._clock.seconds)))
423 -
                )
424 -
                .addCallback(tap(
425 -
                    lambda a_r: action.add_success_fields(
426 -
                        authorization=a_r[0], retry_after=a_r[1])))
427 -
                .addActionFinish())
508 +
        return self._poll(
509 +
            authzz.uri, messages.AuthorizationResource, messages.Authorization
510 +
        )
428 511
429 -
    @classmethod
430 -
    def retry_after(cls, response, default=5, _now=time.time):
512 +
    def check_order(self, orderr):
431 513
        """
432 -
        Parse the Retry-After value from a response.
514 +
        Check the status of the authorization.
515 +
516 +
        Return an updated message.OrderResource.
433 517
        """
434 -
        val = response.headers.getRawHeaders(b'retry-after', [default])[0]
435 -
        try:
436 -
            return int(val)
437 -
        except ValueError:
438 -
            return http.stringToDatetime(val) - _now()
518 +
        return self._poll(orderr.uri, messages.OrderResource, messages.Order)
439 519
440 -
    def request_issuance(self, csr):
520 +
    @defer.inlineCallbacks
521 +
    def _poll(self, url, resource_class, body_class,):
441 522
        """
442 -
        Request a certificate.
523 +
        Make a POST-as-GET for a resource.
524 +
        """
525 +
        response = yield self._client.post(url, obj=None)
526 +
        self._expect_response(response, [http.OK])
527 +
        body = yield response.json()
528 +
        defer.returnValue(resource_class(
529 +
            uri=url,
530 +
            body=body_class.from_json(body),
531 +
            ))
532 +
533 +
    @defer.inlineCallbacks
534 +
    def finalize(self, order):
535 +
        """
536 +
        Request order finalization.
443 537
444 538
        Authorizations should have already been completed for all of the names
445 -
        requested in the CSR.
446 -
447 -
        Note that unlike `acme.client.Client.request_issuance`, the certificate
448 -
        resource will have the body data as raw bytes.
449 -
450 -
        ..  seealso:: `txacme.util.csr_for_names`
451 -
452 -
        ..  todo:: Delayed issuance is not currently supported, the server must
453 -
                   issue the requested certificate immediately.
539 +
        requested in the order.
454 540
455 -
        :param csr: A certificate request message: normally
456 -
            `txacme.messages.CertificateRequest` or
457 -
            `acme.messages.CertificateRequest`.
541 +
        :param ~acme.messages.Order order: The order for which the certificate
542 +
            is requested.
458 543
459 -
        :rtype: Deferred[`acme.messages.CertificateResource`]
544 +
        :rtype: Deferred[`acme.messages.OrderResource`]
460 545
        :return: The issued certificate.
461 546
        """
462 -
        action = LOG_ACME_REQUEST_CERTIFICATE()
463 -
        with action.context():
464 -
            return (
465 -
                DeferredContext(
466 -
                    self._client.post(
467 -
                        self.directory[csr], csr,
468 -
                        content_type=DER_CONTENT_TYPE,
469 -
                        headers=Headers({b'Accept': [DER_CONTENT_TYPE]})))
470 -
                .addCallback(self._expect_response, http.CREATED)
471 -
                .addCallback(self._parse_certificate)
472 -
                .addActionFinish())
547 +
        csr = OpenSSL.crypto.load_certificate_request(
548 +
            OpenSSL.crypto.FILETYPE_PEM, order.csr_pem
549 +
        )
550 +
        request = Finalize(csr=jose.ComparableX509(csr))
551 +
        response = yield self._client.post(
552 +
            order.body.finalize, obj=request
553 +
        )
554 +
        self._expect_response(response, [http.OK])
555 +
        body = yield response.json()
556 +
        defer.returnValue(messages.OrderResource(
557 +
            uri=order.uri,
558 +
            body=messages.Order.from_json(body),
559 +
        ))
473 560
474 561
    @classmethod
475 562
    def _parse_certificate(cls, response):
@@ -490,37 +577,24 @@
Loading
490 577
                    body=body))
491 578
            )
492 579
493 -
    def fetch_chain(self, certr, max_length=10):
580 +
    @defer.inlineCallbacks
581 +
    def fetch_certificate(self, url):
494 582
        """
495 -
        Fetch the intermediary chain for a certificate.
496 -
497 -
        :param acme.messages.CertificateResource certr: The certificate to
498 -
            fetch the chain for.
499 -
        :param int max_length: The maximum length of the chain that will be
500 -
            fetched.
583 +
        Download the certificate for `order`.
501 584
502 -
        :rtype: Deferred[List[`acme.messages.CertificateResource`]]
503 -
        :return: The issuer certificate chain, ordered with the trust anchor
504 -
                 last.
585 +
        :rtype: acme.messages.CertificateResource
586 +
        :return: The certificate which was downloaded.
505 587
        """
506 -
        action = LOG_ACME_FETCH_CHAIN()
507 -
        with action.context():
508 -
            if certr.cert_chain_uri is None:
509 -
                return succeed([])
510 -
            elif max_length < 1:
511 -
                raise errors.ClientError('chain too long')
512 -
            return (
513 -
                DeferredContext(
514 -
                    self._client.get(
515 -
                        certr.cert_chain_uri,
516 -
                        content_type=DER_CONTENT_TYPE,
517 -
                        headers=Headers({b'Accept': [DER_CONTENT_TYPE]})))
518 -
                .addCallback(self._parse_certificate)
519 -
                .addCallback(
520 -
                    lambda issuer:
521 -
                    self.fetch_chain(issuer, max_length=max_length - 1)
522 -
                    .addCallback(lambda chain: [issuer] + chain))
523 -
                .addActionFinish())
588 +
        deferred = self._client.post(
589 +
            url,
590 +
            content_type=PEM_CHAIN_TYPE,
591 +
            response_type=PEM_CHAIN_TYPE,
592 +
            obj=None,
593 +
            )
594 +
        deferred.addCallback(self._parse_certificate)
595 +
596 +
        result = yield deferred
597 +
        defer.returnValue(result)
524 598
525 599
526 600
def _find_supported_challenge(authzr, responders):
@@ -528,8 +602,8 @@
Loading
528 602
    Find a challenge combination that consists of a single challenge that the
529 603
    responder can satisfy.
530 604
531 -
    :param ~acme.messages.AuthorizationResource auth: The authorization to
532 -
        examine.
605 +
    :param ~acme.messages.AuthorizationResource authzr:
606 +
        The authorization to examine.
533 607
534 608
    :type responder: List[`~txacme.interfaces.IResponder`]
535 609
    :param responder: The possible responders to use.
@@ -541,98 +615,161 @@
Loading
541 615
            `~acme.messages.ChallengeBody`]
542 616
    :return: The responder and challenge that were found.
543 617
    """
544 -
    matches = [
545 -
        (responder, challbs[0])
546 -
        for challbs in authzr.body.resolved_combinations
547 -
        for responder in responders
548 -
        if [challb.typ for challb in challbs] == [responder.challenge_type]]
549 -
    if len(matches) == 0:
550 -
        raise NoSupportedChallenges(authzr)
551 -
    else:
552 -
        return matches[0]
618 +
    for responder in responders:
619 +
        r_type = responder.challenge_type
620 +
        for challenge in authzr.body.challenges:
621 +
            if r_type == challenge.chall.typ:
622 +
                return (responder, challenge)
623 +
624 +
    raise NoSupportedChallenges(authzr)
553 625
554 626
555 -
def answer_challenge(authzr, client, responders):
627 +
@defer.inlineCallbacks
628 +
def answer_challenge(authz, client, responders, clock, timeout=300.0):
556 629
    """
557 630
    Complete an authorization using a responder.
558 631
559 -
    :param ~acme.messages.AuthorizationResource auth: The authorization to
560 -
        complete.
632 +
    It waits for the authorization to be completed (as valid or invliad)
633 +
    for a maximum of 'timeout' seconds.
634 +
635 +
                      pending --------------------+
636 +
                         |                        |
637 +
       Challenge failure |                        |
638 +
              or         |                        |
639 +
             Error       |  Challenge valid       |
640 +
               +---------+---------+              |
641 +
               |                   |              |
642 +
               V                   V              |
643 +
            invalid              valid            |
644 +
                                   |              |
645 +
                                   |              |
646 +
                                   |              |
647 +
                    +--------------+--------------+
648 +
                    |              |              |
649 +
                    |              |              |
650 +
             Server |       Client |   Time after |
651 +
             revoke |   deactivate |    "expires" |
652 +
                    V              V              V
653 +
                 revoked      deactivated      expired
654 +
655 +
    :param ~acme.messages.AuthorizationResource authz:
656 +
        The authorization answer the challenges for.
561 657
    :param .Client client: The ACME client.
562 658
563 659
    :type responders: List[`~txacme.interfaces.IResponder`]
564 660
    :param responders: A list of responders that can be used to complete the
565 661
        challenge with.
662 +
    :param clock: The ``IReactorTime`` implementation to use; usually the
663 +
        reactor, when not testing.
664 +
    :param float timeout: Maximum time to poll in seconds, before giving up.
665 +
666 +
    :raises AuthorizationFailed: If the challenge was not validated.
566 667
567 668
    :return: A deferred firing when the authorization is verified.
568 669
    """
569 -
    responder, challb = _find_supported_challenge(authzr, responders)
570 -
    response = challb.response(client.key)
571 -
572 -
    def _stop_responding():
573 -
        return maybeDeferred(
574 -
            responder.stop_responding,
575 -
            authzr.body.identifier.value,
576 -
            challb.chall,
577 -
            response)
578 -
    return (
579 -
        maybeDeferred(
580 -
            responder.start_responding,
581 -
            authzr.body.identifier.value,
582 -
            challb.chall,
583 -
            response)
584 -
        .addCallback(lambda _: client.answer_challenge(challb, response))
585 -
        .addCallback(lambda _: _stop_responding)
586 -
        )
670 +
    server_name = authz.body.identifier.value
671 +
    responder, challenge = _find_supported_challenge(authz, responders)
672 +
    response = challenge.response(client.key)
673 +
    yield defer.maybeDeferred(
674 +
        responder.start_responding, server_name, challenge.chall, response)
675 +
676 +
    resource = yield client.answer_challenge(challenge, response)
677 +
678 +
    now = clock.seconds()
679 +
    sleep = 0.5
680 +
    try:
681 +
        while True:
682 +
            resource = yield client.check_authorization(authz)
683 +
            status = resource.body.status
684 +
685 +
            if status == STATUS_INVALID:
686 +
                # No need to wait longer as we got a definitive answer.
687 +
                raise AuthorizationFailed(resource)
587 688
689 +
            if status == STATUS_VALID:
690 +
                # All good.
691 +
                defer.returnValue(resource)
588 692
589 -
def poll_until_valid(authzr, clock, client, timeout=300.0):
693 +
            if clock.seconds() - now > timeout:
694 +
                raise AuthorizationFailed(resource)
695 +
696 +
            yield deferLater(clock, sleep, lambda: None)
697 +
            sleep += sleep
698 +
    finally:
699 +
        yield defer.maybeDeferred(
700 +
            responder.stop_responding, server_name, challenge.chall, response)
701 +
702 +
703 +
@defer.inlineCallbacks
704 +
def get_certificate(orderr, client, clock, timeout=300.0):
590 705
    """
591 -
    Poll an authorization until it is in a state other than pending or
592 -
    processing.
706 +
    Finalize the order and return the associated certificate.
707 +
708 +
    It assumes all authorizations were already validated.
709 +
710 +
    It waits for the order to be 'valid' for a maximum of 'timeout' seconds.::
711 +
712 +
         pending --------------+
713 +
            |                  |
714 +
            | All authz        |
715 +
            | "valid"          |
716 +
            V                  |
717 +
          ready ---------------+
718 +
            |                  |
719 +
            | Receive          |
720 +
            | finalize         |
721 +
            | request          |
722 +
            V                  |
723 +
        processing ------------+
724 +
            |                  |
725 +
            | Certificate      | Error or
726 +
            | issued           | Authorization failure
727 +
            V                  V
728 +
          valid             invalid
729 +
730 +
    :param ~acme.messages.OrderResource orderr: The order to finalize.
731 +
    :param .Client client: The ACME client.
593 732
594 -
    :param ~acme.messages.AuthorizationResource auth: The authorization to
595 -
        complete.
596 733
    :param clock: The ``IReactorTime`` implementation to use; usually the
597 734
        reactor, when not testing.
598 -
    :param .Client client: The ACME client.
599 735
    :param float timeout: Maximum time to poll in seconds, before giving up.
600 736
601 -
    :raises txacme.client.AuthorizationFailed: if the authorization is no
602 -
        longer in the pending, processing, or valid states.
603 -
    :raises: ``twisted.internet.defer.CancelledError`` if the authorization was
604 -
        still in pending or processing state when the timeout was reached.
737 +
    :raises ServerError: If a certificate could not be retrieved.
605 738
606 -
    :rtype: Deferred[`~acme.messages.AuthorizationResource`]
607 -
    :return: A deferred firing when the authorization has completed/failed; if
608 -
             the authorization is valid, the authorization resource will be
609 -
             returned.
739 +
    :return: A deferred firing when the PEM certificate is retrieved.
610 740
    """
611 -
    def repoll(result):
612 -
        authzr, retry_after = result
613 -
        if authzr.body.status in {STATUS_PENDING, STATUS_PROCESSING}:
614 -
            return (
615 -
                deferLater(clock, retry_after, lambda: None)
616 -
                .addCallback(lambda _: client.poll(authzr))
617 -
                .addCallback(repoll)
618 -
                )
619 -
        if authzr.body.status != STATUS_VALID:
620 -
            raise AuthorizationFailed(authzr)
621 -
        return authzr
622 -
623 -
    def cancel_timeout(result):
624 -
        if timeout_call.active():
625 -
            timeout_call.cancel()
626 -
        return result
627 -
    d = client.poll(authzr).addCallback(repoll)
628 -
    timeout_call = clock.callLater(timeout, d.cancel)
629 -
    d.addBoth(cancel_timeout)
630 -
    return d
741 +
    orderr = yield client.finalize(orderr)
742 +
743 +
    now = clock.seconds()
744 +
    sleep = 0.5
745 +
746 +
    while True:
747 +
        status = orderr.body.status
748 +
749 +
        if status == STATUS_VALID:
750 +
            # All good.
751 +
            break
752 +
753 +
        if status == STATUS_INVALID:
754 +
            raise ServerError('Order is now invalid.')
755 +
756 +
        if clock.seconds() - now > timeout:
757 +
            raise ServerError('Timeout while waiting for order finalization.')
758 +
759 +
        yield deferLater(clock, sleep, lambda: None)
760 +
        sleep += sleep
761 +
762 +
        orderr = yield client.check_order(orderr)
763 +
764 +
    certificate = yield client.fetch_certificate(orderr.body.certificate)
765 +
    defer.returnValue(certificate)
631 766
632 767
633 768
JSON_CONTENT_TYPE = b'application/json'
769 +
JOSE_CONTENT_TYPE = b'application/jose+json'
634 770
JSON_ERROR_CONTENT_TYPE = b'application/problem+json'
635 771
DER_CONTENT_TYPE = b'application/pkix-cert'
772 +
PEM_CHAIN_TYPE = b'application/pem-certificate-chain'
636 773
REPLAY_NONCE_HEADER = b'Replay-Nonce'
637 774
638 775
@@ -683,11 +820,11 @@
Loading
683 820
684 821
class JWSClient(object):
685 822
    """
686 -
    HTTP client using JWS-signed messages.
823 +
    HTTP client using JWS-signed messages for ACME.
687 824
    """
688 825
    timeout = _DEFAULT_TIMEOUT
689 826
690 -
    def __init__(self, agent, key, alg,
827 +
    def __init__(self, agent, key, alg, new_nonce_url, kid,
691 828
                 user_agent=u'txacme/{}'.format(__version__).encode('ascii')):
692 829
        self._treq = HTTPClient(agent=agent)
693 830
        self._agent = agent
@@ -697,27 +834,37 @@
Loading
697 834
        self._user_agent = user_agent
698 835
699 836
        self._nonces = set()
837 +
        self._new_nonce = new_nonce_url
838 +
        self._kid = kid
700 839
701 -
    def _wrap_in_jws(self, nonce, obj):
840 +
    @classmethod
841 +
    def from_directory(cls, agent, key, alg, directory):
702 842
        """
703 -
        Wrap ``JSONDeSerializable`` object in JWS.
843 +
        Prepare for ACME operations based on 'directory' url.
704 844
705 -
        ..  todo:: Implement ``acmePath``.
845 +
        :param str directory: The URL to the ACME v2 directory.
706 846
707 -
        :param ~josepy.interfaces.JSONDeSerializable obj:
708 -
        :param bytes nonce:
709 -
710 -
        :rtype: `bytes`
711 -
        :return: JSON-encoded data
847 +
        :return: When operation is done.
848 +
        :rtype: Deferred[None]
712 849
        """
713 -
        with LOG_JWS_SIGN(key_type=self._key.typ, alg=self._alg.name,
714 -
                          nonce=nonce):
715 -
            jobj = obj.json_dumps().encode()
716 -
            return (
717 -
                JWS.sign(
718 -
                    payload=jobj, key=self._key, alg=self._alg, nonce=nonce)
719 -
                .json_dumps()
720 -
                .encode())
850 +
        # Provide invalid new_nonce_url & kid, but don't expose it to the
851 +
        # caller.
852 +
        self = cls(agent, key, alg, None, None)
853 +
854 +
        def cb_extract_new_nonce(directory):
855 +
            try:
856 +
                self._new_nonce = directory.newNonce
857 +
            except AttributeError:
858 +
                raise errors.ClientError(
859 +
                    'Directory has no newNonce URL', directory)
860 +
861 +
            return directory
862 +
        return (
863 +
            self.get(directory)
864 +
            .addCallback(json_content)
865 +
            .addCallback(messages.Directory.from_json)
866 +
            .addCallback(cb_extract_new_nonce)
867 +
        )
721 868
722 869
    @classmethod
723 870
    def _check_response(cls, response, content_type=JSON_CONTENT_TYPE):
@@ -726,7 +873,7 @@
Loading
726 873
727 874
        ..  note::
728 875
729 -
            Unlike :mod:`acme.client`, checking is strict.
876 +
            Unlike :mod:content_type`acme.client`, checking is strict.
730 877
731 878
        :param bytes content_type: Expected Content-Type response header.  If
732 879
            the response Content-Type does not match, :exc:`ClientError` is
@@ -742,18 +889,24 @@
Loading
742 889
743 890
        def _got_json(jobj):
744 891
            if 400 <= response.code < 600:
745 -
                if response_ct == JSON_ERROR_CONTENT_TYPE and jobj is not None:
892 +
                if (
893 +
                    response_ct.lower().startswith(JSON_ERROR_CONTENT_TYPE)
894 +
                    and jobj is not None
895 +
                        ):
746 896
                    raise ServerError(
747 897
                        messages.Error.from_json(jobj), response)
748 898
                else:
749 899
                    # response is not JSON object
750 -
                    raise errors.ClientError(response)
751 -
            elif response_ct != content_type:
752 -
                raise errors.ClientError(
753 -
                    'Unexpected response Content-Type: {0!r}'.format(
754 -
                        response_ct))
755 -
            elif content_type == JSON_CONTENT_TYPE and jobj is None:
756 -
                raise errors.ClientError(response)
900 +
                    return _fail_and_consume(
901 +
                        response, errors.ClientError('Response is not JSON.'))
902 +
            elif content_type not in response_ct.lower():
903 +
                return _fail_and_consume(response, errors.ClientError(
904 +
                    'Unexpected response Content-Type: {0!r}. '
905 +
                    'Expecting {1!r}.'.format(
906 +
                        response_ct, content_type)))
907 +
            elif JSON_CONTENT_TYPE in content_type.lower() and jobj is None:
908 +
                return _fail_and_consume(
909 +
                    response, errors.ClientError('Missing JSON body.'))
757 910
            return response
758 911
759 912
        response_ct = response.headers.getRawHeaders(
@@ -779,6 +932,9 @@
Loading
779 932
780 933
        :return: Deferred firing with the HTTP response.
781 934
        """
935 +
        if self._current_request is not None:
936 +
            return defer.fail(RuntimeError('Overlapped HTTP request'))
937 +
782 938
        def cb_request_done(result):
783 939
            """
784 940
            Called when we got a response from the request.
@@ -819,7 +975,7 @@
Loading
819 975
        agent_pool = getattr(self._agent, '_pool', None)
820 976
        if agent_pool:
821 977
            return agent_pool.closeCachedConnections()
822 -
        return succeed(None)
978 +
        return defer.succeed(None)
823 979
824 980
    def head(self, url, *args, **kwargs):
825 981
        """
@@ -866,7 +1022,10 @@
Loading
866 1022
            REPLAY_NONCE_HEADER, [None])[0]
867 1023
        with LOG_JWS_ADD_NONCE(raw_nonce=nonce) as action:
868 1024
            if nonce is None:
869 -
                raise errors.MissingNonce(response)
1025 +
                return _fail_and_consume(
1026 +
                    response,
1027 +
                    errors.ClientError(str(errors.MissingNonce(response))),
1028 +
                    )
870 1029
            else:
871 1030
                try:
872 1031
                    decoded_nonce = Header._fields['nonce'].decode(
@@ -874,7 +1033,8 @@
Loading
874 1033
                    )
875 1034
                    action.add_success_fields(nonce=decoded_nonce)
876 1035
                except DeserializationError as error:
877 -
                    raise errors.BadNonce(nonce, error)
1036 +
                    return _fail_and_consume(
1037 +
                        response, errors.BadNonce(nonce, error))
878 1038
                self._nonces.add(decoded_nonce)
879 1039
                return response
880 1040
@@ -887,18 +1047,22 @@
Loading
887 1047
            with action:
888 1048
                nonce = self._nonces.pop()
889 1049
                action.add_success_fields(nonce=nonce)
890 -
                return succeed(nonce)
1050 +
                return defer.succeed(nonce)
891 1051
        else:
892 1052
            with action.context():
893 1053
                return (
894 -
                    DeferredContext(self.head(url))
1054 +
                    DeferredContext(self.head(self._new_nonce))
895 1055
                    .addCallback(self._add_nonce)
896 1056
                    .addCallback(lambda _: self._nonces.pop())
897 1057
                    .addCallback(tap(
898 1058
                        lambda nonce: action.add_success_fields(nonce=nonce)))
899 1059
                    .addActionFinish())
900 1060
901 -
    def _post(self, url, obj, content_type, **kwargs):
1061 +
    def _post(
1062 +
        self, url, obj, content_type,
1063 +
        response_type=JSON_CONTENT_TYPE, kid=None,
1064 +
        **kwargs
1065 +
            ):
902 1066
        """
903 1067
        POST an object and check the response.
904 1068
@@ -911,20 +1075,43 @@
Loading
911 1075
            Problem (draft-ietf-appsawg-http-problem-00).
912 1076
        :raises acme.errors.ClientError: In case of other protocol errors.
913 1077
        """
1078 +
        if kid is None:
1079 +
            kid = self._kid
1080 +
1081 +
        def cb_wrap_in_jws(nonce):
1082 +
            with LOG_JWS_SIGN(key_type=self._key.typ, alg=self._alg.name,
1083 +
                              nonce=nonce):
1084 +
                if obj is None:
1085 +
                    jobj = b''
1086 +
                else:
1087 +
                    jobj = obj.json_dumps().encode()
1088 +
                result = (
1089 +
                    JWS.sign(
1090 +
                        payload=jobj,
1091 +
                        key=self._key,
1092 +
                        alg=self._alg,
1093 +
                        nonce=nonce,
1094 +
                        url=url,
1095 +
                        kid=kid,
1096 +
                        )
1097 +
                    .json_dumps()
1098 +
                    .encode())
1099 +
                return result
1100 +
914 1101
        with LOG_JWS_POST().context():
915 1102
            headers = kwargs.setdefault('headers', Headers())
916 -
            headers.setRawHeaders(b'content-type', [JSON_CONTENT_TYPE])
1103 +
            headers.setRawHeaders(b'content-type', [JOSE_CONTENT_TYPE])
917 1104
            return (
918 1105
                DeferredContext(self._get_nonce(url))
919 -
                .addCallback(self._wrap_in_jws, obj)
1106 +
                .addCallback(cb_wrap_in_jws)
920 1107
                .addCallback(
921 1108
                    lambda data: self._send_request(
922 1109
                        u'POST', url, data=data, **kwargs))
923 1110
                .addCallback(self._add_nonce)
924 -
                .addCallback(self._check_response, content_type=content_type)
1111 +
                .addCallback(self._check_response, content_type=response_type)
925 1112
                .addActionFinish())
926 1113
927 -
    def post(self, url, obj, content_type=JSON_CONTENT_TYPE, **kwargs):
1114 +
    def post(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs):
928 1115
        """
929 1116
        POST an object and check the response. Retry once if a badNonce error
930 1117
        is received.
@@ -957,8 +1144,18 @@
Loading
957 1144
            .addErrback(retry_bad_nonce))
958 1145
959 1146
1147 +
def _fail_and_consume(response, error):
1148 +
    """
1149 +
    Fail the deferred, but before the read all the pending data from the
1150 +
    response.
1151 +
    """
1152 +
    def fail(_):
1153 +
        raise error
1154 +
    return response.text().addBoth(fail)
1155 +
1156 +
960 1157
__all__ = [
961 1158
    'Client', 'JWSClient', 'ServerError', 'JSON_CONTENT_TYPE',
962 1159
    'JSON_ERROR_CONTENT_TYPE', 'REPLAY_NONCE_HEADER', 'fqdn_identifier',
963 -
    'answer_challenge', 'poll_until_valid', 'NoSupportedChallenges',
1160 +
    'answer_challenge', 'get_certificate', 'NoSupportedChallenges',
964 1161
    'AuthorizationFailed', 'DER_CONTENT_TYPE']

@@ -11,10 +11,10 @@
Loading
11 11
from cryptography.hazmat.backends import default_backend
12 12
from cryptography.hazmat.primitives import hashes, serialization
13 13
from cryptography.x509.oid import ExtensionOID, NameOID
14 -
from testtools import TestCase
15 14
from twisted.internet import reactor
16 15
from twisted.internet.defer import Deferred, fail, succeed
17 16
from twisted.python.compat import unicode
17 +
from twisted.trial.unittest import TestCase
18 18
from zope.interface import implementer
19 19
20 20
from txacme.interfaces import ICertificateStore, IResponder
@@ -39,208 +39,6 @@
Loading
39 39
                'Reactor is not clean. DelayedCalls: %s' % (junk,))
40 40
41 41
42 -
@attr.s
43 -
class FakeClientController(object):
44 -
    """
45 -
    Controls issuing for `FakeClient`.
46 -
    """
47 -
    paused = attr.ib(default=False)
48 -
49 -
    _waiting = attr.ib(default=attr.Factory(list), init=False)
50 -
51 -
    def issue(self):
52 -
        """
53 -
        Return a deferred that fires when we are ready to issue.
54 -
        """
55 -
        if self.paused:
56 -
            d = Deferred()
57 -
            self._waiting.append(d)
58 -
            return d
59 -
        else:
60 -
            return succeed(None)
61 -
62 -
    def pause(self):
63 -
        """
64 -
        Temporarily pause issuing.
65 -
        """
66 -
        self.paused = True
67 -
68 -
    def resume(self, value=None):
69 -
        """
70 -
        Resume issuing, allowing any pending issuances to proceed.
71 -
72 -
        :param value: An (optional) value with which pending deferreds
73 -
            will be called back.
74 -
        """
75 -
        _waiting = self._waiting
76 -
        self._waiting = []
77 -
        for d in _waiting:
78 -
            d.callback(value)
79 -
80 -
    def count(self):
81 -
        """
82 -
        Count pending issuances.
83 -
        """
84 -
        return len(self._waiting)
85 -
86 -
87 -
class FakeClient(object):
88 -
    """
89 -
    Provides the same API as `~txacme.client.Client`, but performs no network
90 -
    operations and issues certificates signed by its own fake CA.
91 -
    """
92 -
    _challenge_types = [challenges.TLSSNI01]
93 -
94 -
    def __init__(self, key, clock, ca_key=None, controller=None):
95 -
        self.key = key
96 -
        self._clock = clock
97 -
        self._registered = False
98 -
        self._tos_agreed = None
99 -
        self._authorizations = {}
100 -
        self._challenges = {}
101 -
        self._ca_key = ca_key
102 -
        self._generate_ca_cert()
103 -
        self._paused = False
104 -
        self._waiting = []
105 -
        if controller is None:
106 -
            controller = FakeClientController()
107 -
        self._controller = controller
108 -
109 -
    def _now(self):
110 -
        """
111 -
        Get the current time.
112 -
        """
113 -
        return clock_now(self._clock)
114 -
115 -
    def _generate_ca_cert(self):
116 -
        """
117 -
        Generate a CA cert/key.
118 -
        """
119 -
        if self._ca_key is None:
120 -
            self._ca_key = generate_private_key(u'rsa')
121 -
        self._ca_name = x509.Name([
122 -
            x509.NameAttribute(NameOID.COMMON_NAME, u'ACME Snake Oil CA')])
123 -
        self._ca_cert = (
124 -
            x509.CertificateBuilder()
125 -
            .subject_name(self._ca_name)
126 -
            .issuer_name(self._ca_name)
127 -
            .not_valid_before(self._now() - timedelta(seconds=3600))
128 -
            .not_valid_after(self._now() + timedelta(days=3650))
129 -
            .public_key(self._ca_key.public_key())
130 -
            .serial_number(int(uuid4()))
131 -
            .add_extension(
132 -
                x509.BasicConstraints(ca=True, path_length=0),
133 -
                critical=True)
134 -
            .add_extension(
135 -
                x509.SubjectKeyIdentifier.from_public_key(
136 -
                    self._ca_key.public_key()),
137 -
                critical=False)
138 -
            .sign(
139 -
                private_key=self._ca_key,
140 -
                algorithm=hashes.SHA256(),
141 -
                backend=default_backend()))
142 -
        self._ca_aki = x509.AuthorityKeyIdentifier.from_issuer_public_key(
143 -
            self._ca_key.public_key())
144 -
145 -
    def stop(self):
146 -
        """
147 -
        Called to stop the client and trigger cleanups.
148 -
        """
149 -
        # Nothing to stop as reactor is not spun.
150 -
        return succeed(None)
151 -
152 -
    def register(self, new_reg=None):
153 -
        self._registered = True
154 -
        if new_reg is None:
155 -
            new_reg = messages.NewRegistration()
156 -
        self.regr = messages.RegistrationResource(
157 -
            body=messages.Registration(
158 -
                contact=new_reg.contact,
159 -
                agreement=new_reg.agreement))
160 -
        return succeed(self.regr)
161 -
162 -
    def agree_to_tos(self, regr):
163 -
        self._tos_agreed = True
164 -
        self.regr = self.regr.update(
165 -
            body=regr.body.update(
166 -
                agreement=regr.terms_of_service))
167 -
        return succeed(self.regr)
168 -
169 -
    def request_challenges(self, identifier):
170 -
        self._authorizations[identifier] = challenges = OrderedDict()
171 -
        for chall_type in self._challenge_types:
172 -
            uuid = unicode(uuid4())
173 -
            challb = messages.ChallengeBody(
174 -
                chall=chall_type(token=b'token'),
175 -
                uri=uuid,
176 -
                status=messages.STATUS_PENDING)
177 -
            challenges[chall_type] = uuid
178 -
            self._challenges[uuid] = challb
179 -
        return succeed(
180 -
            messages.AuthorizationResource(
181 -
                body=messages.Authorization(
182 -
                    identifier=identifier,
183 -
                    status=messages.STATUS_PENDING,
184 -
                    challenges=[
185 -
                        self._challenges[u] for u in challenges.values()],
186 -
                    combinations=[[n] for n in range(len(challenges))])))
187 -
188 -
    def answer_challenge(self, challenge_body, response):
189 -
        challb = self._challenges[challenge_body.uri]
190 -
        challb = challb.update(status=messages.STATUS_VALID)
191 -
        self._challenges[challenge_body.uri] = challb
192 -
        return succeed(challb)
193 -
194 -
    def poll(self, authzr):
195 -
        challenges = [
196 -
            self._challenges[u] for u
197 -
            in self._authorizations[authzr.body.identifier].values()]
198 -
        status = (
199 -
            messages.STATUS_VALID
200 -
            if any(c.status == messages.STATUS_VALID for c in challenges)
201 -
            else messages.STATUS_PENDING)
202 -
        return succeed(
203 -
            (messages.AuthorizationResource(
204 -
                body=messages.Authorization(
205 -
                    status=status,
206 -
                    challenges=challenges,
207 -
                    combinations=[[n] for n in range(len(challenges))])),
208 -
             1.0))
209 -
210 -
    def request_issuance(self, csr):
211 -
        csr = csr.csr
212 -
        # TODO: Only in Cryptography 1.3
213 -
        # assert csr.is_signature_valid
214 -
        cert = (
215 -
            x509.CertificateBuilder()
216 -
            .subject_name(csr.subject)
217 -
            .issuer_name(self._ca_name)
218 -
            .not_valid_before(self._now() - timedelta(seconds=3600))
219 -
            .not_valid_after(self._now() + timedelta(days=90))
220 -
            .serial_number(int(uuid4()))
221 -
            .public_key(csr.public_key())
222 -
            .add_extension(
223 -
                csr.extensions.get_extension_for_oid(
224 -
                    ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value,
225 -
                critical=False)
226 -
            .add_extension(
227 -
                x509.SubjectKeyIdentifier.from_public_key(csr.public_key()),
228 -
                critical=False)
229 -
            .add_extension(self._ca_aki, critical=False)
230 -
            .sign(
231 -
                private_key=self._ca_key,
232 -
                algorithm=hashes.SHA256(),
233 -
                backend=default_backend()))
234 -
        cert_res = messages.CertificateResource(
235 -
            body=cert.public_bytes(encoding=serialization.Encoding.DER))
236 -
        return self._controller.issue().addCallback(lambda _: cert_res)
237 -
238 -
    def fetch_chain(self, certr, max_length=10):
239 -
        return succeed([
240 -
            messages.CertificateResource(
241 -
                body=self._ca_cert.public_bytes(
242 -
                    encoding=serialization.Encoding.DER))])
243 -
244 42
245 43
@implementer(IResponder)
246 44
@attr.s
@@ -282,5 +80,4 @@
Loading
282 80
        return succeed(self._store)
283 81
284 82
285 -
__all__ = [
286 -
    'FakeClient', 'FakeClientController', 'MemoryStore', 'NullResponder']
83 +
__all__ = ['MemoryStore', 'NullResponder']

@@ -1,5 +1,4 @@
Loading
1 1
from ._http import HTTP01Responder
2 -
from ._tls import TLSSNI01Responder
3 2
4 3
5 4
try:
@@ -9,4 +8,4 @@
Loading
9 8
    pass
10 9
11 10
12 -
__all__ = ['HTTP01Responder', 'LibcloudDNSResponder', 'TLSSNI01Responder']
11 +
__all__ = ['HTTP01Responder', 'LibcloudDNSResponder']

@@ -29,37 +29,30 @@
Loading
29 29
    raise ValueError(key_type)
30 30
31 31
32 -
def generate_tls_sni_01_cert(server_name, key_type=u'rsa',
33 -
                             _generate_private_key=None):
34 -
    """
35 -
    Generate a certificate/key pair for responding to a tls-sni-01 challenge.
36 -
37 -
    :param str server_name: The SAN the certificate should have.
38 -
    :param str key_type: The type of key to generate; usually not necessary.
39 -
40 -
    :rtype: ``Tuple[`~cryptography.x509.Certificate`, PrivateKey]``
41 -
    :return: A tuple of the certificate and private key.
42 -
    """
43 -
    key = (_generate_private_key or generate_private_key)(key_type)
44 -
    name = x509.Name([
45 -
        x509.NameAttribute(NameOID.COMMON_NAME, u'acme.invalid')])
46 -
    cert = (
47 -
        x509.CertificateBuilder()
48 -
        .subject_name(name)
49 -
        .issuer_name(name)
50 -
        .not_valid_before(datetime.now() - timedelta(seconds=3600))
51 -
        .not_valid_after(datetime.now() + timedelta(seconds=3600))
52 -
        .serial_number(int(uuid.uuid4()))
53 -
        .public_key(key.public_key())
54 -
        .add_extension(
55 -
            x509.SubjectAlternativeName([x509.DNSName(server_name)]),
56 -
            critical=False)
57 -
        .sign(
58 -
            private_key=key,
59 -
            algorithm=hashes.SHA256(),
32 +
def load_or_create_client_key(pem_path):
33 +
    """
34 +
    Load the client key from a directory, creating it if it does not exist.
35 +
36 +
    .. note:: The client key that will be created will be a 2048-bit RSA key.
37 +
38 +
    :type pem_path: ``twisted.python.filepath.FilePath``
39 +
    :param pem_path: The certificate directory
40 +
        to use, as with the endpoint.
41 +
    """
42 +
    acme_key_file = pem_path.asTextMode().child(u'client.key')
43 +
    if acme_key_file.exists():
44 +
        key = serialization.load_pem_private_key(
45 +
            acme_key_file.getContent(),
46 +
            password=None,
60 47
            backend=default_backend())
61 -
        )
62 -
    return (cert, key)
48 +
    else:
49 +
        key = generate_private_key(u'rsa')
50 +
        acme_key_file.setContent(
51 +
            key.private_bytes(
52 +
                encoding=serialization.Encoding.PEM,
53 +
                format=serialization.PrivateFormat.TraditionalOpenSSL,
54 +
                encryption_algorithm=serialization.NoEncryption()))
55 +
    return JWKRSA(key=key)
63 56
64 57
65 58
def tap(f):

@@ -1,18 +1,15 @@
Loading
1 1
from operator import methodcaller
2 +
import os
3 +
import tempfile
4 +
import shutil
2 5
3 6
import pem
4 -
from fixtures import TempDir
5 -
from hypothesis import example, given
6 -
from testtools import TestCase
7 -
from testtools.matchers import (
8 -
    AfterPreprocessing, AllMatch, ContainsDict, Equals, Is, IsInstance)
9 -
from testtools.twistedsupport import succeeded
7 +
from twisted.internet import defer
10 8
from twisted.python.compat import unicode
11 9
from twisted.python.filepath import FilePath
10 +
from twisted.trial.unittest import TestCase
12 11
13 12
from txacme.store import DirectoryStore
14 -
from txacme.test import strategies as ts
15 -
from txacme.test.test_client import failed_with
16 13
from txacme.testing import MemoryStore
17 14
18 15
@@ -47,93 +44,109 @@
Loading
47 44
    """
48 45
    Tests for `txacme.interfaces.ICertificateStore` implementations.
49 46
    """
50 -
    @example(u'example.com', EXAMPLE_PEM_OBJECTS)
51 -
    @given(ts.dns_names(), ts.pem_objects())
52 -
    def test_insert(self, server_name, pem_objects):
47 +
48 +
    @defer.inlineCallbacks
49 +
    def test_insert(self):
53 50
        """
54 51
        Inserting an entry causes the same entry to be returned by ``get`` and
55 52
        ``as_dict``.
56 53
        """
57 -
        self.assertThat(
58 -
            self.cert_store.store(server_name, pem_objects),
59 -
            succeeded(Is(None)))
60 -
        self.assertThat(
61 -
            self.cert_store.get(server_name),
62 -
            succeeded(Equals(pem_objects)))
63 -
        self.assertThat(
64 -
            self.cert_store.as_dict(),
65 -
            succeeded(ContainsDict(
66 -
                {server_name: Equals(pem_objects)})))
67 -
68 -
    @example(u'example.com', EXAMPLE_PEM_OBJECTS, EXAMPLE_PEM_OBJECTS2)
69 -
    @given(ts.dns_names(), ts.pem_objects(), ts.pem_objects())
70 -
    def test_insert_twice(self, server_name, pem_objects, pem_objects2):
54 +
        server_name = 'example.com'
55 +
        pem_objects = EXAMPLE_PEM_OBJECTS
56 +
        cert_store = self.getCertStore()
57 +
58 +
        result = yield cert_store.store(server_name, pem_objects)
59 +
        self.assertIsNone(result)
60 +
61 +
        result = yield cert_store.get(server_name)
62 +
        self.assertEqual(pem_objects, result)
63 +
64 +
        result = yield cert_store.as_dict()
65 +
        self.assertEqual({'example.com': pem_objects}, result)
66 +