Пример #1
0
class _StoreTestsMixin(object):
    """
    Tests for `txacme.interfaces.ICertificateStore` implementations.
    """
    @example(u'example.com', EXAMPLE_PEM_OBJECTS)
    @given(ts.dns_names(), ts.pem_objects())
    def test_insert(self, server_name, pem_objects):
        """
        Inserting an entry causes the same entry to be returned by ``get`` and
        ``as_dict``.
        """
        self.assertThat(self.cert_store.store(server_name, pem_objects),
                        succeeded(Is(None)))
        self.assertThat(self.cert_store.get(server_name),
                        succeeded(Equals(pem_objects)))
        self.assertThat(
            self.cert_store.as_dict(),
            succeeded(ContainsDict({server_name: Equals(pem_objects)})))

    @example(u'example.com', EXAMPLE_PEM_OBJECTS, EXAMPLE_PEM_OBJECTS2)
    @given(ts.dns_names(), ts.pem_objects(), ts.pem_objects())
    def test_insert_twice(self, server_name, pem_objects, pem_objects2):
        """
        Inserting an entry a second time overwrites the first entry.
        """
        self.assertThat(self.cert_store.store(server_name, pem_objects),
                        succeeded(Is(None)))
        self.assertThat(self.cert_store.store(server_name, pem_objects2),
                        succeeded(Is(None)))
        self.assertThat(self.cert_store.get(server_name),
                        succeeded(Equals(pem_objects2)))
        self.assertThat(
            self.cert_store.as_dict(),
            succeeded(ContainsDict({server_name: Equals(pem_objects2)})))

    @example(u'example.com')
    @given(ts.dns_names())
    def test_get_missing(self, server_name):
        """
        Getting a non-existent entry results in `KeyError`.
        """
        self.assertThat(self.cert_store.get(server_name),
                        failed_with(IsInstance(KeyError)))

    @example(u'example.com', EXAMPLE_PEM_OBJECTS)
    @given(ts.dns_names(), ts.pem_objects())
    def test_unicode_keys(self, server_name, pem_objects):
        """
        The keys of the dict returned by ``as_dict`` are ``unicode``.
        """
        self.assertThat(self.cert_store.store(server_name, pem_objects),
                        succeeded(Is(None)))
        self.assertThat(
            self.cert_store.as_dict(),
            succeeded(
                AfterPreprocessing(methodcaller('keys'),
                                   AllMatch(IsInstance(unicode)))))
Пример #2
0
class CSRTests(TestCase):
    """
    `~txacme.util.encode_csr` and `~txacme.util.decode_csr` serialize CSRs in
    JOSE Base64 DER encoding.
    """
    @example(names=[u'example.com', u'example.org'])
    @given(names=s.lists(ts.dns_names(), min_size=1))
    def test_roundtrip(self, names):
        """
        The encoding roundtrips.
        """
        assume(len(names[0]) <= 64)
        csr = csr_for_names(names, RSA_KEY_512_RAW)
        self.assertThat(decode_csr(encode_csr(csr)), Equals(csr))

    def test_decode_garbage(self):
        """
        If decoding fails, `~txacme.util.decode_csr` raises
        `~josepy.errors.DeserializationError`.
        """
        with ExpectedException(DeserializationError):
            decode_csr(u'blah blah not a valid CSR')

    def test_empty_names_invalid(self):
        """
        `~txacme.util.csr_for_names` raises `ValueError` if given an empty list
        of names.
        """
        with ExpectedException(ValueError):
            csr_for_names([], RSA_KEY_512_RAW)

    @example(names=[u'example.com', u'example.org'], key=RSA_KEY_512_RAW)
    @given(names=s.lists(ts.dns_names(), min_size=1),
           key=s.just(RSA_KEY_512_RAW))
    def test_valid_for_names(self, names, key):
        """
        `~txacme.util.csr_for_names` returns a CSR that is actually valid for
        the given names.
        """
        assume(len(names[0]) <= 64)

        self.assertThat(csr_for_names(names, key),
                        MatchesAll(*[ValidForName(name) for name in names]))

    def test_common_name_too_long(self):
        """
        If the first name provided is too long, `~txacme.util.csr_for_names`
        uses a dummy value for the common name.
        """
        self.assertThat(
            csr_for_names([u'aaaa.' * 16], RSA_KEY_512_RAW),
            MatchesStructure(subject=Equals(
                x509.Name([
                    x509.NameAttribute(NameOID.COMMON_NAME,
                                       u'san.too.long.invalid')
                ]))))
