| # test mod_md stapling support |
| |
| import os |
| import time |
| from datetime import timedelta |
| import pytest |
| |
| from pyhttpd.depends import needs_dependency |
| from pyhttpd.certs import CertificateSpec |
| |
| from .md_conf import MDConf |
| from .md_env import MDTestEnv |
| |
| |
| @needs_dependency("ACME test server", MDTestEnv.has_acme_server(), |
| reason="no ACME test server configured", |
| detected=f"ACME={MDTestEnv.get_acme_server()}") |
| class TestStapling: |
| |
| @pytest.fixture(autouse=True, scope='class') |
| def _class_scope(self, env, acme): |
| acme.start(config='default') |
| env.check_acme() |
| env.clear_store() |
| domain = env.get_class_domain(self.__class__) |
| mdA = "a-" + domain |
| mdB = "b-" + domain |
| self.configure_httpd(env, [mdA, mdB]).install() |
| env.apache_stop() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| assert env.await_completion([mdA, mdB]) |
| env.check_md_complete(mdA) |
| env.check_md_complete(mdB) |
| |
| @pytest.fixture(autouse=True, scope='function') |
| def _method_scope(self, env, request): |
| self.domain = env.get_class_domain(self.__class__) |
| self.mdA = "a-" + self.domain |
| self.mdB = "b-" + self.domain |
| yield |
| env.apache_stop() |
| |
| def configure_httpd(self, env, domains=None, add_lines="", ssl_stapling=False): |
| if not isinstance(domains, list): |
| domains = [domains] if domains else [] |
| conf = MDConf(env) |
| conf.add(""" |
| <IfModule tls_module> |
| LogLevel tls:trace4 |
| </IfModule> |
| <IfModule ssl_module> |
| LogLevel ssl:trace4 |
| </IfModule> |
| """) |
| if ssl_stapling: |
| conf.add(""" |
| <IfModule ssl_module> |
| SSLUseStapling On |
| SSLStaplingCache shmcb:stapling_cache(128000) |
| </IfModule> |
| """) |
| conf.add(add_lines) |
| for domain in domains: |
| conf.add_md([domain]) |
| conf.add_vhost(domain) |
| return conf |
| |
| # MD with stapling on/off and mod_ssl stapling off |
| # expect to only see stapling response when MD stapling is on |
| def test_md_801_001(self, env): |
| md = self.mdA |
| self.configure_httpd(env, md).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.get_ocsp_status(md) |
| if MDTestEnv.lacks_ocsp(): |
| assert 'ocsp' not in stat or stat['ocsp'] == "no response sent", f'{stat}' |
| else: |
| assert stat['ocsp'] == "no response sent" |
| stat = env.get_md_status(md) |
| assert not stat["stapling"] |
| # |
| # turn stapling on, wait for it to appear in connections |
| self.configure_httpd(env, md, """ |
| MDStapling on |
| LogLevel md:trace5 |
| """).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| if MDTestEnv.lacks_ocsp(): |
| stat = env.get_md_status(md) |
| assert 'ocsp' not in stat or stat['ocsp'] == "no response sent", f'{stat}' |
| else: |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| stat = env.get_md_status(md) |
| if MDTestEnv.lacks_ocsp(): |
| assert stat["stapling"] |
| pkey = 'rsa' |
| assert 'ocsp' not in stat["cert"][pkey], f'{stat}' |
| else: |
| assert stat["stapling"] |
| pkey = 'rsa' |
| assert stat["cert"][pkey]["ocsp"]["status"] == "good" |
| assert stat["cert"][pkey]["ocsp"]["valid"] |
| # |
| # turn stapling off (explicitly) again, should disappear |
| self.configure_httpd(env, md, "MDStapling off").install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.get_ocsp_status(md) |
| if MDTestEnv.lacks_ocsp(): |
| assert 'ocsp' not in stat or stat['ocsp'] == "no response sent", f'{stat}' |
| else: |
| assert stat['ocsp'] == "no response sent" |
| stat = env.get_md_status(md) |
| assert not stat["stapling"] |
| |
| # MD with stapling on/off and mod_ssl stapling on |
| # expect to see stapling response in all cases |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_002(self, env): |
| md = self.mdA |
| self.configure_httpd(env, md, ssl_stapling=True).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.get_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" if \ |
| env.ssl_module == "mod_ssl" else "no response sent" |
| stat = env.get_md_status(md) |
| assert not stat["stapling"] |
| # |
| # turn stapling on, wait for it to appear in connections |
| self.configure_httpd(env, md, "MDStapling on", ssl_stapling=True).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| stat = env.get_md_status(md) |
| assert stat["stapling"] |
| pkey = 'rsa' |
| assert stat["cert"][pkey]["ocsp"]["status"] == "good" |
| assert stat["cert"][pkey]["ocsp"]["valid"] |
| # |
| # turn stapling off (explicitly) again, should disappear |
| self.configure_httpd(env, md, "MDStapling off", ssl_stapling=True).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.get_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" if \ |
| env.ssl_module == "mod_ssl" else "no response sent" |
| stat = env.get_md_status(md) |
| assert not stat["stapling"] |
| |
| # 2 MDs, one with md stapling on, one with default (off) |
| def test_md_801_003(self, env): |
| md_a = self.mdA |
| md_b = self.mdB |
| conf = self.configure_httpd(env) |
| conf.add(""" |
| <MDomain %s> |
| MDStapling on |
| </MDomain> |
| <MDomain %s> |
| </MDomain> |
| """ % (md_a, md_b)) |
| conf.add_vhost(md_a) |
| conf.add_vhost(md_b) |
| conf.install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| # mdA has stapling |
| if MDTestEnv.lacks_ocsp(): |
| stat = env.get_ocsp_status(md_a) |
| assert 'ocsp' not in stat or stat['ocsp'] == "no response sent", f'{stat}' |
| else: |
| stat = env.await_ocsp_status(md_a) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| stat = env.get_md_status(md_a) |
| assert stat["stapling"] |
| if not MDTestEnv.lacks_ocsp(): |
| pkey = 'rsa' |
| assert stat["cert"][pkey]["ocsp"]["status"] == "good" |
| assert stat["cert"][pkey]["ocsp"]["valid"] |
| # mdB has no stapling |
| if not MDTestEnv.lacks_ocsp(): |
| stat = env.get_ocsp_status(md_b) |
| assert stat['ocsp'] == "no response sent" |
| stat = env.get_md_status(md_b) |
| assert not stat["stapling"] |
| |
| # 2 MDs, md stapling on+off, ssl stapling on |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_004(self, env): |
| md_a = self.mdA |
| md_b = self.mdB |
| conf = self.configure_httpd(env, ssl_stapling=True) |
| conf.add(""" |
| <MDomain %s> |
| MDStapling on |
| </MDomain> |
| <MDomain %s> |
| </MDomain> |
| """ % (md_a, md_b)) |
| conf.add_vhost(md_a) |
| conf.add_vhost(md_b) |
| conf.install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| # mdA has stapling |
| stat = env.await_ocsp_status(md_a) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| stat = env.get_md_status(md_a) |
| assert stat["stapling"] |
| pkey = 'rsa' |
| assert stat["cert"][pkey]["ocsp"]["status"] == "good" |
| assert stat["cert"][pkey]["ocsp"]["valid"] |
| # mdB has no md stapling, but mod_ssl kicks in |
| stat = env.get_ocsp_status(md_b) |
| assert stat['ocsp'] == "successful (0x0)" if \ |
| env.ssl_module == "mod_ssl" else "no response sent" |
| stat = env.get_md_status(md_b) |
| assert not stat["stapling"] |
| |
| # MD, check that restart leaves response unchanged, reconfigure keep interval, |
| # should remove the file on restart and get a new one |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_005(self, env): |
| # TODO: mod_watchdog seems to have problems sometimes with fast restarts |
| # turn stapling on, wait for it to appear in connections |
| md = self.mdA |
| self.configure_httpd(env, md, "MDStapling on").install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| # fine the file where the ocsp response is stored |
| dirpath = os.path.join(env.store_dir, 'ocsp', md) |
| files = os.listdir(dirpath) |
| ocsp_file = None |
| for name in files: |
| if name.startswith("ocsp-"): |
| ocsp_file = os.path.join(dirpath, name) |
| assert ocsp_file |
| mtime1 = os.path.getmtime(ocsp_file) |
| # wait a sec, restart and check that file does not change |
| time.sleep(1) |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| mtime2 = os.path.getmtime(ocsp_file) |
| assert mtime1 == mtime2 |
| # configure a keep time of 1 second, restart, the file is gone |
| # (which is a side effec that we load it before the cleanup removes it. |
| # since it was valid, no new one needed fetching |
| self.configure_httpd(env, md, """ |
| MDStapling on |
| MDStaplingKeepResponse 1s |
| """).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert not os.path.exists(ocsp_file) |
| # if we restart again, a new file needs to appear |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| mtime3 = os.path.getmtime(ocsp_file) |
| assert mtime1 != mtime3 |
| |
| # MD, check that stapling renew window works. Set a large window |
| # that causes response to be retrieved all the time. |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_006(self, env): |
| # turn stapling on, wait for it to appear in connections |
| md = self.mdA |
| self.configure_httpd(env, md, "MDStapling on").install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| # fine the file where the ocsp response is stored |
| dirpath = os.path.join(env.store_dir, 'ocsp', md) |
| files = os.listdir(dirpath) |
| ocsp_file = None |
| for name in files: |
| if name.startswith("ocsp-"): |
| ocsp_file = os.path.join(dirpath, name) |
| assert ocsp_file |
| mtime1 = os.path.getmtime(ocsp_file) |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| # wait a sec, restart and check that file does not change |
| time.sleep(1) |
| mtime2 = os.path.getmtime(ocsp_file) |
| assert mtime1 == mtime2 |
| # configure a renew window of 10 days, restart, larger than any life time. |
| self.configure_httpd(env, md, """ |
| MDStapling on |
| MDStaplingRenewWindow 10d |
| """).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| # wait a sec, restart and check that file does change |
| time.sleep(1) |
| mtime3 = os.path.getmtime(ocsp_file) |
| assert mtime1 != mtime3 |
| |
| # MD, make a MDomain with static files, check that stapling works |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_007(self, env): |
| # turn stapling on, wait for it to appear in connections |
| md = self.mdA |
| conf = self.configure_httpd(env) |
| conf.add(""" |
| <MDomain %s> |
| MDCertificateKeyFile %s |
| MDCertificateFile %s |
| MDStapling on |
| </MDomain> |
| """ % (md, env.store_domain_file(md, 'privkey.pem'), |
| env.store_domain_file(md, 'pubcert.pem'))) |
| conf.add_vhost(md) |
| conf.install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| # fine the file where the ocsp response is stored |
| dirpath = os.path.join(env.store_dir, 'ocsp', md) |
| files = os.listdir(dirpath) |
| ocsp_file = None |
| for name in files: |
| if name.startswith("ocsp-"): |
| ocsp_file = os.path.join(dirpath, name) |
| assert ocsp_file |
| |
| # Use certificate files in direct config, check that stapling works |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_008(self, env): |
| # turn stapling on, wait for it to appear in connections |
| md = self.mdA |
| conf = self.configure_httpd(env) |
| conf.add("MDStapling on") |
| conf.start_vhost(md) |
| conf.add_certificate(env.store_domain_file(md, 'pubcert.pem'), |
| env.store_domain_file(md, 'privkey.pem')) |
| conf.end_vhost() |
| conf.install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.await_ocsp_status(md) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |
| # fine the file where the ocsp response is stored |
| dirpath = os.path.join(env.store_dir, 'ocsp', 'other') |
| files = os.listdir(dirpath) |
| ocsp_file = None |
| for name in files: |
| if name.startswith("ocsp-"): |
| ocsp_file = os.path.join(dirpath, name) |
| assert ocsp_file |
| |
| # Turn on stapling for a certificate without OCSP responder and issuer |
| # (certificates without issuer prevent mod_ssl asking around for stapling) |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_009(self, env): |
| md = self.mdA |
| domains = [md] |
| testpath = os.path.join(env.gen_dir, 'test_801_009') |
| env.mkpath(testpath) |
| # cert that is 30 more days valid |
| creds = env.create_self_signed_cert(CertificateSpec(domains=domains), |
| valid_from=timedelta(days=-60), |
| valid_to=timedelta(days=30), |
| serial=801009) |
| cert_file = os.path.join(testpath, 'pubcert.pem') |
| pkey_file = os.path.join(testpath, 'privkey.pem') |
| creds.save_cert_pem(cert_file) |
| creds.save_pkey_pem(pkey_file) |
| conf = MDConf(env) |
| conf.start_md(domains) |
| conf.add("MDCertificateFile %s" % cert_file) |
| conf.add("MDCertificateKeyFile %s" % pkey_file) |
| conf.add("MDStapling on") |
| conf.end_md() |
| conf.add_vhost(md) |
| conf.install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| time.sleep(1) |
| stat = env.get_ocsp_status(md) |
| assert stat['ocsp'] == "no response sent" |
| |
| # Turn on stapling for an MDomain not used in any virtualhost |
| # There was a crash in server-status in this case |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_010(self, env): |
| env.clear_ocsp_store() |
| md = self.mdA |
| domains = [md] |
| conf = MDConf(env) |
| conf.start_md(domains) |
| conf.add("MDStapling on") |
| conf.end_md() |
| conf.install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| stat = env.get_server_status() |
| assert stat |
| |
| # add 7 mdomains that need OCSP stapling, once activated |
| # we use at max 6 connections against the same OCSP responder and |
| # this triggers our use of curl_multi_perform with iterative |
| # scheduling. |
| # This checks the mistaken assert() reported in |
| # <https://bz.apache.org/bugzilla/show_bug.cgi?id=65567> |
| @needs_dependency("OCSP responder", not MDTestEnv.lacks_ocsp(), |
| reason="no OCSP responder", |
| detected=f"ACME={MDTestEnv.get_acme_server()} serves no OCSP") |
| def test_md_801_011(self, env): |
| domains = [ f'test-801-011-{i}-{env.DOMAIN_SUFFIX}' for i in range(7)] |
| self.configure_httpd(env, domains, """ |
| MDStapling on |
| LogLevel md:trace2 ssl:warn |
| """).install() |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| assert env.await_completion(domains, restart=False, timeout=120) |
| assert env.apache_restart() == 0, f'{env.apachectl_stderr}' |
| # now the certs are installed and ocsp will be retrieved |
| time.sleep(1) |
| for domain in domains: |
| stat = env.await_ocsp_status(domain) |
| assert stat['ocsp'] == "successful (0x0)" |
| assert stat['verify'] == "0 (ok)" |