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)))))
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') ]))))
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))
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))
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)))))
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]', )))))