Пример #3
0
def panicing_cert(draw, now, panic):
    server_name = draw(ts.dns_names())
    offset = timedelta(seconds=draw(
        s.integers(min_value=-1000, max_value=int(panic.total_seconds()))))
    return (server_name,
            _generate_cert(server_name,
                           not_valid_before=now + offset -
                           timedelta(seconds=1),
                           not_valid_after=now + offset))
Пример #4
0
def panicing_cert(draw, now, panic):
    server_name = draw(ts.dns_names())
    offset = timedelta(seconds=draw(
        s.integers(
                min_value=-1000,
                max_value=int(panic.total_seconds()))))
    return (server_name,
            _generate_cert(
                server_name,
                not_valid_before=now + offset - timedelta(seconds=1),
                not_valid_after=now + offset))
Пример #5
0
class LibcloudResponderTests(_CommonResponderTests, TestCase):
    """
    `.LibcloudDNSResponder` implements a responder for dns-01 challenges using
    libcloud on the backend.
    """
    _challenge_factory = challenges.DNS01
    _challenge_type = u'dns-01'

    def _responder_factory(self, zone_name=u'example.com'):
        responder = LibcloudDNSResponder.create(
            reactor=SynchronousReactorThreads(),
            driver_name='dummy',
            username='******',
            password='******',
            zone_name=zone_name,
            settle_delay=0.0)
        if zone_name is not None:
            responder._driver.create_zone(zone_name)
        responder._thread_pool, self._perform = createMemoryWorker()
        return responder

    def _do_one_thing(self):
        return self._perform()

    def test_daemon_threads(self):
        """
        ``_daemon_thread`` creates thread objects with ``daemon`` set.
        """
        thread = _daemon_thread()
        self.assertThat(thread, MatchesStructure(daemon=Equals(True)))

    @example(token=EXAMPLE_TOKEN,
             subdomain=u'acme-testing',
             zone_name=u'example.com')
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
           subdomain=ts.dns_names(),
           zone_name=ts.dns_names())
    def test_start_responding(self, token, subdomain, zone_name):
        """
        Calling ``start_responding`` causes an appropriate TXT record to be
        created.
        """
        challenge = self._challenge_factory(token=token)
        response = challenge.response(RSA_KEY_512)
        responder = self._responder_factory(zone_name=zone_name)
        server_name = u'{}.{}'.format(subdomain, zone_name)
        zone = responder._driver.list_zones()[0]

        self.assertThat(zone.list_records(), HasLength(0))
        d = responder.start_responding(server_name, challenge, response)
        self._perform()
        self.assertThat(d, succeeded(Always()))
        self.assertThat(
            zone.list_records(),
            MatchesListwise([
                MatchesStructure(
                    name=EndsWith(u'.' + subdomain),
                    type=Equals('TXT'),
                )
            ]))

        # Starting twice before stopping doesn't break things
        d = responder.start_responding(server_name, challenge, response)
        self._perform()
        self.assertThat(d, succeeded(Always()))
        self.assertThat(zone.list_records(), HasLength(1))

        d = responder.stop_responding(server_name, challenge, response)
        self._perform()
        self.assertThat(d, succeeded(Always()))
        self.assertThat(zone.list_records(), HasLength(0))

    @example(token=EXAMPLE_TOKEN,
             subdomain=u'acme-testing',
             zone_name=u'example.com')
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
           subdomain=ts.dns_names(),
           zone_name=ts.dns_names())
    def test_wrong_zone(self, token, subdomain, zone_name):
        """
        Trying to respond for a domain not in the configured zone results in a
        `.NotInZone` exception.
        """
        challenge = self._challenge_factory(token=token)
        response = challenge.response(RSA_KEY_512)
        responder = self._responder_factory(zone_name=zone_name)
        server_name = u'{}.{}.junk'.format(subdomain, zone_name)
        d = maybeDeferred(responder.start_responding, server_name, challenge,
                          response)
        self._perform()
        self.assertThat(
            d,
            failed_with(
                MatchesAll(
                    IsInstance(NotInZone),
                    MatchesStructure(server_name=EndsWith(u'.' + server_name),
                                     zone_name=Equals(zone_name)))))

    @example(token=EXAMPLE_TOKEN,
             subdomain=u'acme-testing',
             zone_name=u'example.com')
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
           subdomain=ts.dns_names(),
           zone_name=ts.dns_names())
    def test_missing_zone(self, token, subdomain, zone_name):
        """
        `.ZoneNotFound` is raised if the configured zone cannot be found at the
        configured provider.
        """
        challenge = self._challenge_factory(token=token)
        response = challenge.response(RSA_KEY_512)
        responder = self._responder_factory(zone_name=zone_name)
        server_name = u'{}.{}'.format(subdomain, zone_name)
        for zone in responder._driver.list_zones():
            zone.delete()
        d = maybeDeferred(responder.start_responding, server_name, challenge,
                          response)
        self._perform()
        self.assertThat(
            d,
            failed_with(
                MatchesAll(IsInstance(ZoneNotFound),
                           MatchesStructure(zone_name=Equals(zone_name)))))

    @example(token=EXAMPLE_TOKEN,
             subdomain=u'acme-testing',
             extra=u'extra',
             zone_name1=u'example.com',
             suffix1=u'.',
             zone_name2=u'example.org',
             suffix2=u'')
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
           subdomain=ts.dns_names(),
           extra=ts.dns_names(),
           zone_name1=ts.dns_names(),
           suffix1=s.sampled_from([u'', u'.']),
           zone_name2=ts.dns_names(),
           suffix2=s.sampled_from([u'', u'.']))
    def test_auto_zone(self, token, subdomain, extra, zone_name1, suffix1,
                       zone_name2, suffix2):
        """
        If the configured zone_name is ``None``, the zone will be guessed by
        finding the longest zone that is a suffix of the server name.
        """
        zone_name3 = extra + u'.' + zone_name1
        zone_name4 = extra + u'.' + zone_name2
        server_name = u'{}.{}.{}'.format(subdomain, extra, zone_name1)
        assume(
            len({server_name, zone_name1, zone_name2, zone_name3, zone_name4})
            == 5)
        challenge = self._challenge_factory(token=token)
        response = challenge.response(RSA_KEY_512)
        responder = self._responder_factory(zone_name=None)
        zone1 = responder._driver.create_zone(zone_name1 + suffix1)
        zone2 = responder._driver.create_zone(zone_name2 + suffix2)
        zone3 = responder._driver.create_zone(zone_name3 + suffix1)
        zone4 = responder._driver.create_zone(zone_name4 + suffix2)
        self.assertThat(zone1.list_records(), HasLength(0))
        self.assertThat(zone2.list_records(), HasLength(0))
        self.assertThat(zone3.list_records(), HasLength(0))
        self.assertThat(zone4.list_records(), HasLength(0))
        d = responder.start_responding(server_name, challenge, response)
        self._perform()
        self.assertThat(d, succeeded(Always()))
        self.assertThat(zone1.list_records(), HasLength(0))
        self.assertThat(zone2.list_records(), HasLength(0))
        self.assertThat(
            zone3.list_records(),
            MatchesListwise([
                MatchesStructure(
                    name=AfterPreprocessing(methodcaller('rstrip', u'.'),
                                            EndsWith(u'.' + subdomain)),
                    type=Equals('TXT'),
                )
            ]))
        self.assertThat(zone4.list_records(), HasLength(0))

    @example(token=EXAMPLE_TOKEN,
             subdomain=u'acme-testing',
             zone_name1=u'example.com',
             zone_name2=u'example.org')
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
           subdomain=ts.dns_names(),
           zone_name1=ts.dns_names(),
           zone_name2=ts.dns_names())
    def test_auto_zone_missing(self, token, subdomain, zone_name1, zone_name2):
        """
        If the configured zone_name is ``None``, and no matching zone is found,
        ``NotInZone`` is raised.
        """
        server_name = u'{}.{}'.format(subdomain, zone_name1)
        assume(not server_name.endswith(zone_name2))
        challenge = self._challenge_factory(token=token)
        response = challenge.response(RSA_KEY_512)
        responder = self._responder_factory(zone_name=None)
        zone = responder._driver.create_zone(zone_name2)
        self.assertThat(zone.list_records(), HasLength(0))
        d = maybeDeferred(responder.start_responding, server_name, challenge,
                          response)
        self._perform()
        self.assertThat(
            d,
            failed_with(
                MatchesAll(
                    IsInstance(NotInZone),
                    MatchesStructure(server_name=EndsWith(u'.' + server_name),
                                     zone_name=Is(None)))))
Пример #6
0
class AcmeIssuingServiceTests(TestCase):
    """
    Tests for `txacme.service.AcmeIssuingService`.
    """
    def test_when_certs_valid_no_certs(self):
        """
        The deferred returned by ``when_certs_valid`` fires immediately if
        there are no certs in the store.
        """
        service = self.useFixture(AcmeFixture()).service
        service.startService()
        self.assertThat(service.when_certs_valid(), succeeded(Is(None)))

    @example(now=datetime(2000, 1, 1, 0, 0, 0),
             certs=[(timedelta(seconds=60), u'example.com'),
                    (timedelta(seconds=90), u'example.org')])
    @given(
        now=datetimes(min_value=datetime(1971, 1, 1),
                      max_value=datetime(2030, 1, 1)),
        certs=s.lists(
            s.tuples(
                s.integers(min_value=0,
                           max_value=1000).map(lambda s: timedelta(seconds=s)),
                ts.dns_names())))
    def test_when_certs_valid_all_certs_valid(self, now, certs):
        """
        The deferred returned by ``when_certs_valid`` fires immediately if
        none of the certs in the store are expired.
        """
        certs = {
            server_name:
            _generate_cert(server_name,
                           not_valid_before=now - timedelta(seconds=1),
                           not_valid_after=now + offset)
            for offset, server_name in certs
        }
        with AcmeFixture(now=now, certs=certs) as fixture:
            service = fixture.service
            service.startService()
            self.assertThat(service.when_certs_valid(), succeeded(Is(None)))
            self.assertThat(fixture.responder.challenges, HasLength(0))

    @given(fixture=panicing_certs_fixture())
    def test_when_certs_valid_certs_expired(self, fixture):
        """
        The deferred returned by ``when_certs_valid`` only fires once all
        panicing and expired certs have been renewed.
        """
        with fixture:
            service = fixture.service
            d = service.when_certs_valid()
            self.assertThat(d, has_no_result())
            service.startService()
            self.assertThat(d, succeeded(Is(None)))
            max_expiry = fixture.now + service.panic_interval
            self.assertThat(
                fixture.cert_store.as_dict(),
                succeeded(
                    AfterPreprocessing(
                        methodcaller('values'),
                        AllMatch(
                            AllMatch(
                                _match_certificate(
                                    MatchesStructure(
                                        not_valid_after=GreaterThan(
                                            max_expiry))))))))
            self.assertThat(fixture.responder.challenges, HasLength(0))

    def test_time_marches_on(self):
        """
        Any certs that have exceeded the panic or reissue intervals will be
        reissued at the next check.
        """
        now = datetime(2000, 1, 1, 0, 0, 0)
        certs = {
            u'example.com':
            _generate_cert(u'example.com',
                           not_valid_before=now - timedelta(seconds=1),
                           not_valid_after=now + timedelta(days=31)),
            u'example.org':
            _generate_cert(u'example.org',
                           not_valid_before=now - timedelta(seconds=1),
                           not_valid_after=now + timedelta(days=32)),
        }
        with AcmeFixture(now=now, certs=certs) as fixture:
            fixture.service.startService()
            self.assertThat(fixture.service.when_certs_valid(),
                            succeeded(Is(None)))
            self.assertThat(fixture.cert_store.as_dict(),
                            succeeded(Equals(certs)))

            fixture.clock.advance(36 * 60 * 60)
            self.assertThat(
                fixture.cert_store.as_dict(),
                succeeded(
                    MatchesDict({
                        u'example.com':
                        Not(Equals(certs[u'example.com'])),
                        u'example.org':
                        Equals(certs[u'example.org']),
                    })))
            self.assertThat(fixture.responder.challenges, HasLength(0))

            fixture.clock.advance(36 * 60 * 60)
            self.assertThat(
                fixture.cert_store.as_dict(),
                succeeded(
                    MatchesDict({
                        u'example.com':
                        Not(Equals(certs[u'example.com'])),
                        u'example.org':
                        Not(Equals(certs[u'example.org'])),
                    })))
            self.assertThat(fixture.responder.challenges, HasLength(0))

    @run_test_with(AsynchronousDeferredRunTest)
    def test_errors(self):
        """
        If a cert renewal fails within the panic interval, the panic callback
        is invoked; otherwise the error is logged normally.
        """
        now = datetime(2000, 1, 1, 0, 0, 0)
        certs = {
            u'example.com':
            _generate_cert(u'example.com',
                           not_valid_before=now - timedelta(seconds=1),
                           not_valid_after=now + timedelta(days=31)),
        }
        panics = []
        with AcmeFixture(now=now,
                         certs=certs,
                         panic=lambda *a: panics.append(a)) as fixture:
            fixture.service.startService()
            self.assertThat(fixture.service.when_certs_valid(),
                            succeeded(Is(None)))
            self.assertThat(fixture.responder.challenges, HasLength(0))

            fixture.controller.pause()
            fixture.clock.advance(36 * 60 * 60)
            # Resume the client.request_issuance deferred with an exception.
            fixture.controller.resume(Failure(Exception()))
            self.assertThat(flush_logged_errors(), HasLength(1))
            self.assertThat(panics, Equals([]))
            self.assertThat(fixture.responder.challenges, HasLength(0))

            fixture.controller.pause()
            fixture.clock.advance(15 * 24 * 60 * 60)
            # Resume the client.request_issuance deferred with an exception.
            fixture.controller.resume(Failure(Exception()))
            self.assertThat(
                panics,
                MatchesListwise([
                    MatchesListwise(
                        [IsInstance(Failure),
                         Equals(u'example.com')]),
                ]))
            self.assertThat(fixture.responder.challenges, HasLength(0))

    @run_test_with(AsynchronousDeferredRunTest)
    def test_timer_errors(self):
        """
        If the timed check fails (for example, because registration fails), the
        error should be caught and logged.
        """
        with AcmeFixture(client=FailingClient()) as fixture:
            fixture.service.startService()
            self.assertThat(fixture.service._check_certs(),
                            succeeded(Always()))
            self.assertThat(flush_logged_errors(), HasLength(2))

    def test_starting_stopping_cancellation(self):
        """
        Test the starting and stopping behaviour.
        """
        with AcmeFixture(client=HangingClient()) as fixture:
            d = fixture.service.when_certs_valid()
            self.assertThat(d, has_no_result())
            fixture.service.startService()
            self.assertThat(d, has_no_result())
            fixture.service.stopService()
            self.assertThat(d, failed(Always()))

    @run_test_with(AsynchronousDeferredRunTest)
    def test_default_panic(self):
        """
        The default panic callback logs a message via ``twisted.logger``.
        """
        try:
            1 / 0
        except BaseException:
            f = Failure()
        _default_panic(f, u'server_name')
        self.assertThat(flush_logged_errors(), Equals([f]))

    @example(u'example.com')
    @given(ts.dns_names())
    def test_blank_cert(self, server_name):
        """
        An empty certificate file will be treated like an expired certificate.
        """
        with AcmeFixture(certs={server_name: []}) as fixture:
            fixture.service.startService()
            self.assertThat(fixture.service.when_certs_valid(),
                            succeeded(Always()))
            self.assertThat(
                fixture.cert_store.as_dict(),
                succeeded(MatchesDict({server_name: Not(Equals([]))})))
            self.assertThat(fixture.responder.challenges, HasLength(0))

    @example(u'example.com')
    @given(ts.dns_names())
    def test_issue_one_cert(self, server_name):
        """
        ``issue_cert`` will (re)issue a single certificate unconditionally.
        """
        with AcmeFixture() as fixture:
            fixture.service.startService()
            self.assertThat(fixture.cert_store.as_dict(),
                            succeeded(Not(Contains(server_name))))
            self.assertThat(fixture.service.issue_cert(server_name),
                            succeeded(Always()))
            self.assertThat(
                fixture.cert_store.as_dict(),
                succeeded(MatchesDict({server_name: Not(Equals([]))})))

    @example(u'example.com')
    @given(ts.dns_names())
    def test_issue_concurrently(self, server_name):
        """
        Invoking ``issue_cert`` multiple times concurrently for the same name
        will not start multiple issuing processes, only wait for the first
        process to complete.
        """
        with AcmeFixture() as fixture:
            fixture.service.startService()
            self.assertThat(fixture.cert_store.as_dict(),
                            succeeded(Not(Contains(server_name))))

            fixture.controller.pause()
            d1 = fixture.service.issue_cert(server_name)
            self.assertThat(d1, has_no_result())
            d2 = fixture.service.issue_cert(server_name)
            self.assertThat(d2, has_no_result())
            self.assertThat(fixture.controller.count(), Equals(1))

            fixture.controller.resume()
            self.assertThat(d1, succeeded(Always()))
            self.assertThat(d2, succeeded(Always()))

            self.assertThat(
                fixture.cert_store.as_dict(),
                succeeded(MatchesDict({server_name: Not(Equals([]))})))

    @example(u'example.com')
    @given(ts.dns_names())
    def test_cancellation(self, server_name):
        """
        Cancelling the deferred returned by ``issue_cert`` cancels the actual
        issuing process.
        """
        with AcmeFixture() as fixture:
            fixture.service.startService()
            self.assertThat(fixture.cert_store.as_dict(),
                            succeeded(Not(Contains(server_name))))

            fixture.controller.pause()
            d1 = fixture.service.issue_cert(server_name)
            self.assertThat(d1, has_no_result())
            d2 = fixture.service.issue_cert(server_name)
            self.assertThat(d2, has_no_result())
            self.assertThat(fixture.controller.count(), Equals(1))
            d2.cancel()

            fixture.controller.resume()
            self.assertThat(d1, failed_with(IsInstance(CancelledError)))
            self.assertThat(d2, failed_with(IsInstance(CancelledError)))
            self.assertThat(fixture.cert_store.as_dict(),
                            succeeded(Not(Contains(server_name))))

    def test_registration_email(self):
        """
        If we give our service an email address, that address will be used as a
        registration contact.
        """
        # First the case with no email given.
        with AcmeFixture() as fixture:
            fixture.service.startService()
            self.assertThat(
                fixture.service._regr,
                MatchesStructure(
                    body=MatchesStructure(key=Is(None), contact=Equals(()))))

        # Next, we give an email.
        with AcmeFixture(email=u'*****@*****.**') as fixture:
            fixture.service.startService()
            self.assertThat(
                fixture.service._regr,
                MatchesStructure(body=MatchesStructure(
                    key=Is(None),
                    contact=Equals((u'mailto:[email protected]', )))))