Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/certbot/certbot.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'certbot-ci/certbot_integration_tests')
-rw-r--r--certbot-ci/certbot_integration_tests/.coveragerc9
-rw-r--r--certbot-ci/certbot_integration_tests/__init__.py1
-rw-r--r--certbot-ci/certbot_integration_tests/assets/cert.pem32
-rwxr-xr-xcertbot-ci/certbot_integration_tests/assets/hook.py11
-rw-r--r--certbot-ci/certbot_integration_tests/assets/key.pem52
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json1
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json1
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json1
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/cert1.pem29
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/chain1.pem27
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/fullchain1.pem56
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/privkey1.pem28
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/cert1.pem29
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/chain1.pem27
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/fullchain1.pem56
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/privkey1.pem28
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/csr/0000_csr-certbot.pem16
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/csr/0001_csr-certbot.pem16
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/csr/0002_csr-certbot.pem17
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/csr/0003_csr-certbot.pem17
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/keys/0000_key-certbot.pem28
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/keys/0001_key-certbot.pem28
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/keys/0002_key-certbot.pem28
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/keys/0003_key-certbot.pem28
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/README10
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/cert.pem1
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/chain.pem1
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/fullchain.pem1
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/privkey.pem1
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/README10
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/cert.pem1
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/chain.pem1
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/fullchain.pem1
l---------certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/privkey.pem1
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/options-ssl-apache.conf22
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/renewal/a.encryption-example.com.conf15
-rw-r--r--certbot-ci/certbot_integration_tests/assets/sample-config/renewal/b.encryption-example.com.conf15
-rw-r--r--certbot-ci/certbot_integration_tests/certbot_tests/__init__.py5
-rw-r--r--certbot-ci/certbot_integration_tests/certbot_tests/assertions.py161
-rw-r--r--certbot-ci/certbot_integration_tests/certbot_tests/context.py83
-rw-r--r--certbot-ci/certbot_integration_tests/certbot_tests/test_main.py613
-rw-r--r--certbot-ci/certbot_integration_tests/conftest.py96
-rw-r--r--certbot-ci/certbot_integration_tests/nginx_tests/__init__.py0
-rw-r--r--certbot-ci/certbot_integration_tests/nginx_tests/context.py62
-rw-r--r--certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py126
-rw-r--r--certbot-ci/certbot_integration_tests/nginx_tests/test_main.py54
-rw-r--r--certbot-ci/certbot_integration_tests/utils/__init__.py0
-rwxr-xr-xcertbot-ci/certbot_integration_tests/utils/acme_server.py223
-rwxr-xr-xcertbot-ci/certbot_integration_tests/utils/certbot_call.py139
-rw-r--r--certbot-ci/certbot_integration_tests/utils/constants.py9
-rw-r--r--certbot-ci/certbot_integration_tests/utils/misc.py302
-rw-r--r--certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py53
-rwxr-xr-xcertbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py72
-rw-r--r--certbot-ci/certbot_integration_tests/utils/proxy.py36
54 files changed, 2680 insertions, 0 deletions
diff --git a/certbot-ci/certbot_integration_tests/.coveragerc b/certbot-ci/certbot_integration_tests/.coveragerc
new file mode 100644
index 000000000..72f7c6adf
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/.coveragerc
@@ -0,0 +1,9 @@
+[run]
+# Avoid false warnings because certbot packages are not installed in the thread that executes
+# the coverage: indeed, certbot is launched as a CLI from a subprocess.
+disable_warnings = module-not-imported,no-data-collected
+omit = **/*_test.py,**/tests/*,**/dns_common*,**/certbot_nginx/_internal/parser_obj.py
+
+[report]
+# Exclude unit tests in coverage during integration tests.
+omit = **/*_test.py,**/tests/*,**/dns_common*,**/certbot_nginx/_internal/parser_obj.py
diff --git a/certbot-ci/certbot_integration_tests/__init__.py b/certbot-ci/certbot_integration_tests/__init__.py
new file mode 100644
index 000000000..434a85a23
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/__init__.py
@@ -0,0 +1 @@
+"""Package certbot_integration_test is for tests that require a live acme ca server instance"""
diff --git a/certbot-ci/certbot_integration_tests/assets/cert.pem b/certbot-ci/certbot_integration_tests/assets/cert.pem
new file mode 100644
index 000000000..5aae58f25
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/cert.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFlTCCA32gAwIBAgIUR3wbM8qFE68f8NxfciHhUjR1GeUwDQYJKoZIhvcNAQEL
+BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbmdpbngud3RmMCAX
+DTE5MDQxODIwMDUwM1oYDzIyOTMwMTMwMjAwNTAzWjBZMQswCQYDVQQGEwJBVTET
+MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
+dHkgTHRkMRIwEAYDVQQDDAluZ2lueC53dGYwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQC/W+yxYE0PWJOS4df71Yx596fDjW03I9JZuu9kfP7mneMgy+OC
+HyRm0TEhl6FPUp9tD9YeEHloUZNjHEOg/qrnbEOspv3Ha3RFinzrzkMwbzEPR3Xf
+0go+aVsWelDhapFl8fccw4tWwijVZQquhBsWOUnPenS3Txe96kEv2NNJlJ0qFUa+
+rOTruzRzOzlbgKv5WRb4+BxxWonHLkAQ5IT87GBlsCerVIyPD+BnZveZGl6e9oMH
+ZlZvUT6aWRnzFWjAnQGiJpVIw7l9r4EW0jq1z7wqb37FrqrFbtWrOfUZVE7AlqXH
+aKIR82/xwkcZfFk3sCAM0IcZc8B2SDLi4zNZtDivW6qQgTC/3z5yf1hnJ+j00dtE
+X5qYlgXRaM2raOn31lxcerk5pjgagQ7Zj+v3YZS0QnenrgyXJcdnXLDj+cIARzx4
+QHtoO0nyP0RJqxvwX/H98513JTkeqFBc/Bx11UWYsUv20Qoo9IAuz0VDARu6rquu
+k9anv56yvxo77qZ8r80l3z8eMyDA+UjuSD2p1Za09RAHfva7o8rMUqULHNQ4pfFH
+JIUozHoinAg/9lBC/W80fcbILks+Sdi6E9WQ0n8PLl7oFLx9prEDCycKuC0z76J/
+Shb6R6sWr1YtzUFUc5EH2g9pMriaqT8uGO4CMOeRemXahrdT/H+Xg5m4TQIDAQAB
+o1MwUTAdBgNVHQ4EFgQU46gJeu9ZOfTQ6c4vfbWbSLUpEMowHwYDVR0jBBgwFoAU
+46gJeu9ZOfTQ6c4vfbWbSLUpEMowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
+AQsFAAOCAgEAcnfkXDUTsEGs0MleegkGbRCVy72a3U7tv1KVTLB8qLPc3tpPJJoT
+D4PbOuw9+yIE+HetZTZooOpaZoorLQdiwAEjlQ44RVuXSHSARQ8KW9ZZeiWN/Qvl
+Ip4xJ/cHxcKTFKSc/99o8M+kmPKEXF9SUMfKPc5jXarNxCsnA3VriYqJ+CnYEox2
+duNUEe3A9Y2d8ZxjmscBqlcXpk1kFwsCRT5UYVoUYwyjYznLkO5A+GJ0ZnMyRMQp
+obUiB34hUrNgyOaBvizk+pNh9EV4rEBPRQwhy4vDMco4AjQcwLWQAQ9G4GSt/E+Q
+62XdVDa6CAuOvBCudDPki7kEqNLbj1tMY1K/gsbgb6TYA/xTOVulAnqm4OEZ2svJ
+0Jqw3BzMfRTaxsNU6jxm8WehVL15GjoJUzfs7Te+l7Vm/QNc1Dv2pmEhVfBibwMa
+YxUZ8ClQtQ1lsOpne97Og0p/Cm93kKELNBLTjzXtpXGGPPYisAyNwe0Hadq8SiOd
+pXeNwXa5vHOXHv8xBENzBvFJ3TRN2GmMlHBp/eOfVUx/huNSpcnh2gO3fn5EbMj7
+43IaR133JW5yWbneYAMJOEAMdEB5EthRmEDtLVA7kLqLc/ywFTQ4VbS2b+PsOr5O
+501nzt0OTMMEz+UafvGXj5OPJBhe26RtnYXzVwwLfto/F5udM5zglWo=
+-----END CERTIFICATE-----
diff --git a/certbot-ci/certbot_integration_tests/assets/hook.py b/certbot-ci/certbot_integration_tests/assets/hook.py
new file mode 100755
index 000000000..39aa72ac5
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/hook.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+import os
+import sys
+
+hook_script_type = os.path.basename(os.path.dirname(sys.argv[1]))
+if hook_script_type == 'deploy' and ('RENEWED_DOMAINS' not in os.environ or 'RENEWED_LINEAGE' not in os.environ):
+ sys.stderr.write('Environment variables not properly set!\n')
+ sys.exit(1)
+
+with open(sys.argv[2], 'a') as file_h:
+ file_h.write(hook_script_type + '\n')
diff --git a/certbot-ci/certbot_integration_tests/assets/key.pem b/certbot-ci/certbot_integration_tests/assets/key.pem
new file mode 100644
index 000000000..1f768f079
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC/W+yxYE0PWJOS
+4df71Yx596fDjW03I9JZuu9kfP7mneMgy+OCHyRm0TEhl6FPUp9tD9YeEHloUZNj
+HEOg/qrnbEOspv3Ha3RFinzrzkMwbzEPR3Xf0go+aVsWelDhapFl8fccw4tWwijV
+ZQquhBsWOUnPenS3Txe96kEv2NNJlJ0qFUa+rOTruzRzOzlbgKv5WRb4+BxxWonH
+LkAQ5IT87GBlsCerVIyPD+BnZveZGl6e9oMHZlZvUT6aWRnzFWjAnQGiJpVIw7l9
+r4EW0jq1z7wqb37FrqrFbtWrOfUZVE7AlqXHaKIR82/xwkcZfFk3sCAM0IcZc8B2
+SDLi4zNZtDivW6qQgTC/3z5yf1hnJ+j00dtEX5qYlgXRaM2raOn31lxcerk5pjga
+gQ7Zj+v3YZS0QnenrgyXJcdnXLDj+cIARzx4QHtoO0nyP0RJqxvwX/H98513JTke
+qFBc/Bx11UWYsUv20Qoo9IAuz0VDARu6rquuk9anv56yvxo77qZ8r80l3z8eMyDA
++UjuSD2p1Za09RAHfva7o8rMUqULHNQ4pfFHJIUozHoinAg/9lBC/W80fcbILks+
+Sdi6E9WQ0n8PLl7oFLx9prEDCycKuC0z76J/Shb6R6sWr1YtzUFUc5EH2g9pMria
+qT8uGO4CMOeRemXahrdT/H+Xg5m4TQIDAQABAoICAAGGL+pxw+tdXz+KQPgmiUnn
+aRSrqbUIugIw9Pst67HWjBqUxSkiKl4PSH7mAEjrdY2e1KvEodLs42mkrf04ShAx
+0pArfFX8Sx7KrZgLOonGOPPQM+YmfCJnIGybaM2C1cmkFb3K6O81+LFKbr1ZHAYf
+SrE2XnufS6cdmItTBMvPPTk6lieqpOAjy5UnYZuS+Muxo/czsrZMbFCD08rOpyiE
+kXf94TMCJ2R0UetA7LPxe9N0TzLd485bLU55azV+dCkklwC9oe7EcFPJ9BNEdWdB
+UlRcMvxMGdwct+L3QTaEb2QlTwi5kqDl+XxJeduAHA3Pf1Haz1iqjVvj01PvT1di
+Cs0+ZeFBsa+BfiGDe9ONwuSQljda1CuI+vDv5bGUExulOSG1dHJ7RK9PBaXFaR/b
+/9tRBwAw1Erm7s1JIkjda5Oc46jFb3HzDaZYB1n5hUmEIrYM8HhUOGITyVT3hxDO
+AWlaV3aveQ0MmMXLptVXDgbjPGbWDGMLD9d5vUE9R7IyOLeXOmjthYlCH2rj378M
+r2PkgX2tD0A/yoEZ8XCFdtBWSVajLdL0/gkm7sKosMABBy3yrSCxbHeq5TFuTAXA
+hOdypX4NOZkA6WJU+hn3GkQyIScLqSrvGRA9kzHGoEWVZDKkB9DXg+dmTARZDWXD
+mCnHkJo6+FcbhUpXniuZAoIBAQDmE94vvdstB+HEtXxN1uNDY7H8gPc/BUonU6a9
+G5YOIbjByCfEDcXF8AUWekc6lc8DNG3ydx0dnb2ZAkxmdlsaD8GLqHGILzlSsOwR
+sez8nR4+4n9vYMfx9Qal8Ren5xEP9Z9sJcNqbKVGta1WFtQzrgYbpVXXf/Luv0xS
+YoVK8KaEACciD6XX4wmajrAXPPQgThvqQtXuTn/AxWsUDg1DK0tw1VRUuOJuJwpw
+f6qocM9AyqUNvdeVyjFx8Slag34ZI7fmxPtHX/e6opTg3zVXab1Ow8AMICOHMRL6
+m5/+wnWa9xMoKI4kYfk/QFqeTccnLDlwi6kQM8WRfbwr9AyPAoIBAQDU60wrX6Lm
+0vIfngv1/4j/w+AGAwjvxiuJ7Q7LwQ2fGsZGOIfMK/lpBxCn543kGbQT+KQKNOjO
++EywObftnJ6Y2+om2NoLkCnCiptsfr5WlN8pxtIPQu2iu5xXA67WpQv4Nc4769PM
+wJGVW3pmPKi6H0QjjqYAZd1NAXdN9Au14zZVh3KBWoz82kTHWKSL6Ld1UClG728W
+k/moyCFFMMGTXX/LVliQzDVLM6L5jbAOaG317qAuxZIqFJ9NLwHFW9uH/i1S6Qfp
++lOmOfVYKu1O/qh1DUBQfuJkR1XIn2ifZEjxOsxeTmWu1LXpyoZy526JRu49pk8Z
+DdEu+w7hsdNjAoIBAD1YWsub8Y6GJXpPcX9HpnzXXiOXN1VEUcs+kJyneFD4SMzS
+U1gA3BS0tIaTv94tB28xUYdunwLAhkb/x+Mh95RxUwert+m5va0Ao1DsgeWw9tmJ
+hrTptyYaUNV5/Pa1s2Tv9rvdLcd4hHDgDAGCQL4uzk4cvVCiOuHRe8YTorqig6N6
+bvSz+2IelPbyyJzJkcXzTZoei+/oWkPJ340PWhXou0qwdrXIPgdkvXHVeGlE+t2p
+qmyJi6vSp3Bb/sy1dq+5SFVtfBpBykmnA88ZdJ2EAge4RcJ150MqoIbVa8l/i9/v
+tNnmRlAJF233+LFwx4L4VbBebIt3YlwyjDOj9J0CggEAIknKOGnsV/O8ni7bikAe
+leG7X/x5IfPt6wZMDbAHO4oaSBCufcjPH4TNv9xgU014XIb8E9C1dS8zWmXRIujH
++aHgsWTWqGoM75FWukAm8taCob2s8lw63KwN301uiI6HwO8ZSTkPILgaOc1DhtdZ
+7K9AT+GXBhVhcBc+WUVl5WKzy05GuGIWtlmIHfo+dXGCqdfA7fV9FEu8NtwTz4qs
+gcja3aoIFTltk7C7HCkfIxLaMnK9RQr4IOK1TL63MEs8rUfXkLSKW7m+YtSOmCZB
+lSkZg9AgfVYRq0h5nhddx91kicSISN+jLGaA7Sd6Q2LVwDG2CCOSNVyuRTyVBu+W
+NQKCAQAWN6vB6oToNIoBLdOThm0HD07cNHcrnBjtaKsYsQDgqbr2m8LRCRzNRML4
+jG0IAOWpuCiEGsgUPxywiI1Ufvyq7ZSNT1QQNzCR47NM3Ve6S2abrQkMIk9VJ+za
+CB9c1BH92GokoRxqswb/BiMttG2EIP8L8/pSRYEcVnaaxAkf9QOhEwj4LJPGX0mS
+t7kWIUVHPdFJ67F25dYr3mUHgyV+QJupQICkkkgY3nBOU1fS42vAugaxqH0wAP3T
+53FlpY3NuE7+kYC3FjfcBer99F1pOac3X9jxhk26w9dr2/QNA33xhDXHKYvoLUCG
+RPQylahJByU7IrtQzSCf/RE7q4v0
+-----END PRIVATE KEY-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json b/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json
new file mode 100644
index 000000000..6fe0b47f3
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json
@@ -0,0 +1 @@
+{"creation_host": "ec2-52-91-193-99.compute-1.amazonaws.com", "creation_dt": "2016-12-23T02:08:32Z"} \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json b/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json
new file mode 100644
index 000000000..0affb573d
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json
@@ -0,0 +1 @@
+{"e": "AQAB", "d": "W410Wny96RO4qJ207KGQ3RSn0KAwqb93JBMHWU1yS9H3fN_2eCpFYdMLNFI9t1__nW1okeUioEfvMN_YW-G9krw97kVdZ63MfbeJCf35Onc8VZhAnk_3V8MtS26Of8ml0tTYhlQ65nuzhvHbY7aP-Uk260oDN-AbCCVhu5G4CQiMY6sdtCc8YkB6gK7SK874oWU7ogvAIPtNtEI-AXDUBYNAfoh34s1r2fE6mJSX4UYtzWB2hTUisvZdVL5JUInvxpCQFttk1cwWLFwwb6d2ERCbseeudvGJ6fkYiJ-EYxfHKOQK2kxPeOlLFMwGYQ0khDxTNajxQ1Asl43r7wgAeQ", "n": "xL5HzdhU_7P-_tphpRxpDSIL2L-aAlWt6r9EVyw53Sp-jx4fHDgnYv9HQOzNeL_IpLRCLLBItMzqnBvHUdHcS3aB6fv8HSNiHdVdC-c2rPFO8DLSGLNqi9G9WshjLDsKwc__BPNX5wHFcm8TZUJ4uZ_Ax1JCe05ePHWAf8GTr8vPaKtMpUVF55HPwpJtYvFZlH1LiVo8I_trJtHl8-pGeel3zdcaDJgNZrohZG2acTg95Ry46FE4HOslAg8Z6yECPyYLInJSDcb5yCgSqtOOp7rMVSPQFhoZRt4KDfew9lqIwNQSJoDE3bJWpwkzL1tp4clG8ExI1WnA86OjW83Vvw", "q": "0xdfHMMKYWHPE1UoQ10niDI7rnCM9vmPo4JpCOCYZf51KPNJgNaPCw62Q0Y-ZQfCBifypQyf291d0_2C_Rif0WMg07Y-Ypv8SpPK77vLV12GoAoAX2Xy3AJAz1gDBcyUzDtRlrzgCZja9YqIDVzMatkdPJXaBrBu5B-sXv4wGa0", "p": "7pl5xe_400Sn6PdN_F6KLWHFROVd7379WPWGHYmnvOvXx7DmrMjDsTOmhNRlrv7jPemVqMzp1FGsubGBizEMFGyCET30bUgH6ZU7Cmgv-2JKKN1FZnm1QTepZ7kjAT_qRCI6nvN6J0SIX197QOSz3hMmP7UYQXQ32QcVKdCksps", "kty": "RSA", "qi": "zG60VpLZjgR0o7dTeEP-HjbtxHUedyZLGe4FIPyWrPRl28anebkMUGzibpB8z5ohRsqHU2i4tmDq2NMvshISqkpk8t5PLiIcQgU46HQ24SCv7lunkVPKYU1n2uXVVfttrBP4c3UkjYzda1bcIVp6cJHanm_JuWI5nxy9ebVQJiw", "dp": "kRIBx0aj7Jh22x_aa9JzgypKDhzDY4W7tmX5-GWk9ioTVZgKeQ3MZiZ4XZTiimbxdchbNXn5xh0uvuzdTesxZA2he6hGwFcmcHBKqIY2fksBuhznQGpJuXCFcMpRLUZWQrzpFZIGOG_j1tEwGIG1lxXfkKakK8_k0PEMfhMcwHc", "dq": "AsoSRa0GHBdQxy6e45T9ir0vMLToB_NwRHbasHVXTjG4lpvwYrVzGnBNVEI_XNJna_FnMWsjSaJ5NO3qpzGGGxw2ONX1qRPql4mwas6Od08TElZPfvM37FRTSuoc0BzN8ozuHRHN3BKbAheciKCrStYnnr9ULDZ0oKsSegbd19k"} \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json b/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json
new file mode 100644
index 000000000..fdd2df7da
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json
@@ -0,0 +1 @@
+{"body": {"agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", "key": {"e": "AQAB", "kty": "RSA", "n": "xL5HzdhU_7P-_tphpRxpDSIL2L-aAlWt6r9EVyw53Sp-jx4fHDgnYv9HQOzNeL_IpLRCLLBItMzqnBvHUdHcS3aB6fv8HSNiHdVdC-c2rPFO8DLSGLNqi9G9WshjLDsKwc__BPNX5wHFcm8TZUJ4uZ_Ax1JCe05ePHWAf8GTr8vPaKtMpUVF55HPwpJtYvFZlH1LiVo8I_trJtHl8-pGeel3zdcaDJgNZrohZG2acTg95Ry46FE4HOslAg8Z6yECPyYLInJSDcb5yCgSqtOOp7rMVSPQFhoZRt4KDfew9lqIwNQSJoDE3bJWpwkzL1tp4clG8ExI1WnA86OjW83Vvw"}}, "uri": "https://acme-staging.api.letsencrypt.org/acme/reg/566631", "new_authzr_uri": "https://acme-staging.api.letsencrypt.org/acme/new-authz", "terms_of_service": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"} \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/cert1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/cert1.pem
new file mode 100644
index 000000000..80739dd3f
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/cert1.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPrA8hxQOlpVRMgAm/Ib0HYdqzANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTAyMDBaFw0xNzAzMjMwMTAyMDBaMCMxITAfBgNVBAMTGGEuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqz0cco
+hsCqyWPwGr79a8j+JO3HqbphLTzhoNHYF+fW8glyMyBmOMyZjc8v8E3U3KYEXuuR
+WzR+bvUXBcLOhSogIifZDNiMKEFyDNcDlG08ze9GTj2hTQyjet2ZuPWNuuJ4u5UM
+FvobaceDqITuqEqUrjCBi5CmEXswrV3l2BVSiOcPf+l+ZR81xG7qcjGfLG6YQWca
+nsYYorz/kSRtwYjAT4NaeUYNXVeH1luWTWhbed8pmKfBVfv+OEmwUyAhSE1ePfny
+Cj37wo1+nqQz37IJNEpI0RNbxrE7ZCgA40QrFVqc9XevcypFi9DftVWzDNBtd97Q
+lmHuIqA9Kb3C/e8CAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+C7/XcCnNRht91hnQVEB2E9AtNUowHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhhLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEAP04z87VVNYYHpBkCLkw3B+gTd/F0xDo7ab2HvJJAeOpZgSfoSYMR
+omYWiug9wGQqKjs4kaOGjAkW1EV3qosumOtvK7uTvoa2caXDjPYAxRiVIp08Qm0J
+/FU/FfGpUXBZW9Ne3m3nDYxOCAWAw9WmV+dUuvb7qZWQSKs7cQv3FY/NuQe0o9LH
+FgL7T0W7vc6uVGeBgcoEkX7xX4T7A9V3BqL6mgkK+L++n0EFrDXXzWWENNdWYCvY
+Ptu0Ez95IyYNRgI3U1waO9QZ944Pc9OuMCZD4ifbYoMKGqSQb3sGR+B2TQ+qqCUC
+4sikdX4WRbEYKlBTcvSpCVJ7ndFTyD6lyg==
+-----END CERTIFICATE-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/chain1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/chain1.pem
new file mode 100644
index 000000000..29a54e2a1
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/chain1.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/fullchain1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/fullchain1.pem
new file mode 100644
index 000000000..ba245d213
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/fullchain1.pem
@@ -0,0 +1,56 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPrA8hxQOlpVRMgAm/Ib0HYdqzANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTAyMDBaFw0xNzAzMjMwMTAyMDBaMCMxITAfBgNVBAMTGGEuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqz0cco
+hsCqyWPwGr79a8j+JO3HqbphLTzhoNHYF+fW8glyMyBmOMyZjc8v8E3U3KYEXuuR
+WzR+bvUXBcLOhSogIifZDNiMKEFyDNcDlG08ze9GTj2hTQyjet2ZuPWNuuJ4u5UM
+FvobaceDqITuqEqUrjCBi5CmEXswrV3l2BVSiOcPf+l+ZR81xG7qcjGfLG6YQWca
+nsYYorz/kSRtwYjAT4NaeUYNXVeH1luWTWhbed8pmKfBVfv+OEmwUyAhSE1ePfny
+Cj37wo1+nqQz37IJNEpI0RNbxrE7ZCgA40QrFVqc9XevcypFi9DftVWzDNBtd97Q
+lmHuIqA9Kb3C/e8CAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+C7/XcCnNRht91hnQVEB2E9AtNUowHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhhLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEAP04z87VVNYYHpBkCLkw3B+gTd/F0xDo7ab2HvJJAeOpZgSfoSYMR
+omYWiug9wGQqKjs4kaOGjAkW1EV3qosumOtvK7uTvoa2caXDjPYAxRiVIp08Qm0J
+/FU/FfGpUXBZW9Ne3m3nDYxOCAWAw9WmV+dUuvb7qZWQSKs7cQv3FY/NuQe0o9LH
+FgL7T0W7vc6uVGeBgcoEkX7xX4T7A9V3BqL6mgkK+L++n0EFrDXXzWWENNdWYCvY
+Ptu0Ez95IyYNRgI3U1waO9QZ944Pc9OuMCZD4ifbYoMKGqSQb3sGR+B2TQ+qqCUC
+4sikdX4WRbEYKlBTcvSpCVJ7ndFTyD6lyg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/privkey1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/privkey1.pem
new file mode 100644
index 000000000..b3059cb47
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/privkey1.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqs9HHKIbAqslj
+8Bq+/WvI/iTtx6m6YS084aDR2Bfn1vIJcjMgZjjMmY3PL/BN1NymBF7rkVs0fm71
+FwXCzoUqICIn2QzYjChBcgzXA5RtPM3vRk49oU0Mo3rdmbj1jbrieLuVDBb6G2nH
+g6iE7qhKlK4wgYuQphF7MK1d5dgVUojnD3/pfmUfNcRu6nIxnyxumEFnGp7GGKK8
+/5EkbcGIwE+DWnlGDV1Xh9Zblk1oW3nfKZinwVX7/jhJsFMgIUhNXj358go9+8KN
+fp6kM9+yCTRKSNETW8axO2QoAONEKxVanPV3r3MqRYvQ37VVswzQbXfe0JZh7iKg
+PSm9wv3vAgMBAAECggEAattP6Wz8FaWTlgTaqU44Z8R314VSQULNr7vKETJFnLKY
+JsOfL5vt2F4TQGxQ8Ffcm+xGgw4l2tF+odv8ljrzbzBYUTt06CWsmXNMiFhMVKlo
+fG01Uy0i71Ny+T9eYhCLuXM8cYv04jHA4M0Q8831+WHjPKgLdswOS2BoVkwoHQfc
+xEo40D0sPynd+KRukhgR+5AjwMdaNOV7S8c5iuQYIaZ1Xe5AyfiQkMV4LdbobMDj
+bHzGxdeC5GRVOHnMBYrRotgSt4+bsQGeoV9yWY0WAVvnoDfRBRdWK8yRVhuJY1+D
+WB6sPJ5cOg7Ijclubo9b+EaUkddvP0aCA3FepqNwcQKBgQDR0hz9OSom2fBjLaR2
+mQe3LqnotwPCuMmXuKndGIwJz9KgelBaRNUcvDtnzSzQVZ3h9/YFJKUkoVPVCoAu
+wAF9aBeDGs+LdHerBK8fI87PXwCV0OlZLQfUw1/82dpO/dyYXVeGorrO6FE/Oxb8
+enLerMW0Ocp/MhEgM5lFRUJM1wKBgQDQRauI9QuMoBnl516pOs+7EPRvTwe4oBpO
+iH2U7ryJ/YQTgsx25sDWqQBouEnv3j83wnVh9kApkS8UXFd4ZwuizIFCMlgrxw4x
+nKDsd1TZOLUO2FNi09YWPUnzxzQBOjBeekEIDKUQCLOKttTrjRHgGld3tmVtHWtL
+W+OvNIdcqQKBgCMpqjAJr3W5Wl7UnFY/yRo62MCmQxwT6bzidp0V6woN6Qd52BN4
+q5pYNUBtExCK+J2Q94rfHEnqO2ldjCPJi7ZfhmkzSgrd5twjOdHnJ1Z7Xla9Hw4R
+zNksMN7oB3zrcFecdPmcNeBM8Ki/F1gSkUOeArf0Y2ozkskpvIruU3EbAoGBAMVz
+h7CMQKrNjj/8Hi5qZ05+QH7Wegd7IfWaSRTNUUmxY2nr81Q2aFQaXRzquo4CMgT3
+Arog76t4zR2MfhDUAKATKehMOnMmgDpgt9/3MiXOMTkltchX9PuYl2faT19qfzjS
+xpyPAF43IaA8vZejYnMIBiyka3wLDBGhyDXuovYhAoGAB/AZnOM/4SQuIdtzmBSy
+YsHpXcNgRPqvfauCus3e5I6H4wmi+nqF/jyt0oyDBDKZki67CpStwu5Eo7tcLLnY
+o+VfJ9co8jUfVxRh0NlZwomF1t/8yAm/deWoV9sX9Yj71ft/eomCifNseeeg31Kl
+wkqKc3PndJHrR40mswUOHbs=
+-----END PRIVATE KEY-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/cert1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/cert1.pem
new file mode 100644
index 000000000..0c1c6b5ef
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/cert1.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPqBl0IgXf6F9LO/8sV1SsoA9DANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTA0MDBaFw0xNzAzMjMwMTA0MDBaMCMxITAfBgNVBAMTGGIuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWA6tWR
+FAfYyOEM9HtJXK4tCd1tGF2QZrlJHEL3PJzFHonv7ZaPo6Vkrar1uLinM4AVux/f
+s9vcsbdebu54DXpj1IllzjKs3tjStHK46luMqj8gf+3yLZIIVnN4YxkItd1WBtim
++144ku1gULsGnnHmuCefXz6qqkLzFZsElqO7NY+TL4F4m/L0lDjYsU++XgbHT9gi
+Tw0jAi8SyH8Ia4IYi4ynnMuHuS11e+yOtq16kLW1RdnxrYpleu9z0DU+6Xlr1tbl
+eSkyzbWelDgdsicfOxZz5pbmALXErb472TidcHHK6bsMVhR/P1zQK9Ydc+tC33d0
+XCRRgPoduN8XRfcCAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+RJ6J6HcpXRdRjqfyGshMEzkJy4cwHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhiLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEA2K8R+nSf9TmfSeUqB+ckObkf8bgyR0qKx/8fGoYGNAzKVE0KUs8u
+SDIITjbcTivEuSChycZAGQMEMZal8uT8GsFqqJUcEJUzuxbv7nvZkCSdal1PrRsw
+U4cBBuuZ/NvisEZCyjZe8mMdlhcSgThzqljF5Tcz3EWvaH9kxhqr8eL/6pYdAasT
+0HqirveIQUrf9LqEEAYGB3P6VI2kjroxUZif7dt2jvOGwJEJfHOjiC8rp0Db0hVZ
+omXSsZN6mVkbv1q0I7lgKWu1RHfNAefado3TJZHe8JJ5Oxrl3f2hxi3SzuPGgfXV
+ZdKb0zjDXhgumrp0F2eT9zltTIUr8alYcg==
+-----END CERTIFICATE-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/chain1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/chain1.pem
new file mode 100644
index 000000000..29a54e2a1
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/chain1.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/fullchain1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/fullchain1.pem
new file mode 100644
index 000000000..705cca6c3
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/fullchain1.pem
@@ -0,0 +1,56 @@
+-----BEGIN CERTIFICATE-----
+MIIE9TCCA92gAwIBAgITAPqBl0IgXf6F9LO/8sV1SsoA9DANBgkqhkiG9w0BAQsF
+ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw
+MTA0MDBaFw0xNzAzMjMwMTA0MDBaMCMxITAfBgNVBAMTGGIuZW5jcnlwdGlvbi1l
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWA6tWR
+FAfYyOEM9HtJXK4tCd1tGF2QZrlJHEL3PJzFHonv7ZaPo6Vkrar1uLinM4AVux/f
+s9vcsbdebu54DXpj1IllzjKs3tjStHK46luMqj8gf+3yLZIIVnN4YxkItd1WBtim
++144ku1gULsGnnHmuCefXz6qqkLzFZsElqO7NY+TL4F4m/L0lDjYsU++XgbHT9gi
+Tw0jAi8SyH8Ia4IYi4ynnMuHuS11e+yOtq16kLW1RdnxrYpleu9z0DU+6Xlr1tbl
+eSkyzbWelDgdsicfOxZz5pbmALXErb472TidcHHK6bsMVhR/P1zQK9Ydc+tC33d0
+XCRRgPoduN8XRfcCAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
+RJ6J6HcpXRdRjqfyGshMEzkJy4cwHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L
+IKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z
+dGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j
+ZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhiLmVu
+Y3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG
+CysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy
+eXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv
+bmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp
+biBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh
+dCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B
+AQsFAAOCAQEA2K8R+nSf9TmfSeUqB+ckObkf8bgyR0qKx/8fGoYGNAzKVE0KUs8u
+SDIITjbcTivEuSChycZAGQMEMZal8uT8GsFqqJUcEJUzuxbv7nvZkCSdal1PrRsw
+U4cBBuuZ/NvisEZCyjZe8mMdlhcSgThzqljF5Tcz3EWvaH9kxhqr8eL/6pYdAasT
+0HqirveIQUrf9LqEEAYGB3P6VI2kjroxUZif7dt2jvOGwJEJfHOjiC8rp0Db0hVZ
+omXSsZN6mVkbv1q0I7lgKWu1RHfNAefado3TJZHe8JJ5Oxrl3f2hxi3SzuPGgfXV
+ZdKb0zjDXhgumrp0F2eT9zltTIUr8alYcg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
+MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
+8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
+oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
+ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
+xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
+dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
+AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
+BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
+hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
+UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
+AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
+DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
+IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
+zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
+PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
+SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
+2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
+WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
+n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
+-----END CERTIFICATE-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/privkey1.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/privkey1.pem
new file mode 100644
index 000000000..c43af4f50
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/privkey1.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC1gOrVkRQH2Mjh
+DPR7SVyuLQndbRhdkGa5SRxC9zycxR6J7+2Wj6OlZK2q9bi4pzOAFbsf37Pb3LG3
+Xm7ueA16Y9SJZc4yrN7Y0rRyuOpbjKo/IH/t8i2SCFZzeGMZCLXdVgbYpvteOJLt
+YFC7Bp5x5rgnn18+qqpC8xWbBJajuzWPky+BeJvy9JQ42LFPvl4Gx0/YIk8NIwIv
+Esh/CGuCGIuMp5zLh7ktdXvsjratepC1tUXZ8a2KZXrvc9A1Pul5a9bW5XkpMs21
+npQ4HbInHzsWc+aW5gC1xK2+O9k4nXBxyum7DFYUfz9c0CvWHXPrQt93dFwkUYD6
+HbjfF0X3AgMBAAECggEAYjEnWnjNTF10d4Qps5UBxdzpzFfb6apYWH78AiJ9MRbX
+Kaqab2ywDKdF6Qpcb9FM5EtdW6YLSLPBlUFKZEqgiAkAD4D7J6EsQkLjinkNmI+l
+/tbXPuRY0PsfwgJsIjv7H44N0CGuNdAHdNI5eqTfDSHTmOP4hA+SYvvdQWsfD94r
+m4ocr2YfL4BmEh3hujb8NjVD8csSnFlpeVibtJ1rWiv1otLaEuVmcN49n0rIj0IK
+tiCIdqqIscVZ+P3fFfr/E3oL2nhBqxRnzqoK/HNTpI4JJAbRGP51nVr0QhZYpIuj
+xDM+zeuIt0lMYOzoE+JD0612Q66mokBPHZAd5MuEwQKBgQDbdJUQfcw/9zHuWm4n
+9+wYgMN1QhfJNEr21LUjbe551YapkU389mBJJIlmjH5p67PaMRuJ1o6uRJWv40hf
+Y4xy6iViLc1FExIvRVznxMCIyCELtuvYMiCJtaekFKunziniw8yg5SwSZJY3GlXN
+cDAwIcgb9PPU5rBEip8g0DIp1wKBgQDTunF3OtEoVqdsPSmw5y1767YTCsm3dnVT
++kwp7ZrX3TJ3Xd6EVPWUBP1HbGD3qfsIR+Ha3Vl8OiLNC4zDoZY886U4qY5Mtn4P
+JhUN0H9zYZg2l9gFf9u8RkUoPZPXXuk+eQnlGT133PrkCloDlqP47u/fQ5dV1t6F
+NghgwfOA4QKBgHI/IRMyylBKmj3h6hL4qHqhHiA/Ri7DAHu7hIlrQ4k9ths0wAr/
+IGUzlixC29S8libzBckeX60tm1ez1QuDwaxZZRjVi1V4djERxSoLbchHl5yHoAQv
+JG1Mmnd7I1n6pCefkzn31JfGscUB+sU2sH9+NrUHMqEVb5JfMDRe7p6FAoGAcYGc
+Xqz7gEKkUtSfSyVELxD4dVDtPxuUXsbqmfe1cVA2Q+Pg7NSXKxlZpzak7WEFITVY
+EXtlA8Iu8fnlJuOzpU2BH9VWYi3beseRtew2x2Zksa/JsXkQFekeHiqU3XsWU9WT
+xmw3ldCz+BjMlOvnUAbYNbsIoI4mkQecijKwFkECgYA2zafSyWCW5zAronUBQDEe
+vJumAJ77TwpYzzvH2ic6siWimdePxQ6TgdM3s1FgpdkbaXgKzS5MbZbD0Uyg3MEj
+t6ZT7GSWq39wLDJVDYJ5ClAi8mv9WNs8X8rJ0CkdiPZgHC77OwBELthGn2p9ncar
+Bwhs4S84KEJFT0LAC3YeRQ==
+-----END PRIVATE KEY-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0000_csr-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0000_csr-certbot.pem
new file mode 100644
index 000000000..16d73ffde
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0000_csr-certbot.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIChjCCAW4CAQIwFzEVMBMGA1UEAwwMaXMuaXNub3Qub3JnMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nsHOCTvvQlRYXpI5xE7AggqTVmM8lGi18Y2
+gVlr3WYAS7higHRJjWroAmZ2Bx9IRfHOxwhVWm/hlc/u4w0IYlRnArg6suXrgtn+
+6Ea0WDUCiKEiKvQqD0kaI936hpydU/dY70UZnpKSyi0kiCrLzCkIaXS8HJdLOIXB
+Q4FMVqjppYjUejMgrabthq1QTqU0S4MxwS1oj67VqaAkedGWxFgFQ2kIFV0/WL13
+Xs0SCTYyN96KK1Q2CF63HoN79zc+TVslg32DDU5UF7sVVvlkoHcl0OgR9l4jfou5
+HwmatMjXPI+0bWVxmw6iC6tbK7Dx+ytYIodhEOL52Youzy/lLwIDAQABoCowKAYJ
+KoZIhvcNAQkOMRswGTAXBgNVHREEEDAOggxpcy5pc25vdC5vcmcwDQYJKoZIhvcN
+AQELBQADggEBAAJsLiylvGq64wxVt8EBeXRB4ycBzC5J/pyOWMP9oexW1o3XPhCC
++0tIQVGk7wJMe3+WiPMVsn4pGOUGDaPvfC7ijlvipzaYyLEfnr+J7pukhYbzNHmu
+XL5lbTJ0hTCfqUjmi1yE4M/v2eX5yNaEHsZExZ1NbtwutE/Tx5iSqt7kxbIoFqmF
+7Tne2JHjt945+/l9yvqaIcEFOmblS0OxY9EjxgJdhKCKbhD/ZoYaVVisc52h/2/M
+jtzvzZr1rZCvFnuQxGDco5vYe3u7uJ9tQHLCMpoIorT3kX3yTdgnWxst6XBVUY/P
+Q6O18obG4ALoP/ESzvTauQIwFVGfal/jqyI=
+-----END CERTIFICATE REQUEST-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0001_csr-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0001_csr-certbot.pem
new file mode 100644
index 000000000..452bc45cd
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0001_csr-certbot.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICgDCCAWgCAQIwFDESMBAGA1UEAwwJaXNub3Qub3JnMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAsEAy7rdPsYFFt9VsK9NZy+W9nbsYGmvIaMSyJkEg
+Xe2P0MmnWG/hn6F1bLPm85uS5oQsOWDpwVz31tKhoWhUDbRzPWP5Ur2NnHY92Whz
+5tP4ir4vEEDuB9etQ8+wZ7+3z9q1VhPcgDdYyouQVB0QejJ1yUBiVPr289bW//ln
+kj9DFxn4oufoJ4ELSZSZgWFM92EGKMMy1zD2bJH87mI0Gs0pIOEo+QMJ8TvVEbau
++aFaTANslqRAF5LaWcrPgvHor7cK5w/4bVBZCmY2QYKqlYwZiRPpwg3Ii6B9Q8kz
+rDkGSDjwsazca4api57cza13XkRl7KvyZbwTwlFBud+ydwIDAQABoCcwJQYJKoZI
+hvcNAQkOMRgwFjAUBgNVHREEDTALgglpc25vdC5vcmcwDQYJKoZIhvcNAQELBQAD
+ggEBAB3vniZw2ML6E9jrMY8DtQjPDDNr1BqOGzyOaJipqpGZSRvhTA44DAAjdFpS
+5BLrnXniPIZGG4/6WorLTEDBnlFcLinUg7GDT2DpauQa+4PLxFi13hE1TuSVOp9A
+08YXhzALvZxMIjQ/tVhAp0+PkGEWU2wI0SmDvUUTJqMwSJYgXkf/vBS34/koKywV
+gPDod5AbLuhYgKiQYwDZ0dd69leT0REmizuaHtA6tW3mBgewSKotwqY3fHmhHV8o
+YLSVhImz4jJjK3LjmcdXuBxqE0z+p6n/+lSGG8RR/E8pix4OAkVAP6nyt/loW1BX
+ZzWOuSHozGN5UJSL248vLFWrsV8=
+-----END CERTIFICATE REQUEST-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0002_csr-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0002_csr-certbot.pem
new file mode 100644
index 000000000..2ee44b3fd
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0002_csr-certbot.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICnjCCAYYCAQIwIzEhMB8GA1UEAwwYYS5lbmNyeXB0aW9uLWV4YW1wbGUuY29t
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqrPRxyiGwKrJY/Aavv1r
+yP4k7cepumEtPOGg0dgX59byCXIzIGY4zJmNzy/wTdTcpgRe65FbNH5u9RcFws6F
+KiAiJ9kM2IwoQXIM1wOUbTzN70ZOPaFNDKN63Zm49Y264ni7lQwW+htpx4OohO6o
+SpSuMIGLkKYRezCtXeXYFVKI5w9/6X5lHzXEbupyMZ8sbphBZxqexhiivP+RJG3B
+iMBPg1p5Rg1dV4fWW5ZNaFt53ymYp8FV+/44SbBTICFITV49+fIKPfvCjX6epDPf
+sgk0SkjRE1vGsTtkKADjRCsVWpz1d69zKkWL0N+1VbMM0G133tCWYe4ioD0pvcL9
+7wIDAQABoDYwNAYJKoZIhvcNAQkOMScwJTAjBgNVHREEHDAaghhhLmVuY3J5cHRp
+b24tZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAJyKJHdUwR9BOKYJarUy
+P8mqu6UBUt8faSu6o3EUeDHbnUgxGAVwB5TJV0+JwIjPFQFRofHE8CFhUvi0W0YJ
+BsGVqblnJzz80NkUX9uwjBAGKaDxXqXDOctkQSAOJxM/rvD2uJLmlokibDDm7mnS
+DX8SUVAPgORDGlVTGATjvmA3YeH05gHRFgRDWFP5DOZs99fx4957HrXhsIxew98s
+Felupgswnouyq3crrgcjY0qo3Pc5gjUcuwaT2cjtvzi93f/ImDt6f1sdSSJB00wk
+34lbs/Z+0G8bH1dqYIZzkwNgq7rolhDYh3WRgTlfkgkV7FlkQGm8qn5uoQvaXaaS
+ShM=
+-----END CERTIFICATE REQUEST-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0003_csr-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0003_csr-certbot.pem
new file mode 100644
index 000000000..2a50dc33d
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/csr/0003_csr-certbot.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICnjCCAYYCAQIwIzEhMB8GA1UEAwwYYi5lbmNyeXB0aW9uLWV4YW1wbGUuY29t
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtYDq1ZEUB9jI4Qz0e0lc
+ri0J3W0YXZBmuUkcQvc8nMUeie/tlo+jpWStqvW4uKczgBW7H9+z29yxt15u7ngN
+emPUiWXOMqze2NK0crjqW4yqPyB/7fItkghWc3hjGQi13VYG2Kb7XjiS7WBQuwae
+cea4J59fPqqqQvMVmwSWo7s1j5MvgXib8vSUONixT75eBsdP2CJPDSMCLxLIfwhr
+ghiLjKecy4e5LXV77I62rXqQtbVF2fGtimV673PQNT7peWvW1uV5KTLNtZ6UOB2y
+Jx87FnPmluYAtcStvjvZOJ1wccrpuwxWFH8/XNAr1h1z60Lfd3RcJFGA+h243xdF
+9wIDAQABoDYwNAYJKoZIhvcNAQkOMScwJTAjBgNVHREEHDAaghhiLmVuY3J5cHRp
+b24tZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBACDw8/zjFaIdp4aqyrzT
+fzaqAnoXZt3+0JDPLANy3DLCJmK2TQMyItg/Oid5NEQ45UluXv811IMCcONyVmrD
+19W3XErhTJOJMgpjg4GLBRRFhLm+uTIcbv/xEeUgOYbslsqwi2gHECe1Vsj/Ahbo
+QXXqcDg1cXe6VTQhX+Nw5q30t/oCmkJWcUVHBON2nbOujRz1+z6AjVl1dM+CYDRq
+bsKn7m3biYS7lx7/ApIuhJQsghcmccCtWrH5GsOUsJUgiANv5u+QZgGaajkCRKYV
+fD/u8qTPfKb/+lTxtDrfFOGH+mbZKbKf2/ibneYcql8fFQWiapbudI2cMk8yDxA9
+2Tw=
+-----END CERTIFICATE REQUEST-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0000_key-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0000_key-certbot.pem
new file mode 100644
index 000000000..9a018c41e
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0000_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDuewc4JO+9CVFh
+ekjnETsCCCpNWYzyUaLXxjaBWWvdZgBLuGKAdEmNaugCZnYHH0hF8c7HCFVab+GV
+z+7jDQhiVGcCuDqy5euC2f7oRrRYNQKIoSIq9CoPSRoj3fqGnJ1T91jvRRmekpLK
+LSSIKsvMKQhpdLwcl0s4hcFDgUxWqOmliNR6MyCtpu2GrVBOpTRLgzHBLWiPrtWp
+oCR50ZbEWAVDaQgVXT9YvXdezRIJNjI33oorVDYIXrceg3v3Nz5NWyWDfYMNTlQX
+uxVW+WSgdyXQ6BH2XiN+i7kfCZq0yNc8j7RtZXGbDqILq1srsPH7K1gih2EQ4vnZ
+ii7PL+UvAgMBAAECggEBAIX9jeLXrfNSRu0z3b4mCjdsCwiGphCIGayOa5VlfptY
+chYZNQ7jR2gzhsPCedIqm1rhL8LYRcyYS/D2cUwUyH8m2PHIPQLC9/3/KZ+sCiv9
+LL1De4USxobsFcnNMLNtT2Ab+1YERw63X85EauAu226MJ3PI6OBPiS3qyNl6zj9p
+do9SyzsNFEGtDk+ndWf3keoHBKLge4DP1lA3Jt42wSUxVv9U5SLvFpMQm8PqbqrK
+4ofXcgxMFIJHDDGXsoDI7LOOsV6ncBVlui0ELM/QWBb5x1605VxqEDRL+h/wMp5Y
+JIc6HbgcERmtHmyFlHHNtjAXxeulJVDJQDekd/irJ5ECgYEA/WQJ4LwkkA/Yhf2W
+WYJtD8LuwzRnvGs3R+rgx3+hOeO4TFZD5fzObZVRSwWQO2jbOtBJOaRLUsUngcJQ
+DXr/FGf1rnGhLmNeLE+jN9FS73wBhEXViFZ/fzhVibGbc7u45Y5REykZj8HtUHP5
+hBKR2Nx94WDiv1MBgcKrRk6yI50CgYEA8O+vWcMzEdPtonHl8UgTa8/c5g/RBBvS
+plB8mVsmM/E5CNwnetZM32cg7dC7yNaZzn3qF6w+LdE2vw3j5VbqvuVUvsRgvYcJ
+3kMbHsbsxkRw+HVWZGgEtWNzuYQUL0xN+xzIZDWkbtuaihqYAy4voYNAM08BTNcE
+POQEMIGxcDsCgYEAg+TLo3grS/WDjhM2bHcQT9D2uRMRIClqx/uBbzaG9HwNFWcd
+xpv102KSwwstTU9CNfXu95sGPhozez5qrumj1rpaTqgE7wF4JnZ5jfdeRRv2KiSz
+hlkH2m+3TontUauYDZ0rpF6TWJnn7iW/7jhARHJY77SfslkBgsqSnnEeFp0CgYEA
+7FsFVvZRzCRt01UOsPL28mWYmyxa7D/rFvKQONUdFgmG3PUz2aIPCX2e5Q1GmlBD
+1Djbg1uaJ9I8dZJHxbzNTnWk+/ujt2mYuax1F20n65xKgsKA/MC6FcM5TH2QW5Hs
+UfI7d2rUI1hVMzPBeiU93qDmQy825E1uP9mjbn5cNe8CgYAsBpJgS1LkDruyWmjG
+ZTzdHGciA1O3gUArLQmyUfJlPS3Hgwn7wnBBihtGZDHmjJ7734+PQ9ioCnO9Pb+K
+8Cp29vJ85lka7o7I48OeScLmczgEUYOPCrbkkKJdKaG6gn5CKpRBVYDlhbWjVZ51
+4uda/BQ1hqHh8WmxK6x21qC9JQ==
+-----END PRIVATE KEY-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0001_key-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0001_key-certbot.pem
new file mode 100644
index 000000000..a3a7faf55
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0001_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwQDLut0+xgUW3
+1Wwr01nL5b2duxgaa8hoxLImQSBd7Y/QyadYb+GfoXVss+bzm5LmhCw5YOnBXPfW
+0qGhaFQNtHM9Y/lSvY2cdj3ZaHPm0/iKvi8QQO4H161Dz7Bnv7fP2rVWE9yAN1jK
+i5BUHRB6MnXJQGJU+vbz1tb/+WeSP0MXGfii5+gngQtJlJmBYUz3YQYowzLXMPZs
+kfzuYjQazSkg4Sj5AwnxO9URtq75oVpMA2yWpEAXktpZys+C8eivtwrnD/htUFkK
+ZjZBgqqVjBmJE+nCDciLoH1DyTOsOQZIOPCxrNxrhqmLntzNrXdeRGXsq/JlvBPC
+UUG537J3AgMBAAECggEBAJoZR27X72GvORmmDFG1FInlcIf8EPLo0exoLaqsvnPh
+RSCzbxEvoQFE1boZARB1MVdCsLfqN/bMJhU5TAAni3YAE9HVGyRwfuQRrbnsTYnA
+Q0prRhLb8kIBHIhxijbrtPaSroF4FA42VfehVqt0TffJLpqrJE5QrqI7cPeVRCzk
+laLyi2rjZBhN6l1OxFSIOrEDlcowlPUMORbmNDMbq/dLu5riVO/kP2x70K1IiANI
+NZzVhMwkktYj3Ku2altRLcyRrC3Bs46w2QF6wiC88/LMapt79um65P/SgcCgyOYE
+oxJywZwMnyw8ut1Y+KS8B7AdzqWmj7Q9wr0xbW6+4eECgYEA6sNrMGZVRUFRPAcr
+m3y5fkM/WJ8tAkT3hI2/noljv3k8iameTy/B/y3p+aM8/6Oa/gdO/SWtfKPednkf
+CIh/3J5tJ1yvK7wHEEU6r6qxVKr2FLCMfSXoGx+E+r9qPF8WdV+55beVgO86UqA5
+y9a6DhNA+Xt4jDJc+rbpga0pj60CgYEAwDHDV0lR7jVT6iiU6VhAu1gM/SBVqXE/
+VSfmGihgaO4pJ9OgfqusKbraNONc+oBub7B4T3sSnF/I0mSUclD6brmG99OWLIg8
+L6/ed+bLPRO0iTvKRLbyBLom1Totfh/X6iQ2Zci40vLIS7kbYDban16ca+iSm+0B
+41RV4q6+vzMCgYBLoxiW6HGStZ+xonHHT+EHsCzppac/su64c18IeiV8HFiH1fFe
+e/mZ+LYIqzJM/u5B6CLn5srFfJqBOzbnbescLqLmarM5eQQhltx4mps1tzs/oT4y
+WBM3IembTC6zMsOun1/qhkKR3wHAe0UDyrP5MvTdLI3DRbq1QFdtY1gfpQKBgEgg
+pNGWJ5RBGSvwbOohf7GPOtioEN3VLVJ09crtSjk23+Uda8b+AE9s20Ur6pHsLwXl
+cVFKu9JJtCEZNAiu0T1KjRdmpZ4yxnuTAed3iuByC7fQ43jkO3GAtuAgxD/oDWzG
+iE+sg4hPKtIYNujlzSgwJn3su1CfIq1A0jaPI/C3AoGAHGTBtsXdR1goFvcxwA+n
+l2bAs/InoED5nj26a//JuONgtGlm//QKCxIgjjktpeZm8sfsaYeR+rwIUODWRX/e
+LUF85a70SaH+FZRXBRS2d/zaNxO4F37nE5fwO+VAurSb7El7yOyCepK22iSHMYdl
+xak78KZKv3HXW5yrfA+dc2Y=
+-----END PRIVATE KEY-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0002_key-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0002_key-certbot.pem
new file mode 100644
index 000000000..b3059cb47
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0002_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqs9HHKIbAqslj
+8Bq+/WvI/iTtx6m6YS084aDR2Bfn1vIJcjMgZjjMmY3PL/BN1NymBF7rkVs0fm71
+FwXCzoUqICIn2QzYjChBcgzXA5RtPM3vRk49oU0Mo3rdmbj1jbrieLuVDBb6G2nH
+g6iE7qhKlK4wgYuQphF7MK1d5dgVUojnD3/pfmUfNcRu6nIxnyxumEFnGp7GGKK8
+/5EkbcGIwE+DWnlGDV1Xh9Zblk1oW3nfKZinwVX7/jhJsFMgIUhNXj358go9+8KN
+fp6kM9+yCTRKSNETW8axO2QoAONEKxVanPV3r3MqRYvQ37VVswzQbXfe0JZh7iKg
+PSm9wv3vAgMBAAECggEAattP6Wz8FaWTlgTaqU44Z8R314VSQULNr7vKETJFnLKY
+JsOfL5vt2F4TQGxQ8Ffcm+xGgw4l2tF+odv8ljrzbzBYUTt06CWsmXNMiFhMVKlo
+fG01Uy0i71Ny+T9eYhCLuXM8cYv04jHA4M0Q8831+WHjPKgLdswOS2BoVkwoHQfc
+xEo40D0sPynd+KRukhgR+5AjwMdaNOV7S8c5iuQYIaZ1Xe5AyfiQkMV4LdbobMDj
+bHzGxdeC5GRVOHnMBYrRotgSt4+bsQGeoV9yWY0WAVvnoDfRBRdWK8yRVhuJY1+D
+WB6sPJ5cOg7Ijclubo9b+EaUkddvP0aCA3FepqNwcQKBgQDR0hz9OSom2fBjLaR2
+mQe3LqnotwPCuMmXuKndGIwJz9KgelBaRNUcvDtnzSzQVZ3h9/YFJKUkoVPVCoAu
+wAF9aBeDGs+LdHerBK8fI87PXwCV0OlZLQfUw1/82dpO/dyYXVeGorrO6FE/Oxb8
+enLerMW0Ocp/MhEgM5lFRUJM1wKBgQDQRauI9QuMoBnl516pOs+7EPRvTwe4oBpO
+iH2U7ryJ/YQTgsx25sDWqQBouEnv3j83wnVh9kApkS8UXFd4ZwuizIFCMlgrxw4x
+nKDsd1TZOLUO2FNi09YWPUnzxzQBOjBeekEIDKUQCLOKttTrjRHgGld3tmVtHWtL
+W+OvNIdcqQKBgCMpqjAJr3W5Wl7UnFY/yRo62MCmQxwT6bzidp0V6woN6Qd52BN4
+q5pYNUBtExCK+J2Q94rfHEnqO2ldjCPJi7ZfhmkzSgrd5twjOdHnJ1Z7Xla9Hw4R
+zNksMN7oB3zrcFecdPmcNeBM8Ki/F1gSkUOeArf0Y2ozkskpvIruU3EbAoGBAMVz
+h7CMQKrNjj/8Hi5qZ05+QH7Wegd7IfWaSRTNUUmxY2nr81Q2aFQaXRzquo4CMgT3
+Arog76t4zR2MfhDUAKATKehMOnMmgDpgt9/3MiXOMTkltchX9PuYl2faT19qfzjS
+xpyPAF43IaA8vZejYnMIBiyka3wLDBGhyDXuovYhAoGAB/AZnOM/4SQuIdtzmBSy
+YsHpXcNgRPqvfauCus3e5I6H4wmi+nqF/jyt0oyDBDKZki67CpStwu5Eo7tcLLnY
+o+VfJ9co8jUfVxRh0NlZwomF1t/8yAm/deWoV9sX9Yj71ft/eomCifNseeeg31Kl
+wkqKc3PndJHrR40mswUOHbs=
+-----END PRIVATE KEY-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0003_key-certbot.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0003_key-certbot.pem
new file mode 100644
index 000000000..c43af4f50
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/keys/0003_key-certbot.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC1gOrVkRQH2Mjh
+DPR7SVyuLQndbRhdkGa5SRxC9zycxR6J7+2Wj6OlZK2q9bi4pzOAFbsf37Pb3LG3
+Xm7ueA16Y9SJZc4yrN7Y0rRyuOpbjKo/IH/t8i2SCFZzeGMZCLXdVgbYpvteOJLt
+YFC7Bp5x5rgnn18+qqpC8xWbBJajuzWPky+BeJvy9JQ42LFPvl4Gx0/YIk8NIwIv
+Esh/CGuCGIuMp5zLh7ktdXvsjratepC1tUXZ8a2KZXrvc9A1Pul5a9bW5XkpMs21
+npQ4HbInHzsWc+aW5gC1xK2+O9k4nXBxyum7DFYUfz9c0CvWHXPrQt93dFwkUYD6
+HbjfF0X3AgMBAAECggEAYjEnWnjNTF10d4Qps5UBxdzpzFfb6apYWH78AiJ9MRbX
+Kaqab2ywDKdF6Qpcb9FM5EtdW6YLSLPBlUFKZEqgiAkAD4D7J6EsQkLjinkNmI+l
+/tbXPuRY0PsfwgJsIjv7H44N0CGuNdAHdNI5eqTfDSHTmOP4hA+SYvvdQWsfD94r
+m4ocr2YfL4BmEh3hujb8NjVD8csSnFlpeVibtJ1rWiv1otLaEuVmcN49n0rIj0IK
+tiCIdqqIscVZ+P3fFfr/E3oL2nhBqxRnzqoK/HNTpI4JJAbRGP51nVr0QhZYpIuj
+xDM+zeuIt0lMYOzoE+JD0612Q66mokBPHZAd5MuEwQKBgQDbdJUQfcw/9zHuWm4n
+9+wYgMN1QhfJNEr21LUjbe551YapkU389mBJJIlmjH5p67PaMRuJ1o6uRJWv40hf
+Y4xy6iViLc1FExIvRVznxMCIyCELtuvYMiCJtaekFKunziniw8yg5SwSZJY3GlXN
+cDAwIcgb9PPU5rBEip8g0DIp1wKBgQDTunF3OtEoVqdsPSmw5y1767YTCsm3dnVT
++kwp7ZrX3TJ3Xd6EVPWUBP1HbGD3qfsIR+Ha3Vl8OiLNC4zDoZY886U4qY5Mtn4P
+JhUN0H9zYZg2l9gFf9u8RkUoPZPXXuk+eQnlGT133PrkCloDlqP47u/fQ5dV1t6F
+NghgwfOA4QKBgHI/IRMyylBKmj3h6hL4qHqhHiA/Ri7DAHu7hIlrQ4k9ths0wAr/
+IGUzlixC29S8libzBckeX60tm1ez1QuDwaxZZRjVi1V4djERxSoLbchHl5yHoAQv
+JG1Mmnd7I1n6pCefkzn31JfGscUB+sU2sH9+NrUHMqEVb5JfMDRe7p6FAoGAcYGc
+Xqz7gEKkUtSfSyVELxD4dVDtPxuUXsbqmfe1cVA2Q+Pg7NSXKxlZpzak7WEFITVY
+EXtlA8Iu8fnlJuOzpU2BH9VWYi3beseRtew2x2Zksa/JsXkQFekeHiqU3XsWU9WT
+xmw3ldCz+BjMlOvnUAbYNbsIoI4mkQecijKwFkECgYA2zafSyWCW5zAronUBQDEe
+vJumAJ77TwpYzzvH2ic6siWimdePxQ6TgdM3s1FgpdkbaXgKzS5MbZbD0Uyg3MEj
+t6ZT7GSWq39wLDJVDYJ5ClAi8mv9WNs8X8rJ0CkdiPZgHC77OwBELthGn2p9ncar
+Bwhs4S84KEJFT0LAC3YeRQ==
+-----END PRIVATE KEY-----
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/README b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/README
new file mode 100644
index 000000000..15194ae3a
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/README
@@ -0,0 +1,10 @@
+This directory contains your keys and certificates.
+
+`privkey.pem` : the private key for your certificate.
+`fullchain.pem`: the certificate file used in most server software.
+`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
+`cert.pem` : will break many server configurations, and should not be used
+ without reading further documentation (see link below).
+
+We recommend not moving these files. For more information, see the Certbot
+User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/cert.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/cert.pem
new file mode 120000
index 000000000..79b6abdf9
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/cert.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/cert1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/chain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/chain.pem
new file mode 120000
index 000000000..2d6b30420
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/chain.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/chain1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/fullchain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/fullchain.pem
new file mode 120000
index 000000000..b801ef735
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/fullchain.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/fullchain1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/privkey.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/privkey.pem
new file mode 120000
index 000000000..74e20c5ff
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/privkey.pem
@@ -0,0 +1 @@
+../../archive/a.encryption-example.com/privkey1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/README b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/README
new file mode 100644
index 000000000..15194ae3a
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/README
@@ -0,0 +1,10 @@
+This directory contains your keys and certificates.
+
+`privkey.pem` : the private key for your certificate.
+`fullchain.pem`: the certificate file used in most server software.
+`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
+`cert.pem` : will break many server configurations, and should not be used
+ without reading further documentation (see link below).
+
+We recommend not moving these files. For more information, see the Certbot
+User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/cert.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/cert.pem
new file mode 120000
index 000000000..41b06370e
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/cert.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/cert1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/chain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/chain.pem
new file mode 120000
index 000000000..2d3e18bec
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/chain.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/chain1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/fullchain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/fullchain.pem
new file mode 120000
index 000000000..3a08c1432
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/fullchain.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/fullchain1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/privkey.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/privkey.pem
new file mode 120000
index 000000000..182aa6d78
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/privkey.pem
@@ -0,0 +1 @@
+../../archive/b.encryption-example.com/privkey1.pem \ No newline at end of file
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/options-ssl-apache.conf b/certbot-ci/certbot_integration_tests/assets/sample-config/options-ssl-apache.conf
new file mode 100644
index 000000000..ec07a4ba3
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/options-ssl-apache.conf
@@ -0,0 +1,22 @@
+# Baseline setting to Include for SSL sites
+
+SSLEngine on
+
+# Intermediate configuration, tweak to your needs
+SSLProtocol all -SSLv2 -SSLv3
+SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA
+SSLHonorCipherOrder on
+SSLCompression off
+
+SSLOptions +StrictRequire
+
+# Add vhost name to log entries:
+LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
+LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
+
+#CustomLog /var/log/apache2/access.log vhost_combined
+#LogLevel warn
+#ErrorLog /var/log/apache2/error.log
+
+# Always ensure Cookies have "Secure" set (JAH 2012/1)
+#Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4"
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/a.encryption-example.com.conf b/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/a.encryption-example.com.conf
new file mode 100644
index 000000000..4455137b4
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/a.encryption-example.com.conf
@@ -0,0 +1,15 @@
+# renew_before_expiry = 30 days
+version = 0.10.0.dev0
+archive_dir = sample-config/archive/a.encryption-example.com
+cert = sample-config/live/a.encryption-example.com/cert.pem
+privkey = sample-config/live/a.encryption-example.com/privkey.pem
+chain = sample-config/live/a.encryption-example.com/chain.pem
+fullchain = sample-config/live/a.encryption-example.com/fullchain.pem
+
+# Options used in the renewal process
+[renewalparams]
+authenticator = apache
+installer = apache
+account = 48d6b9e8d767eccf7e4d877d6ffa81e3
+config_dir = sample-config
+server = https://acme-staging.api.letsencrypt.org/directory
diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/b.encryption-example.com.conf b/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/b.encryption-example.com.conf
new file mode 100644
index 000000000..58d8a13d9
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/b.encryption-example.com.conf
@@ -0,0 +1,15 @@
+# renew_before_expiry = 30 days
+version = 0.10.0.dev0
+archive_dir = sample-config/archive/b.encryption-example.com
+cert = sample-config/live/b.encryption-example.com/cert.pem
+privkey = sample-config/live/b.encryption-example.com/privkey.pem
+chain = sample-config/live/b.encryption-example.com/chain.pem
+fullchain = sample-config/live/b.encryption-example.com/fullchain.pem
+
+# Options used in the renewal process
+[renewalparams]
+authenticator = apache
+installer = apache
+account = 48d6b9e8d767eccf7e4d877d6ffa81e3
+config_dir = sample-config
+server = https://acme-staging.api.letsencrypt.org/directory
diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/__init__.py b/certbot-ci/certbot_integration_tests/certbot_tests/__init__.py
new file mode 100644
index 000000000..60c2fcdd8
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/certbot_tests/__init__.py
@@ -0,0 +1,5 @@
+import pytest
+
+# Custom assertions defined in the following package need to be registered to be properly
+# displayed in a pytest report when they are failing.
+pytest.register_assert_rewrite('certbot_integration_tests.certbot_tests.assertions')
diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py
new file mode 100644
index 000000000..1b5914d1a
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py
@@ -0,0 +1,161 @@
+"""This module contains advanced assertions for the certbot integration tests."""
+import os
+
+try:
+ import grp
+ POSIX_MODE = True
+except ImportError:
+ import win32api
+ import win32security
+ import ntsecuritycon
+ POSIX_MODE = False
+
+EVERYBODY_SID = 'S-1-1-0'
+SYSTEM_SID = 'S-1-5-18'
+ADMINS_SID = 'S-1-5-32-544'
+
+
+def assert_hook_execution(probe_path, probe_content):
+ """
+ Assert that a certbot hook has been executed
+ :param probe_path: path to the file that received the hook output
+ :param probe_content: content expected when the hook is executed
+ """
+ with open(probe_path, 'r') as file:
+ data = file.read()
+
+ lines = [line.strip() for line in data.splitlines()]
+ assert probe_content in lines
+
+
+def assert_saved_renew_hook(config_dir, lineage):
+ """
+ Assert that the renew hook configuration of a lineage has been saved.
+ :param config_dir: location of the certbot configuration
+ :param lineage: lineage domain name
+ """
+ with open(os.path.join(config_dir, 'renewal', '{0}.conf'.format(lineage))) as file_h:
+ assert 'renew_hook' in file_h.read()
+
+
+def assert_cert_count_for_lineage(config_dir, lineage, count):
+ """
+ Assert the number of certificates generated for a lineage.
+ :param config_dir: location of the certbot configuration
+ :param lineage: lineage domain name
+ :param count: number of expected certificates
+ """
+ archive_dir = os.path.join(config_dir, 'archive')
+ lineage_dir = os.path.join(archive_dir, lineage)
+ certs = [file for file in os.listdir(lineage_dir) if file.startswith('cert')]
+ assert len(certs) == count
+
+
+def assert_equals_group_permissions(file1, file2):
+ """
+ Assert that two files have the same permissions for group owner.
+ :param file1: first file path to compare
+ :param file2: second file path to compare
+ """
+ # On Windows there is no group, so this assertion does nothing on this platform
+ if POSIX_MODE:
+ mode_file1 = os.stat(file1).st_mode & 0o070
+ mode_file2 = os.stat(file2).st_mode & 0o070
+
+ assert mode_file1 == mode_file2
+
+
+def assert_equals_world_read_permissions(file1, file2):
+ """
+ Assert that two files have the same read permissions for everyone.
+ :param file1: first file path to compare
+ :param file2: second file path to compare
+ """
+ if POSIX_MODE:
+ mode_file1 = os.stat(file1).st_mode & 0o004
+ mode_file2 = os.stat(file2).st_mode & 0o004
+ else:
+ everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)
+
+ security1 = win32security.GetFileSecurity(file1, win32security.DACL_SECURITY_INFORMATION)
+ dacl1 = security1.GetSecurityDescriptorDacl()
+
+ mode_file1 = dacl1.GetEffectiveRightsFromAcl({
+ 'TrusteeForm': win32security.TRUSTEE_IS_SID,
+ 'TrusteeType': win32security.TRUSTEE_IS_USER,
+ 'Identifier': everybody,
+ })
+ mode_file1 = mode_file1 & ntsecuritycon.FILE_GENERIC_READ
+
+ security2 = win32security.GetFileSecurity(file2, win32security.DACL_SECURITY_INFORMATION)
+ dacl2 = security2.GetSecurityDescriptorDacl()
+
+ mode_file2 = dacl2.GetEffectiveRightsFromAcl({
+ 'TrusteeForm': win32security.TRUSTEE_IS_SID,
+ 'TrusteeType': win32security.TRUSTEE_IS_USER,
+ 'Identifier': everybody,
+ })
+ mode_file2 = mode_file2 & ntsecuritycon.FILE_GENERIC_READ
+
+ assert mode_file1 == mode_file2
+
+
+def assert_equals_group_owner(file1, file2):
+ """
+ Assert that two files have the same group owner.
+ :param file1: first file path to compare
+ :param file2: second file path to compare
+ """
+ # On Windows there is no group, so this assertion does nothing on this platform
+ if POSIX_MODE:
+ group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0]
+ group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0]
+
+ assert group_owner_file1 == group_owner_file2
+
+
+def assert_world_no_permissions(file):
+ """
+ Assert that the given file is not world-readable.
+ :param file: path of the file to check
+ """
+ if POSIX_MODE:
+ mode_file_all = os.stat(file).st_mode & 0o007
+ assert mode_file_all == 0
+ else:
+ security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)
+ dacl = security.GetSecurityDescriptorDacl()
+ mode = dacl.GetEffectiveRightsFromAcl({
+ 'TrusteeForm': win32security.TRUSTEE_IS_SID,
+ 'TrusteeType': win32security.TRUSTEE_IS_USER,
+ 'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),
+ })
+
+ assert not mode
+
+
+def assert_world_read_permissions(file):
+ """
+ Assert that the given file is world-readable, but not world-writable or world-executable.
+ :param file: path of the file to check
+ """
+ if POSIX_MODE:
+ mode_file_all = os.stat(file).st_mode & 0o007
+ assert mode_file_all == 4
+ else:
+ security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)
+ dacl = security.GetSecurityDescriptorDacl()
+ mode = dacl.GetEffectiveRightsFromAcl({
+ 'TrusteeForm': win32security.TRUSTEE_IS_SID,
+ 'TrusteeType': win32security.TRUSTEE_IS_USER,
+ 'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),
+ })
+
+ assert not mode & ntsecuritycon.FILE_GENERIC_WRITE
+ assert not mode & ntsecuritycon.FILE_GENERIC_EXECUTE
+ assert mode & ntsecuritycon.FILE_GENERIC_READ == ntsecuritycon.FILE_GENERIC_READ
+
+
+def _get_current_user():
+ account_name = win32api.GetUserNameEx(win32api.NameSamCompatible)
+ return win32security.LookupAccountName(None, account_name)[0]
diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/context.py b/certbot-ci/certbot_integration_tests/certbot_tests/context.py
new file mode 100644
index 000000000..6f8670000
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/certbot_tests/context.py
@@ -0,0 +1,83 @@
+"""Module to handle the context of integration tests."""
+import logging
+import os
+import shutil
+import sys
+import tempfile
+
+from certbot_integration_tests.utils import certbot_call
+
+
+class IntegrationTestsContext(object):
+ """General fixture describing a certbot integration tests context"""
+ def __init__(self, request):
+ self.request = request
+
+ if hasattr(request.config, 'slaveinput'): # Worker node
+ self.worker_id = request.config.slaveinput['slaveid']
+ acme_xdist = request.config.slaveinput['acme_xdist']
+ else: # Primary node
+ self.worker_id = 'primary'
+ acme_xdist = request.config.acme_xdist
+
+ self.acme_server = acme_xdist['acme_server']
+ self.directory_url = acme_xdist['directory_url']
+ self.tls_alpn_01_port = acme_xdist['https_port'][self.worker_id]
+ self.http_01_port = acme_xdist['http_port'][self.worker_id]
+ self.other_port = acme_xdist['other_port'][self.worker_id]
+ # Challtestsrv REST API, that exposes entrypoints to register new DNS entries,
+ # is listening on challtestsrv_port.
+ self.challtestsrv_port = acme_xdist['challtestsrv_port']
+
+ self.workspace = tempfile.mkdtemp()
+ self.config_dir = os.path.join(self.workspace, 'conf')
+
+ probe = tempfile.mkstemp(dir=self.workspace)
+ os.close(probe[0])
+ self.hook_probe = probe[1]
+
+ self.manual_dns_auth_hook = (
+ '{0} -c "import os; import requests; import json; '
+ "assert not os.environ.get('CERTBOT_DOMAIN').startswith('fail'); "
+ "data = {{'host':'_acme-challenge.{{0}}.'.format(os.environ.get('CERTBOT_DOMAIN')),"
+ "'value':os.environ.get('CERTBOT_VALIDATION')}}; "
+ "request = requests.post('http://localhost:{1}/set-txt', data=json.dumps(data)); "
+ "request.raise_for_status(); "
+ '"'
+ ).format(sys.executable, self.challtestsrv_port)
+ self.manual_dns_cleanup_hook = (
+ '{0} -c "import os; import requests; import json; '
+ "data = {{'host':'_acme-challenge.{{0}}.'.format(os.environ.get('CERTBOT_DOMAIN'))}}; "
+ "request = requests.post('http://localhost:{1}/clear-txt', data=json.dumps(data)); "
+ "request.raise_for_status(); "
+ '"'
+ ).format(sys.executable, self.challtestsrv_port)
+
+ def cleanup(self):
+ """Cleanup the integration test context."""
+ shutil.rmtree(self.workspace)
+
+ def certbot(self, args, force_renew=True):
+ """
+ Execute certbot with given args, not renewing certificates by default.
+ :param args: args to pass to certbot
+ :param force_renew: set to False to not renew by default
+ :return: output of certbot execution
+ """
+ command = ['--authenticator', 'standalone', '--installer', 'null']
+ command.extend(args)
+ return certbot_call.certbot_test(
+ command, self.directory_url, self.http_01_port, self.tls_alpn_01_port,
+ self.config_dir, self.workspace, force_renew=force_renew)
+
+ def get_domain(self, subdomain='le'):
+ """
+ Generate a certificate domain name suitable for distributed certbot integration tests.
+ This is a requirement to let the distribution know how to redirect the challenge check
+ from the ACME server to the relevant pytest-xdist worker. This resolution is done by
+ appending the pytest worker id to the subdomain, using this pattern:
+ {subdomain}.{worker_id}.wtf
+ :param subdomain: the subdomain to use in the generated domain (default 'le')
+ :return: the well-formed domain suitable for redirection on
+ """
+ return '{0}.{1}.wtf'.format(subdomain, self.worker_id)
diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
new file mode 100644
index 000000000..94e76cf79
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
@@ -0,0 +1,613 @@
+"""Module executing integration tests against certbot core."""
+from __future__ import print_function
+
+import os
+from os.path import exists
+from os.path import join
+import re
+import shutil
+import subprocess
+import time
+
+import pytest
+
+from certbot_integration_tests.certbot_tests import context as certbot_context
+from certbot_integration_tests.certbot_tests.assertions import assert_cert_count_for_lineage
+from certbot_integration_tests.certbot_tests.assertions import assert_equals_group_owner
+from certbot_integration_tests.certbot_tests.assertions import assert_equals_group_permissions
+from certbot_integration_tests.certbot_tests.assertions import assert_equals_world_read_permissions
+from certbot_integration_tests.certbot_tests.assertions import assert_hook_execution
+from certbot_integration_tests.certbot_tests.assertions import assert_saved_renew_hook
+from certbot_integration_tests.certbot_tests.assertions import assert_world_no_permissions
+from certbot_integration_tests.certbot_tests.assertions import assert_world_read_permissions
+from certbot_integration_tests.certbot_tests.assertions import EVERYBODY_SID
+from certbot_integration_tests.utils import misc
+
+
+@pytest.fixture()
+def context(request):
+ # Fixture request is a built-in pytest fixture describing current test request.
+ integration_test_context = certbot_context.IntegrationTestsContext(request)
+ try:
+ yield integration_test_context
+ finally:
+ integration_test_context.cleanup()
+
+
+def test_basic_commands(context):
+ """Test simple commands on Certbot CLI."""
+ # TMPDIR env variable is set to workspace for the certbot subprocess.
+ # So tempdir module will create any temporary files/dirs in workspace,
+ # and its content can be tested to check correct certbot cleanup.
+ initial_count_tmpfiles = len(os.listdir(context.workspace))
+
+ context.certbot(['--help'])
+ context.certbot(['--help', 'all'])
+ context.certbot(['--version'])
+
+ with pytest.raises(subprocess.CalledProcessError):
+ context.certbot(['--csr'])
+
+ new_count_tmpfiles = len(os.listdir(context.workspace))
+ assert initial_count_tmpfiles == new_count_tmpfiles
+
+
+def test_hook_dirs_creation(context):
+ """Test all hooks directory are created during Certbot startup."""
+ context.certbot(['register'])
+
+ for hook_dir in misc.list_renewal_hooks_dirs(context.config_dir):
+ assert os.path.isdir(hook_dir)
+
+
+def test_registration_override(context):
+ """Test correct register/unregister, and registration override."""
+ context.certbot(['register'])
+ context.certbot(['unregister'])
+ context.certbot(['register', '--email', 'ex1@domain.org,ex2@domain.org'])
+
+ context.certbot(['update_account', '--email', 'example@domain.org'])
+ context.certbot(['update_account', '--email', 'ex1@domain.org,ex2@domain.org'])
+
+
+def test_prepare_plugins(context):
+ """Test that plugins are correctly instantiated and displayed."""
+ output = context.certbot(['plugins', '--init', '--prepare'])
+
+ assert 'webroot' in output
+
+
+def test_http_01(context):
+ """Test the HTTP-01 challenge using standalone plugin."""
+ # We start a server listening on the port for the
+ # TLS-SNI challenge to prevent regressions in #3601.
+ with misc.create_http_server(context.tls_alpn_01_port):
+ certname = context.get_domain('le2')
+ context.certbot([
+ '--domains', certname, '--preferred-challenges', 'http-01', 'run',
+ '--cert-name', certname,
+ '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
+ '--post-hook', misc.echo('wtf_post', context.hook_probe),
+ '--deploy-hook', misc.echo('deploy', context.hook_probe),
+ ])
+
+ assert_hook_execution(context.hook_probe, 'deploy')
+ assert_saved_renew_hook(context.config_dir, certname)
+
+
+def test_manual_http_auth(context):
+ """Test the HTTP-01 challenge using manual plugin."""
+ with misc.create_http_server(context.http_01_port) as webroot,\
+ misc.manual_http_hooks(webroot, context.http_01_port) as scripts:
+
+ certname = context.get_domain()
+ context.certbot([
+ 'certonly', '-a', 'manual', '-d', certname,
+ '--cert-name', certname,
+ '--manual-auth-hook', scripts[0],
+ '--manual-cleanup-hook', scripts[1],
+ '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
+ '--post-hook', misc.echo('wtf_post', context.hook_probe),
+ '--renew-hook', misc.echo('renew', context.hook_probe),
+ ])
+
+ with pytest.raises(AssertionError):
+ assert_hook_execution(context.hook_probe, 'renew')
+ assert_saved_renew_hook(context.config_dir, certname)
+
+
+def test_manual_dns_auth(context):
+ """Test the DNS-01 challenge using manual plugin."""
+ certname = context.get_domain('dns')
+ context.certbot([
+ '-a', 'manual', '-d', certname, '--preferred-challenges', 'dns',
+ 'run', '--cert-name', certname,
+ '--manual-auth-hook', context.manual_dns_auth_hook,
+ '--manual-cleanup-hook', context.manual_dns_cleanup_hook,
+ '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
+ '--post-hook', misc.echo('wtf_post', context.hook_probe),
+ '--renew-hook', misc.echo('renew', context.hook_probe),
+ ])
+
+ with pytest.raises(AssertionError):
+ assert_hook_execution(context.hook_probe, 'renew')
+ assert_saved_renew_hook(context.config_dir, certname)
+
+ context.certbot(['renew', '--cert-name', certname, '--authenticator', 'manual'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+
+
+def test_certonly(context):
+ """Test the certonly verb on certbot."""
+ context.certbot(['certonly', '--cert-name', 'newname', '-d', context.get_domain('newname')])
+
+
+def test_auth_and_install_with_csr(context):
+ """Test certificate issuance and install using an existing CSR."""
+ certname = context.get_domain('le3')
+ key_path = join(context.workspace, 'key.pem')
+ csr_path = join(context.workspace, 'csr.der')
+
+ misc.generate_csr([certname], key_path, csr_path)
+
+ cert_path = join(context.workspace, 'csr', 'cert.pem')
+ chain_path = join(context.workspace, 'csr', 'chain.pem')
+
+ context.certbot([
+ 'auth', '--csr', csr_path,
+ '--cert-path', cert_path,
+ '--chain-path', chain_path
+ ])
+
+ print(misc.read_certificate(cert_path))
+ print(misc.read_certificate(chain_path))
+
+ context.certbot([
+ '--domains', certname, 'install',
+ '--cert-path', cert_path,
+ '--key-path', key_path
+ ])
+
+
+def test_renew_files_permissions(context):
+ """Test proper certificate file permissions upon renewal"""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname])
+
+ privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')
+ privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+ assert_world_no_permissions(privkey1)
+
+ context.certbot(['renew'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+ assert_world_no_permissions(privkey2)
+ assert_equals_group_owner(privkey1, privkey2)
+ assert_equals_world_read_permissions(privkey1, privkey2)
+ assert_equals_group_permissions(privkey1, privkey2)
+
+
+def test_renew_with_hook_scripts(context):
+ """Test certificate renewal with script hooks."""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+ misc.generate_test_file_hooks(context.config_dir, context.hook_probe)
+ context.certbot(['renew'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+ assert_hook_execution(context.hook_probe, 'deploy')
+
+
+def test_renew_files_propagate_permissions(context):
+ """Test proper certificate renewal with custom permissions propagated on private key."""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+ privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')
+ privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
+
+ if os.name != 'nt':
+ os.chmod(privkey1, 0o444)
+ else:
+ import win32security
+ import ntsecuritycon
+ # Get the current DACL of the private key
+ security = win32security.GetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION)
+ dacl = security.GetSecurityDescriptorDacl()
+ # Create a read permission for Everybody group
+ everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)
+ dacl.AddAccessAllowedAce(win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody)
+ # Apply the updated DACL to the private key
+ security.SetSecurityDescriptorDacl(1, dacl, 0)
+ win32security.SetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION, security)
+
+ context.certbot(['renew'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+ if os.name != 'nt':
+ # On Linux, read world permissions + all group permissions will be copied from the previous private key
+ assert_world_read_permissions(privkey2)
+ assert_equals_world_read_permissions(privkey1, privkey2)
+ assert_equals_group_permissions(privkey1, privkey2)
+ else:
+ # On Windows, world will never have any permissions, and group permission is irrelevant for this platform
+ assert_world_no_permissions(privkey2)
+
+
+def test_graceful_renew_it_is_not_time(context):
+ """Test graceful renew is not done when it is not due time."""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+ context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],
+ force_renew=False)
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+ with pytest.raises(AssertionError):
+ assert_hook_execution(context.hook_probe, 'deploy')
+
+
+def test_graceful_renew_it_is_time(context):
+ """Test graceful renew is done when it is due time."""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+ with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'r') as file:
+ lines = file.readlines()
+ lines.insert(4, 'renew_before_expiry = 100 years{0}'.format(os.linesep))
+ with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'w') as file:
+ file.writelines(lines)
+
+ context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],
+ force_renew=False)
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+ assert_hook_execution(context.hook_probe, 'deploy')
+
+
+def test_renew_with_changed_private_key_complexity(context):
+ """Test proper renew with updated private key complexity."""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname, '--rsa-key-size', '4096'])
+
+ key1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')
+ assert os.stat(key1).st_size > 3000 # 4096 bits keys takes more than 3000 bytes
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+ context.certbot(['renew'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+ key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
+ assert os.stat(key2).st_size > 3000
+
+ context.certbot(['renew', '--rsa-key-size', '2048'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 3)
+ key3 = join(context.config_dir, 'archive', certname, 'privkey3.pem')
+ assert os.stat(key3).st_size < 1800 # 2048 bits keys takes less than 1800 bytes
+
+
+def test_renew_ignoring_directory_hooks(context):
+ """Test hooks are ignored during renewal with relevant CLI flag."""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+ misc.generate_test_file_hooks(context.config_dir, context.hook_probe)
+ context.certbot(['renew', '--no-directory-hooks'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+ with pytest.raises(AssertionError):
+ assert_hook_execution(context.hook_probe, 'deploy')
+
+
+def test_renew_empty_hook_scripts(context):
+ """Test proper renew with empty hook scripts."""
+ certname = context.get_domain('renew')
+ context.certbot(['-d', certname])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+ misc.generate_test_file_hooks(context.config_dir, context.hook_probe)
+ for hook_dir in misc.list_renewal_hooks_dirs(context.config_dir):
+ shutil.rmtree(hook_dir)
+ os.makedirs(join(hook_dir, 'dir'))
+ open(join(hook_dir, 'file'), 'w').close()
+ context.certbot(['renew'])
+
+ assert_cert_count_for_lineage(context.config_dir, certname, 2)
+
+
+def test_renew_hook_override(context):
+ """Test correct hook override on renew."""
+ certname = context.get_domain('override')
+ context.certbot([
+ 'certonly', '-d', certname,
+ '--preferred-challenges', 'http-01',
+ '--pre-hook', misc.echo('pre', context.hook_probe),
+ '--post-hook', misc.echo('post', context.hook_probe),
+ '--deploy-hook', misc.echo('deploy', context.hook_probe),
+ ])
+
+ assert_hook_execution(context.hook_probe, 'pre')
+ assert_hook_execution(context.hook_probe, 'post')
+ assert_hook_execution(context.hook_probe, 'deploy')
+
+ # Now we override all previous hooks during next renew.
+ open(context.hook_probe, 'w').close()
+ context.certbot([
+ 'renew', '--cert-name', certname,
+ '--pre-hook', misc.echo('pre_override', context.hook_probe),
+ '--post-hook', misc.echo('post_override', context.hook_probe),
+ '--deploy-hook', misc.echo('deploy_override', context.hook_probe),
+ ])
+
+ assert_hook_execution(context.hook_probe, 'pre_override')
+ assert_hook_execution(context.hook_probe, 'post_override')
+ assert_hook_execution(context.hook_probe, 'deploy_override')
+ with pytest.raises(AssertionError):
+ assert_hook_execution(context.hook_probe, 'pre')
+ with pytest.raises(AssertionError):
+ assert_hook_execution(context.hook_probe, 'post')
+ with pytest.raises(AssertionError):
+ assert_hook_execution(context.hook_probe, 'deploy')
+
+ # Expect that this renew will reuse new hooks registered in the previous renew.
+ open(context.hook_probe, 'w').close()
+ context.certbot(['renew', '--cert-name', certname])
+
+ assert_hook_execution(context.hook_probe, 'pre_override')
+ assert_hook_execution(context.hook_probe, 'post_override')
+ assert_hook_execution(context.hook_probe, 'deploy_override')
+
+
+def test_invalid_domain_with_dns_challenge(context):
+ """Test certificate issuance failure with DNS-01 challenge."""
+ # Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'.
+ domains = ','.join([context.get_domain('dns1'), context.get_domain('fail-dns1')])
+ context.certbot([
+ '-a', 'manual', '-d', domains,
+ '--allow-subset-of-names',
+ '--preferred-challenges', 'dns',
+ '--manual-auth-hook', context.manual_dns_auth_hook,
+ '--manual-cleanup-hook', context.manual_dns_cleanup_hook
+ ])
+
+ output = context.certbot(['certificates'])
+
+ assert context.get_domain('fail-dns1') not in output
+
+
+def test_reuse_key(context):
+ """Test various scenarios where a key is reused."""
+ certname = context.get_domain('reusekey')
+ context.certbot(['--domains', certname, '--reuse-key'])
+ context.certbot(['renew', '--cert-name', certname])
+
+ with open(join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname), 'r') as file:
+ privkey1 = file.read()
+ with open(join(context.config_dir, 'archive/{0}/privkey2.pem').format(certname), 'r') as file:
+ privkey2 = file.read()
+ assert privkey1 == privkey2
+
+ context.certbot(['--cert-name', certname, '--domains', certname, '--force-renewal'])
+
+ with open(join(context.config_dir, 'archive/{0}/privkey3.pem').format(certname), 'r') as file:
+ privkey3 = file.read()
+ assert privkey2 != privkey3
+
+ with open(join(context.config_dir, 'archive/{0}/cert1.pem').format(certname), 'r') as file:
+ cert1 = file.read()
+ with open(join(context.config_dir, 'archive/{0}/cert2.pem').format(certname), 'r') as file:
+ cert2 = file.read()
+ with open(join(context.config_dir, 'archive/{0}/cert3.pem').format(certname), 'r') as file:
+ cert3 = file.read()
+
+ assert len({cert1, cert2, cert3}) == 3
+
+
+def test_ecdsa(context):
+ """Test certificate issuance with ECDSA key."""
+ key_path = join(context.workspace, 'privkey-p384.pem')
+ csr_path = join(context.workspace, 'csr-p384.der')
+ cert_path = join(context.workspace, 'cert-p384.pem')
+ chain_path = join(context.workspace, 'chain-p384.pem')
+
+ misc.generate_csr([context.get_domain('ecdsa')], key_path, csr_path, key_type=misc.ECDSA_KEY_TYPE)
+ context.certbot(['auth', '--csr', csr_path, '--cert-path', cert_path, '--chain-path', chain_path])
+
+ certificate = misc.read_certificate(cert_path)
+ assert 'ASN1 OID: secp384r1' in certificate
+
+
+def test_ocsp_must_staple(context):
+ """Test that OCSP Must-Staple is correctly set in the generated certificate."""
+ if context.acme_server == 'pebble':
+ pytest.skip('Pebble does not support OCSP Must-Staple.')
+
+ certname = context.get_domain('must-staple')
+ context.certbot(['auth', '--must-staple', '--domains', certname])
+
+ certificate = misc.read_certificate(join(context.config_dir,
+ 'live/{0}/cert.pem').format(certname))
+ assert 'status_request' in certificate or '1.3.6.1.5.5.7.1.24' in certificate
+
+
+def test_revoke_simple(context):
+ """Test various scenarios that revokes a certificate."""
+ # Default action after revoke is to delete the certificate.
+ certname = context.get_domain()
+ cert_path = join(context.config_dir, 'live', certname, 'cert.pem')
+ context.certbot(['-d', certname])
+ context.certbot(['revoke', '--cert-path', cert_path, '--delete-after-revoke'])
+
+ assert not exists(cert_path)
+
+ # Check default deletion is overridden.
+ certname = context.get_domain('le1')
+ cert_path = join(context.config_dir, 'live', certname, 'cert.pem')
+ context.certbot(['-d', certname])
+ context.certbot(['revoke', '--cert-path', cert_path, '--no-delete-after-revoke'])
+
+ assert exists(cert_path)
+
+ context.certbot(['delete', '--cert-name', certname])
+
+ assert not exists(join(context.config_dir, 'archive', certname))
+ assert not exists(join(context.config_dir, 'live', certname))
+ assert not exists(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)))
+
+ certname = context.get_domain('le2')
+ key_path = join(context.config_dir, 'live', certname, 'privkey.pem')
+ cert_path = join(context.config_dir, 'live', certname, 'cert.pem')
+ context.certbot(['-d', certname])
+ context.certbot(['revoke', '--cert-path', cert_path, '--key-path', key_path])
+
+
+def test_revoke_and_unregister(context):
+ """Test revoke with a reason then unregister."""
+ cert1 = context.get_domain('le1')
+ cert2 = context.get_domain('le2')
+ cert3 = context.get_domain('le3')
+
+ cert_path1 = join(context.config_dir, 'live', cert1, 'cert.pem')
+ key_path2 = join(context.config_dir, 'live', cert2, 'privkey.pem')
+ cert_path2 = join(context.config_dir, 'live', cert2, 'cert.pem')
+
+ context.certbot(['-d', cert1])
+ context.certbot(['-d', cert2])
+ context.certbot(['-d', cert3])
+
+ context.certbot(['revoke', '--cert-path', cert_path1,
+ '--reason', 'cessationOfOperation'])
+ context.certbot(['revoke', '--cert-path', cert_path2, '--key-path', key_path2,
+ '--reason', 'keyCompromise'])
+
+ context.certbot(['unregister'])
+
+ output = context.certbot(['certificates'])
+
+ assert cert1 not in output
+ assert cert2 not in output
+ assert cert3 in output
+
+
+def test_revoke_mutual_exclusive_flags(context):
+ """Test --cert-path and --cert-name cannot be used during revoke."""
+ cert = context.get_domain('le1')
+ context.certbot(['-d', cert])
+ with pytest.raises(subprocess.CalledProcessError) as error:
+ context.certbot([
+ 'revoke', '--cert-name', cert,
+ '--cert-path', join(context.config_dir, 'live', cert, 'fullchain.pem')
+ ])
+ assert 'Exactly one of --cert-path or --cert-name must be specified' in error.out
+
+
+def test_revoke_multiple_lineages(context):
+ """Test revoke does not delete certs if multiple lineages share the same dir."""
+ cert1 = context.get_domain('le1')
+ context.certbot(['-d', cert1])
+
+ assert os.path.isfile(join(context.config_dir, 'renewal', '{0}.conf'.format(cert1)))
+
+ cert2 = context.get_domain('le2')
+ context.certbot(['-d', cert2])
+
+ # Copy over renewal configuration of cert1 into renewal configuration of cert2.
+ with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'r') as file:
+ data = file.read()
+
+ data = re.sub('archive_dir = .*\n',
+ 'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1).replace('\\', '\\\\')),
+ data)
+
+ with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file:
+ file.write(data)
+
+ output = context.certbot([
+ 'revoke', '--cert-path', join(context.config_dir, 'live', cert1, 'cert.pem')
+ ])
+
+ assert 'Not deleting revoked certs due to overlapping archive dirs' in output
+
+
+def test_wildcard_certificates(context):
+ """Test wildcard certificate issuance."""
+ if context.acme_server == 'boulder-v1':
+ pytest.skip('Wildcard certificates are not supported on ACME v1')
+
+ certname = context.get_domain('wild')
+
+ context.certbot([
+ '-a', 'manual', '-d', '*.{0},{0}'.format(certname),
+ '--preferred-challenge', 'dns',
+ '--manual-auth-hook', context.manual_dns_auth_hook,
+ '--manual-cleanup-hook', context.manual_dns_cleanup_hook
+ ])
+
+ assert exists(join(context.config_dir, 'live', certname, 'fullchain.pem'))
+
+
+def test_ocsp_status_stale(context):
+ """Test retrieval of OCSP statuses for staled config"""
+ sample_data_path = misc.load_sample_data_path(context.workspace)
+ output = context.certbot(['certificates', '--config-dir', sample_data_path])
+
+ assert output.count('TEST_CERT') == 2, ('Did not find two test certs as expected ({0})'
+ .format(output.count('TEST_CERT')))
+ assert output.count('EXPIRED') == 2, ('Did not find two expired certs as expected ({0})'
+ .format(output.count('EXPIRED')))
+
+
+def test_ocsp_status_live(context):
+ """Test retrieval of OCSP statuses for live config"""
+ cert = context.get_domain('ocsp-check')
+
+ # OSCP 1: Check live certificate OCSP status (VALID)
+ context.certbot(['--domains', cert])
+ output = context.certbot(['certificates'])
+
+ assert output.count('VALID') == 1, 'Expected {0} to be VALID'.format(cert)
+ assert output.count('EXPIRED') == 0, 'Did not expect {0} to be EXPIRED'.format(cert)
+
+ # OSCP 2: Check live certificate OCSP status (REVOKED)
+ context.certbot(['revoke', '--cert-name', cert, '--no-delete-after-revoke'])
+ # Sometimes in oldest tests (using openssl binary and not cryptography), the OCSP status is
+ # not seen immediately by Certbot as invalid. Waiting few seconds solves this transient issue.
+ time.sleep(5)
+ output = context.certbot(['certificates'])
+
+ assert output.count('INVALID') == 1, 'Expected {0} to be INVALID'.format(cert)
+ assert output.count('REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert)
+
+
+def test_dry_run_deactivate_authzs(context):
+ """Test that Certbot deactivates authorizations when performing a dry run"""
+
+ name = context.get_domain('dry-run-authz-deactivation')
+ args = ['certonly', '--cert-name', name, '-d', name, '--dry-run']
+ log_line = 'Recreating order after authz deactivation'
+
+ # First order will not need deactivation
+ context.certbot(args)
+ with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f:
+ assert log_line not in f.read(), 'First order should not have had any authz reuse'
+
+ # Second order will require deactivation
+ context.certbot(args)
+ with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f:
+ assert log_line in f.read(), 'Second order should have been recreated due to authz reuse'
diff --git a/certbot-ci/certbot_integration_tests/conftest.py b/certbot-ci/certbot_integration_tests/conftest.py
new file mode 100644
index 000000000..bb1d76e57
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/conftest.py
@@ -0,0 +1,96 @@
+"""
+General conftest for pytest execution of all integration tests lying
+in the certbot_integration tests package.
+As stated by pytest documentation, conftest module is used to set on
+for a directory a specific configuration using built-in pytest hooks.
+
+See https://docs.pytest.org/en/latest/reference.html#hook-reference
+"""
+from __future__ import print_function
+import contextlib
+import subprocess
+import sys
+
+from certbot_integration_tests.utils import acme_server as acme_lib
+
+
+def pytest_addoption(parser):
+ """
+ Standard pytest hook to add options to the pytest parser.
+ :param parser: current pytest parser that will be used on the CLI
+ """
+ parser.addoption('--acme-server', default='pebble',
+ choices=['boulder-v1', 'boulder-v2', 'pebble'],
+ help='select the ACME server to use (boulder-v1, boulder-v2, '
+ 'pebble), defaulting to pebble')
+
+
+def pytest_configure(config):
+ """
+ Standard pytest hook used to add a configuration logic for each node of a pytest run.
+ :param config: the current pytest configuration
+ """
+ if not hasattr(config, 'slaveinput'): # If true, this is the primary node
+ with _print_on_err():
+ config.acme_xdist = _setup_primary_node(config)
+
+
+def pytest_configure_node(node):
+ """
+ Standard pytest-xdist hook used to configure a worker node.
+ :param node: current worker node
+ """
+ node.slaveinput['acme_xdist'] = node.config.acme_xdist
+
+
+@contextlib.contextmanager
+def _print_on_err():
+ """
+ During pytest-xdist setup, stdout is used for nodes communication, so print is useless.
+ However, stderr is still available. This context manager transfers stdout to stderr
+ for the duration of the context, allowing to display prints to the user.
+ """
+ old_stdout = sys.stdout
+ sys.stdout = sys.stderr
+ try:
+ yield
+ finally:
+ sys.stdout = old_stdout
+
+
+def _setup_primary_node(config):
+ """
+ Setup the environment for integration tests.
+ Will:
+ - check runtime compatibility (Docker, docker-compose, Nginx)
+ - create a temporary workspace and the persistent GIT repositories space
+ - configure and start paralleled ACME CA servers using Docker
+ - transfer ACME CA servers configurations to pytest nodes using env variables
+ :param config: Configuration of the pytest primary node
+ """
+ # Check for runtime compatibility: some tools are required to be available in PATH
+ if 'boulder' in config.option.acme_server:
+ try:
+ subprocess.check_output(['docker', '-v'], stderr=subprocess.STDOUT)
+ except (subprocess.CalledProcessError, OSError):
+ raise ValueError('Error: docker is required in PATH to launch the integration tests on'
+ 'boulder, but is not installed or not available for current user.')
+
+ try:
+ subprocess.check_output(['docker-compose', '-v'], stderr=subprocess.STDOUT)
+ except (subprocess.CalledProcessError, OSError):
+ raise ValueError('Error: docker-compose is required in PATH to launch the integration tests, '
+ 'but is not installed or not available for current user.')
+
+ # Parameter numprocesses is added to option by pytest-xdist
+ workers = ['primary'] if not config.option.numprocesses\
+ else ['gw{0}'.format(i) for i in range(config.option.numprocesses)]
+
+ # By calling setup_acme_server we ensure that all necessary acme server instances will be
+ # fully started. This runtime is reflected by the acme_xdist returned.
+ acme_server = acme_lib.ACMEServer(config.option.acme_server, workers)
+ config.add_cleanup(acme_server.stop)
+ print('ACME xdist config:\n{0}'.format(acme_server.acme_xdist))
+ acme_server.start()
+
+ return acme_server.acme_xdist
diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/__init__.py b/certbot-ci/certbot_integration_tests/nginx_tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/nginx_tests/__init__.py
diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/context.py b/certbot-ci/certbot_integration_tests/nginx_tests/context.py
new file mode 100644
index 000000000..3a769840c
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/nginx_tests/context.py
@@ -0,0 +1,62 @@
+import os
+import subprocess
+
+from certbot_integration_tests.certbot_tests import context as certbot_context
+from certbot_integration_tests.nginx_tests import nginx_config as config
+from certbot_integration_tests.utils import certbot_call
+from certbot_integration_tests.utils import misc
+
+
+class IntegrationTestsContext(certbot_context.IntegrationTestsContext):
+ """General fixture describing a certbot-nginx integration tests context"""
+ def __init__(self, request):
+ super(IntegrationTestsContext, self).__init__(request)
+
+ self.nginx_root = os.path.join(self.workspace, 'nginx')
+ os.mkdir(self.nginx_root)
+
+ self.webroot = os.path.join(self.nginx_root, 'webroot')
+ os.mkdir(self.webroot)
+ with open(os.path.join(self.webroot, 'index.html'), 'w') as file_handler:
+ file_handler.write('Hello World!')
+
+ self.nginx_config_path = os.path.join(self.nginx_root, 'nginx.conf')
+ self.nginx_config = None
+
+ default_server = request.param['default_server']
+ self.process = self._start_nginx(default_server)
+
+ def cleanup(self):
+ self._stop_nginx()
+ super(IntegrationTestsContext, self).cleanup()
+
+ def certbot_test_nginx(self, args):
+ """
+ Main command to execute certbot using the nginx plugin.
+ :param list args: list of arguments to pass to nginx
+ :param bool force_renew: set to False to not renew by default
+ """
+ command = ['--authenticator', 'nginx', '--installer', 'nginx',
+ '--nginx-server-root', self.nginx_root]
+ command.extend(args)
+ return certbot_call.certbot_test(
+ command, self.directory_url, self.http_01_port, self.tls_alpn_01_port,
+ self.config_dir, self.workspace, force_renew=True)
+
+ def _start_nginx(self, default_server):
+ self.nginx_config = config.construct_nginx_config(
+ self.nginx_root, self.webroot, self.http_01_port, self.tls_alpn_01_port,
+ self.other_port, default_server, wtf_prefix=self.worker_id)
+ with open(self.nginx_config_path, 'w') as file:
+ file.write(self.nginx_config)
+
+ process = subprocess.Popen(['nginx', '-c', self.nginx_config_path, '-g', 'daemon off;'])
+
+ assert process.poll() is None
+ misc.check_until_timeout('http://localhost:{0}'.format(self.http_01_port))
+ return process
+
+ def _stop_nginx(self):
+ assert self.process.poll() is None
+ self.process.terminate()
+ self.process.wait()
diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py
new file mode 100644
index 000000000..18991ae62
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py
@@ -0,0 +1,126 @@
+"""General purpose nginx test configuration generator."""
+import getpass
+
+import pkg_resources
+
+
+def construct_nginx_config(nginx_root, nginx_webroot, http_port, https_port, other_port,
+ default_server, key_path=None, cert_path=None, wtf_prefix='le'):
+ """
+ This method returns a full nginx configuration suitable for integration tests.
+ :param str nginx_root: nginx root configuration path
+ :param str nginx_webroot: nginx webroot path
+ :param int http_port: HTTP port to listen on
+ :param int https_port: HTTPS port to listen on
+ :param int other_port: other HTTP port to listen on
+ :param bool default_server: True to set a default server in nginx config, False otherwise
+ :param str key_path: the path to a SSL key
+ :param str cert_path: the path to a SSL certificate
+ :param str wtf_prefix: the prefix to use in all domains handled by this nginx config
+ :return: a string containing the full nginx configuration
+ :rtype: str
+ """
+ key_path = key_path if key_path \
+ else pkg_resources.resource_filename('certbot_integration_tests', 'assets/key.pem')
+ cert_path = cert_path if cert_path \
+ else pkg_resources.resource_filename('certbot_integration_tests', 'assets/cert.pem')
+ return '''\
+# This error log will be written regardless of server scope error_log
+# definitions, so we have to set this here in the main scope.
+#
+# Even doing this, Nginx will still try to create the default error file, and
+# log a non-fatal error when it fails. After that things will work, however.
+error_log {nginx_root}/error.log;
+
+# The pidfile will be written to /var/run unless this is set.
+pid {nginx_root}/nginx.pid;
+
+user {user};
+worker_processes 1;
+
+events {{
+ worker_connections 1024;
+}}
+
+http {{
+ # Set an array of temp, cache and log file options that will otherwise default to
+ # restricted locations accessible only to root.
+ client_body_temp_path {nginx_root}/client_body;
+ fastcgi_temp_path {nginx_root}/fastcgi_temp;
+ proxy_temp_path {nginx_root}/proxy_temp;
+ #scgi_temp_path {nginx_root}/scgi_temp;
+ #uwsgi_temp_path {nginx_root}/uwsgi_temp;
+ access_log {nginx_root}/error.log;
+
+ # This should be turned off in a Virtualbox VM, as it can cause some
+ # interesting issues with data corruption in delivered files.
+ sendfile off;
+
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+ types_hash_max_size 2048;
+
+ #include /etc/nginx/mime.types;
+ index index.html index.htm index.php;
+
+ log_format main '$remote_addr - $remote_user [$time_local] $status '
+ '"$request" $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ default_type application/octet-stream;
+
+ server {{
+ # IPv4.
+ listen {http_port} {default_server};
+ # IPv6.
+ listen [::]:{http_port} {default_server};
+ server_name nginx.{wtf_prefix}.wtf nginx2.{wtf_prefix}.wtf;
+
+ root {nginx_webroot};
+
+ location / {{
+ # First attempt to serve request as file, then as directory, then fall
+ # back to index.html.
+ try_files $uri $uri/ /index.html;
+ }}
+ }}
+
+ server {{
+ listen {http_port};
+ listen [::]:{http_port};
+ server_name nginx3.{wtf_prefix}.wtf;
+
+ root {nginx_webroot};
+
+ location /.well-known/ {{
+ return 404;
+ }}
+
+ return 301 https://$host$request_uri;
+ }}
+
+ server {{
+ listen {other_port};
+ listen [::]:{other_port};
+ server_name nginx4.{wtf_prefix}.wtf nginx5.{wtf_prefix}.wtf;
+ }}
+
+ server {{
+ listen {http_port};
+ listen [::]:{http_port};
+ listen {https_port} ssl;
+ listen [::]:{https_port} ssl;
+ if ($scheme != "https") {{
+ return 301 https://$host$request_uri;
+ }}
+ server_name nginx6.{wtf_prefix}.wtf nginx7.{wtf_prefix}.wtf;
+
+ ssl_certificate {cert_path};
+ ssl_certificate_key {key_path};
+ }}
+}}
+'''.format(nginx_root=nginx_root, nginx_webroot=nginx_webroot, user=getpass.getuser(),
+ http_port=http_port, https_port=https_port, other_port=other_port,
+ default_server='default_server' if default_server else '', wtf_prefix=wtf_prefix,
+ key_path=key_path, cert_path=cert_path)
diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py b/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py
new file mode 100644
index 000000000..1a62ea8d7
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py
@@ -0,0 +1,54 @@
+"""Module executing integration tests against certbot with nginx plugin."""
+import os
+import ssl
+
+import pytest
+
+from certbot_integration_tests.nginx_tests import context as nginx_context
+
+
+@pytest.fixture()
+def context(request):
+ # Fixture request is a built-in pytest fixture describing current test request.
+ integration_test_context = nginx_context.IntegrationTestsContext(request)
+ try:
+ yield integration_test_context
+ finally:
+ integration_test_context.cleanup()
+
+
+@pytest.mark.parametrize('certname_pattern, params, context', [
+ ('nginx.{0}.wtf', ['run'], {'default_server': True}),
+ ('nginx2.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': True}),
+ # Overlapping location block and server-block-level return 301
+ ('nginx3.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': True}),
+ # No matching server block; default_server exists
+ ('nginx4.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': True}),
+ # No matching server block; default_server does not exist
+ ('nginx5.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': False}),
+ # Multiple domains, mix of matching and not
+ ('nginx6.{0}.wtf,nginx7.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': False}),
+], indirect=['context'])
+def test_certificate_deployment(certname_pattern, params, context):
+ # type: (str, list, nginx_context.IntegrationTestsContext) -> None
+ """
+ Test various scenarios to deploy a certificate to nginx using certbot.
+ """
+ domains = certname_pattern.format(context.worker_id)
+ command = ['--domains', domains]
+ command.extend(params)
+ context.certbot_test_nginx(command)
+
+ lineage = domains.split(',')[0]
+ server_cert = ssl.get_server_certificate(('localhost', context.tls_alpn_01_port))
+ with open(os.path.join(context.workspace, 'conf/live/{0}/cert.pem'.format(lineage)), 'r') as file:
+ certbot_cert = file.read()
+
+ assert server_cert == certbot_cert
+
+ context.certbot_test_nginx(['rollback', '--checkpoints', '1'])
+
+ with open(context.nginx_config_path, 'r') as file_h:
+ current_nginx_config = file_h.read()
+
+ assert context.nginx_config == current_nginx_config
diff --git a/certbot-ci/certbot_integration_tests/utils/__init__.py b/certbot-ci/certbot_integration_tests/utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/__init__.py
diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py
new file mode 100755
index 000000000..5483251e6
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+"""Module to setup an ACME CA server environment able to run multiple tests in parallel"""
+from __future__ import print_function
+
+import errno
+import json
+import os
+from os.path import join
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+
+import requests
+
+from certbot_integration_tests.utils import misc
+from certbot_integration_tests.utils import pebble_artifacts
+from certbot_integration_tests.utils import proxy
+from certbot_integration_tests.utils.constants import *
+
+
+class ACMEServer(object):
+ """
+ ACMEServer configures and handles the lifecycle of an ACME CA server and an HTTP reverse proxy
+ instance, to allow parallel execution of integration tests against the unique http-01 port
+ expected by the ACME CA server.
+ Typically all pytest integration tests will be executed in this context.
+ ACMEServer gives access the acme_xdist parameter, listing the ports and directory url to use
+ for each pytest node. It exposes also start and stop methods in order to start the stack, and
+ stop it with proper resources cleanup.
+ ACMEServer is also a context manager, and so can be used to ensure ACME server is started/stopped
+ upon context enter/exit.
+ """
+ def __init__(self, acme_server, nodes, http_proxy=True, stdout=False):
+ """
+ Create an ACMEServer instance.
+ :param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble)
+ :param list nodes: list of node names that will be setup by pytest xdist
+ :param bool http_proxy: if False do not start the HTTP proxy
+ :param bool stdout: if True stream subprocesses stdout to standard stdout
+ """
+ self._construct_acme_xdist(acme_server, nodes)
+
+ self._acme_type = 'pebble' if acme_server == 'pebble' else 'boulder'
+ self._proxy = http_proxy
+ self._workspace = tempfile.mkdtemp()
+ self._processes = []
+ self._stdout = sys.stdout if stdout else open(os.devnull, 'w')
+
+ def start(self):
+ """Start the test stack"""
+ try:
+ if self._proxy:
+ self._prepare_http_proxy()
+ if self._acme_type == 'pebble':
+ self._prepare_pebble_server()
+ if self._acme_type == 'boulder':
+ self._prepare_boulder_server()
+ except BaseException as e:
+ self.stop()
+ raise e
+
+ def stop(self):
+ """Stop the test stack, and clean its resources"""
+ print('=> Tear down the test infrastructure...')
+ try:
+ for process in self._processes:
+ try:
+ process.terminate()
+ except OSError as e:
+ # Process may be not started yet, so no PID and terminate fails.
+ # Then the process never started, and the situation is acceptable.
+ if e.errno != errno.ESRCH:
+ raise
+ for process in self._processes:
+ process.wait()
+
+ if os.path.exists(os.path.join(self._workspace, 'boulder')):
+ # Boulder docker generates build artifacts owned by root with 0o744 permissions.
+ # If we started the acme server from a normal user that has access to the Docker
+ # daemon, this user will not be able to delete these artifacts from the host.
+ # We need to do it through a docker.
+ process = self._launch_process(['docker', 'run', '--rm', '-v',
+ '{0}:/workspace'.format(self._workspace),
+ 'alpine', 'rm', '-rf', '/workspace/boulder'])
+ process.wait()
+ finally:
+ shutil.rmtree(self._workspace)
+ if self._stdout != sys.stdout:
+ self._stdout.close()
+ print('=> Test infrastructure stopped and cleaned up.')
+
+ def __enter__(self):
+ self.start()
+ return self.acme_xdist
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.stop()
+
+ def _construct_acme_xdist(self, acme_server, nodes):
+ """Generate and return the acme_xdist dict"""
+ acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT}
+
+ # Directory and ACME port are set implicitly in the docker-compose.yml files of Boulder/Pebble.
+ if acme_server == 'pebble':
+ acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL
+ else: # boulder
+ acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL \
+ if acme_server == 'boulder-v2' else BOULDER_V1_DIRECTORY_URL
+
+ acme_xdist['http_port'] = {node: port for (node, port)
+ in zip(nodes, range(5200, 5200 + len(nodes)))}
+ acme_xdist['https_port'] = {node: port for (node, port)
+ in zip(nodes, range(5100, 5100 + len(nodes)))}
+ acme_xdist['other_port'] = {node: port for (node, port)
+ in zip(nodes, range(5300, 5300 + len(nodes)))}
+
+ self.acme_xdist = acme_xdist
+
+ def _prepare_pebble_server(self):
+ """Configure and launch the Pebble server"""
+ print('=> Starting pebble instance deployment...')
+ pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts.fetch(self._workspace)
+
+ # Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid
+ # nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment.
+ environ = os.environ.copy()
+ environ['PEBBLE_VA_NOSLEEP'] = '1'
+ environ['PEBBLE_WFE_NONCEREJECT'] = '0'
+ environ['PEBBLE_AUTHZREUSE'] = '100'
+
+ self._launch_process(
+ [pebble_path, '-config', pebble_config_path, '-dnsserver', '127.0.0.1:8053'],
+ env=environ)
+
+ self._launch_process(
+ [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT), '-defaultIPv6', '""',
+ '-defaultIPv4', '127.0.0.1', '-http01', '""', '-tlsalpn01', '""', '-https01', '""'])
+
+ # pebble_ocsp_server is imported here and not at the top of module in order to avoid a useless
+ # ImportError, in the case where cryptography dependency is too old to support ocsp, but
+ # Boulder is used instead of Pebble, so pebble_ocsp_server is not used. This is the typical
+ # situation of integration-certbot-oldest tox testenv.
+ from certbot_integration_tests.utils import pebble_ocsp_server
+ self._launch_process([sys.executable, pebble_ocsp_server.__file__])
+
+ # Wait for the ACME CA server to be up.
+ print('=> Waiting for pebble instance to respond...')
+ misc.check_until_timeout(self.acme_xdist['directory_url'])
+
+ print('=> Finished pebble instance deployment.')
+
+ def _prepare_boulder_server(self):
+ """Configure and launch the Boulder server"""
+ print('=> Starting boulder instance deployment...')
+ instance_path = join(self._workspace, 'boulder')
+
+ # Load Boulder from git, that includes a docker-compose.yml ready for production.
+ process = self._launch_process(['git', 'clone', 'https://github.com/letsencrypt/boulder',
+ '--single-branch', '--depth=1', instance_path])
+ process.wait()
+
+ # Allow Boulder to ignore usual limit rate policies, useful for tests.
+ os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'),
+ join(instance_path, 'test/rate-limit-policies.yml'))
+
+ # Launch the Boulder server
+ self._launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path)
+
+ # Wait for the ACME CA server to be up.
+ print('=> Waiting for boulder instance to respond...')
+ misc.check_until_timeout(self.acme_xdist['directory_url'], attempts=240)
+
+ # Configure challtestsrv to answer any A record request with ip of the docker host.
+ response = requests.post('http://localhost:{0}/set-default-ipv4'.format(CHALLTESTSRV_PORT),
+ json={'ip': '10.77.77.1'})
+ response.raise_for_status()
+
+ print('=> Finished boulder instance deployment.')
+
+ def _prepare_http_proxy(self):
+ """Configure and launch an HTTP proxy"""
+ print('=> Configuring the HTTP proxy...')
+ mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port)
+ for node, port in self.acme_xdist['http_port'].items()}
+ command = [sys.executable, proxy.__file__, str(HTTP_01_PORT), json.dumps(mapping)]
+ self._launch_process(command)
+ print('=> Finished configuring the HTTP proxy.')
+
+ def _launch_process(self, command, cwd=os.getcwd(), env=None):
+ """Launch silently a subprocess OS command"""
+ if not env:
+ env = os.environ
+ process = subprocess.Popen(command, stdout=self._stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env)
+ self._processes.append(process)
+ return process
+
+
+def main():
+ args = sys.argv[1:]
+ server_type = args[0] if args else 'pebble'
+ possible_values = ('pebble', 'boulder-v1', 'boulder-v2')
+ if server_type not in possible_values:
+ raise ValueError('Invalid server value {0}, should be one of {1}'
+ .format(server_type, possible_values))
+
+ acme_server = ACMEServer(server_type, [], http_proxy=False, stdout=True)
+
+ try:
+ with acme_server as acme_xdist:
+ print('--> Instance of {0} is running, directory URL is {0}'
+ .format(acme_xdist['directory_url']))
+ print('--> Press CTRL+C to stop the ACME server.')
+
+ while True:
+ time.sleep(3600)
+ except KeyboardInterrupt:
+ pass
+
+
+if __name__ == '__main__':
+ main()
diff --git a/certbot-ci/certbot_integration_tests/utils/certbot_call.py b/certbot-ci/certbot_integration_tests/utils/certbot_call.py
new file mode 100755
index 000000000..2ddaa41c8
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/certbot_call.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+"""Module to call certbot in test mode"""
+from __future__ import absolute_import
+
+from distutils.version import LooseVersion
+import os
+import subprocess
+import sys
+
+import certbot_integration_tests
+from certbot_integration_tests.utils.constants import *
+
+
+def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
+ config_dir, workspace, force_renew=True):
+ """
+ Invoke the certbot executable available in PATH in a test context for the given args.
+ The test context consists in running certbot in debug mode, with various flags suitable
+ for tests (eg. no ssl check, customizable ACME challenge ports and config directory ...).
+ This command captures stdout and returns it to the caller.
+ :param list certbot_args: the arguments to pass to the certbot executable
+ :param str directory_url: URL of the ACME directory server to use
+ :param int http_01_port: port for the HTTP-01 challenges
+ :param int tls_alpn_01_port: port for the TLS-ALPN-01 challenges
+ :param str config_dir: certbot configuration directory to use
+ :param str workspace: certbot current directory to use
+ :param bool force_renew: set False to not force renew existing certificates (default: True)
+ :return: stdout as string
+ :rtype: str
+ """
+ command, env = _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
+ config_dir, workspace, force_renew)
+
+ return subprocess.check_output(command, universal_newlines=True, cwd=workspace, env=env)
+
+
+def _prepare_environ(workspace):
+ new_environ = os.environ.copy()
+ new_environ['TMPDIR'] = workspace
+
+ # So, pytest is nice, and a little too nice for our usage.
+ # In order to help user to call seamlessly any piece of python code without requiring to
+ # install it as a full-fledged setuptools distribution for instance, it may inject the path
+ # to the test files into the PYTHONPATH. This allows the python interpreter to import
+ # as modules any python file available at this path.
+ # See https://docs.pytest.org/en/3.2.5/pythonpath.html for the explanation and description.
+ # However this behavior is not good in integration tests, in particular the nginx oldest ones.
+ # Indeed during these kind of tests certbot is installed as a transitive dependency to
+ # certbot-nginx. Here is the trick: this certbot version is not necessarily the same as
+ # the certbot codebase lying in current working directory. For instance in oldest tests
+ # certbot==0.36.0 may be installed while the codebase corresponds to certbot==0.37.0.dev0.
+ # Then during a pytest run, PYTHONPATH contains the path to the Certbot codebase, so invoking
+ # certbot will import the modules from the codebase (0.37.0.dev0), not from the
+ # required/installed version (0.36.0).
+ # This will lead to funny and totally incomprehensible errors. To avoid that, we ensure that
+ # if PYTHONPATH is set, it does not contain the path to the root of the codebase.
+ if new_environ.get('PYTHONPATH'):
+ # certbot_integration_tests.__file__ is:
+ # '/path/to/certbot/certbot-ci/certbot_integration_tests/__init__.pyc'
+ # ... and we want '/path/to/certbot'
+ certbot_root = os.path.dirname(os.path.dirname(os.path.dirname(certbot_integration_tests.__file__)))
+ python_paths = [path for path in new_environ['PYTHONPATH'].split(':') if path != certbot_root]
+ new_environ['PYTHONPATH'] = ':'.join(python_paths)
+
+ return new_environ
+
+
+def _compute_additional_args(workspace, environ, force_renew):
+ additional_args = []
+ output = subprocess.check_output(['certbot', '--version'],
+ universal_newlines=True, stderr=subprocess.STDOUT,
+ cwd=workspace, env=environ)
+ version_str = output.split(' ')[1].strip() # Typical response is: output = 'certbot 0.31.0.dev0'
+ if LooseVersion(version_str) >= LooseVersion('0.30.0'):
+ additional_args.append('--no-random-sleep-on-renew')
+
+ if force_renew:
+ additional_args.append('--renew-by-default')
+
+ return additional_args
+
+
+def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
+ config_dir, workspace, force_renew):
+
+ new_environ = _prepare_environ(workspace)
+ additional_args = _compute_additional_args(workspace, new_environ, force_renew)
+
+ command = [
+ 'certbot',
+ '--server', directory_url,
+ '--no-verify-ssl',
+ '--http-01-port', str(http_01_port),
+ '--https-port', str(tls_alpn_01_port),
+ '--manual-public-ip-logging-ok',
+ '--config-dir', config_dir,
+ '--work-dir', os.path.join(workspace, 'work'),
+ '--logs-dir', os.path.join(workspace, 'logs'),
+ '--non-interactive',
+ '--no-redirect',
+ '--agree-tos',
+ '--register-unsafely-without-email',
+ '--debug',
+ '-vv'
+ ]
+
+ command.extend(certbot_args)
+ command.extend(additional_args)
+
+ print('--> Invoke command:\n=====\n{0}\n====='.format(subprocess.list2cmdline(command)))
+
+ return command, new_environ
+
+
+def main():
+ args = sys.argv[1:]
+
+ # Default config is pebble
+ directory_url = os.environ.get('SERVER', PEBBLE_DIRECTORY_URL)
+ http_01_port = int(os.environ.get('HTTP_01_PORT', HTTP_01_PORT))
+ tls_alpn_01_port = int(os.environ.get('TLS_ALPN_01_PORT', TLS_ALPN_01_PORT))
+
+ # Execution of certbot in a self-contained workspace
+ workspace = os.environ.get('WORKSPACE', os.path.join(os.getcwd(), '.certbot_test_workspace'))
+ if not os.path.exists(workspace):
+ print('--> Creating a workspace for certbot_test: {0}'.format(workspace))
+ os.mkdir(workspace)
+ else:
+ print('--> Using an existing workspace for certbot_test: {0}'.format(workspace))
+ config_dir = os.path.join(workspace, 'conf')
+
+ # Invoke certbot in test mode, without capturing output so users see directly the outcome.
+ command, env = _prepare_args_env(args, directory_url, http_01_port, tls_alpn_01_port,
+ config_dir, workspace, True)
+ subprocess.check_call(command, universal_newlines=True, cwd=workspace, env=env)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/certbot-ci/certbot_integration_tests/utils/constants.py b/certbot-ci/certbot_integration_tests/utils/constants.py
new file mode 100644
index 000000000..dfdeda411
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/constants.py
@@ -0,0 +1,9 @@
+"""Some useful constants to use throughout certbot-ci integration tests"""
+HTTP_01_PORT = 5002
+TLS_ALPN_01_PORT = 5001
+CHALLTESTSRV_PORT = 8055
+BOULDER_V1_DIRECTORY_URL = 'http://localhost:4000/directory'
+BOULDER_V2_DIRECTORY_URL = 'http://localhost:4001/directory'
+PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir'
+PEBBLE_MANAGEMENT_URL = 'https://localhost:15000'
+MOCK_OCSP_SERVER_PORT = 4002
diff --git a/certbot-ci/certbot_integration_tests/utils/misc.py b/certbot-ci/certbot_integration_tests/utils/misc.py
new file mode 100644
index 000000000..b08f11e89
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/misc.py
@@ -0,0 +1,302 @@
+"""
+Misc module contains stateless functions that could be used during pytest execution,
+or outside during setup/teardown of the integration tests environment.
+"""
+import contextlib
+import errno
+import multiprocessing
+import os
+import re
+import shutil
+import stat
+import sys
+import tempfile
+import time
+import warnings
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.serialization import Encoding
+from cryptography.hazmat.primitives.serialization import NoEncryption
+from cryptography.hazmat.primitives.serialization import PrivateFormat
+from OpenSSL import crypto
+import pkg_resources
+import requests
+from six.moves import SimpleHTTPServer
+from six.moves import socketserver
+
+RSA_KEY_TYPE = 'rsa'
+ECDSA_KEY_TYPE = 'ecdsa'
+
+
+def check_until_timeout(url, attempts=30):
+ """
+ Wait and block until given url responds with status 200, or raise an exception
+ after the specified number of attempts.
+ :param str url: the URL to test
+ :param int attempts: the number of times to try to connect to the URL
+ :raise ValueError: exception raised if unable to reach the URL
+ """
+ try:
+ import urllib3
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+ except ImportError:
+ # Handle old versions of request with vendorized urllib3
+ from requests.packages.urllib3.exceptions import InsecureRequestWarning
+ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+
+ for _ in range(attempts):
+ time.sleep(1)
+ try:
+ if requests.get(url, verify=False).status_code == 200:
+ return
+ except requests.exceptions.ConnectionError:
+ pass
+
+ raise ValueError('Error, url did not respond after {0} attempts: {1}'.format(attempts, url))
+
+
+class GracefulTCPServer(socketserver.TCPServer):
+ """
+ This subclass of TCPServer allows graceful reuse of an address that has
+ just been released by another instance of TCPServer.
+ """
+ allow_reuse_address = True
+
+
+def _run_server(port):
+ GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever()
+
+
+@contextlib.contextmanager
+def create_http_server(port):
+ """
+ Setup and start an HTTP server for the given TCP port.
+ This server stays active for the lifetime of the context, and is automatically
+ stopped with context exit, while its temporary webroot is deleted.
+ :param int port: the TCP port to use
+ :return str: the temporary webroot attached to this server
+ """
+ current_cwd = os.getcwd()
+ webroot = tempfile.mkdtemp()
+
+ process = multiprocessing.Process(target=_run_server, args=(port,))
+
+ try:
+ # SimpleHTTPServer is designed to serve files from the current working directory at the
+ # time it starts. So we temporarily change the cwd to our crafted webroot before launch.
+ try:
+ os.chdir(webroot)
+ process.start()
+ finally:
+ os.chdir(current_cwd)
+
+ check_until_timeout('http://localhost:{0}/'.format(port))
+
+ yield webroot
+ finally:
+ try:
+ if process.is_alive():
+ process.terminate()
+ process.join() # Block until process is effectively terminated
+ finally:
+ shutil.rmtree(webroot)
+
+
+def list_renewal_hooks_dirs(config_dir):
+ """
+ Find and return paths of all hook directories for the given certbot config directory
+ :param str config_dir: path to the certbot config directory
+ :return str[]: list of path to the standard hooks directory for this certbot instance
+ """
+ renewal_hooks_root = os.path.join(config_dir, 'renewal-hooks')
+ return [os.path.join(renewal_hooks_root, item) for item in ['pre', 'deploy', 'post']]
+
+
+def generate_test_file_hooks(config_dir, hook_probe):
+ """
+ Create a suite of certbot hook scripts and put them in the relevant hook directory
+ for the given certbot configuration directory. These scripts, when executed, will write
+ specific verbs in the given hook_probe file to allow asserting they have effectively
+ been executed. The deploy hook also checks that the renewal environment variables are set.
+ :param str config_dir: current certbot config directory
+ :param hook_probe: path to the hook probe to test hook scripts execution
+ """
+ hook_path = pkg_resources.resource_filename('certbot_integration_tests', 'assets/hook.py')
+
+ for hook_dir in list_renewal_hooks_dirs(config_dir):
+ # We want an equivalent of bash `chmod -p $HOOK_DIR, that does not fail if one folder of
+ # the hierarchy already exists. It is not the case of os.makedirs. Python 3 has an
+ # optional parameter `exists_ok` to not fail on existing dir, but Python 2.7 does not.
+ # So we pass through a try except pass for it. To be removed with dropped support on py27.
+ try:
+ os.makedirs(hook_dir)
+ except OSError as error:
+ if error.errno != errno.EEXIST:
+ raise
+
+ if os.name != 'nt':
+ entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.sh')
+ entrypoint_script = '''\
+#!/usr/bin/env bash
+set -e
+"{0}" "{1}" "{2}" "{3}"
+'''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)
+ else:
+ entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.bat')
+ entrypoint_script = '''\
+@echo off
+"{0}" "{1}" "{2}" "{3}"
+ '''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)
+
+ with open(entrypoint_script_path, 'w') as file_h:
+ file_h.write(entrypoint_script)
+
+ os.chmod(entrypoint_script_path, os.stat(entrypoint_script_path).st_mode | stat.S_IEXEC)
+
+
+@contextlib.contextmanager
+def manual_http_hooks(http_server_root, http_port):
+ """
+ Generate suitable http-01 hooks command for test purpose in the given HTTP
+ server webroot directory. These hooks command use temporary python scripts
+ that are deleted upon context exit.
+ :param str http_server_root: path to the HTTP server configured to serve http-01 challenges
+ :param int http_port: HTTP port that the HTTP server listen on
+ :return (str, str): a tuple containing the authentication hook and cleanup hook commands
+ """
+ tempdir = tempfile.mkdtemp()
+ try:
+ auth_script_path = os.path.join(tempdir, 'auth.py')
+ with open(auth_script_path, 'w') as file_h:
+ file_h.write('''\
+#!/usr/bin/env python
+import os
+import requests
+import time
+import sys
+challenge_dir = os.path.join('{0}', '.well-known', 'acme-challenge')
+os.makedirs(challenge_dir)
+challenge_file = os.path.join(challenge_dir, os.environ.get('CERTBOT_TOKEN'))
+with open(challenge_file, 'w') as file_h:
+ file_h.write(os.environ.get('CERTBOT_VALIDATION'))
+url = 'http://localhost:{1}/.well-known/acme-challenge/' + os.environ.get('CERTBOT_TOKEN')
+for _ in range(0, 10):
+ time.sleep(1)
+ try:
+ if request.get(url).status_code == 200:
+ sys.exit(0)
+ except requests.exceptions.ConnectionError:
+ pass
+raise ValueError('Error, url did not respond after 10 attempts: {{0}}'.format(url))
+'''.format(http_server_root.replace('\\', '\\\\'), http_port))
+ os.chmod(auth_script_path, 0o755)
+
+ cleanup_script_path = os.path.join(tempdir, 'cleanup.py')
+ with open(cleanup_script_path, 'w') as file_h:
+ file_h.write('''\
+#!/usr/bin/env python
+import os
+import shutil
+well_known = os.path.join('{0}', '.well-known')
+shutil.rmtree(well_known)
+'''.format(http_server_root.replace('\\', '\\\\')))
+ os.chmod(cleanup_script_path, 0o755)
+
+ yield ('{0} {1}'.format(sys.executable, auth_script_path),
+ '{0} {1}'.format(sys.executable, cleanup_script_path))
+ finally:
+ shutil.rmtree(tempdir)
+
+
+def generate_csr(domains, key_path, csr_path, key_type=RSA_KEY_TYPE):
+ """
+ Generate a private key, and a CSR for the given domains using this key.
+ :param domains: the domain names to include in the CSR
+ :type domains: `list` of `str`
+ :param str key_path: path to the private key that will be generated
+ :param str csr_path: path to the CSR that will be generated
+ :param str key_type: type of the key (misc.RSA_KEY_TYPE or misc.ECDSA_KEY_TYPE)
+ """
+ if key_type == RSA_KEY_TYPE:
+ key = crypto.PKey()
+ key.generate_key(crypto.TYPE_RSA, 2048)
+ elif key_type == ECDSA_KEY_TYPE:
+ with warnings.catch_warnings():
+ # Ignore a warning on some old versions of cryptography
+ warnings.simplefilter('ignore', category=PendingDeprecationWarning)
+ key = ec.generate_private_key(ec.SECP384R1(), default_backend())
+ key = key.private_bytes(encoding=Encoding.PEM, format=PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=NoEncryption())
+ key = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
+ else:
+ raise ValueError('Invalid key type: {0}'.format(key_type))
+
+ with open(key_path, 'wb') as file_h:
+ file_h.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
+
+ req = crypto.X509Req()
+ san = ', '.join(['DNS:{0}'.format(item) for item in domains])
+ san_constraint = crypto.X509Extension(b'subjectAltName', False, san.encode('utf-8'))
+ req.add_extensions([san_constraint])
+
+ req.set_pubkey(key)
+ req.set_version(2)
+ req.sign(key, 'sha256')
+
+ with open(csr_path, 'wb') as file_h:
+ file_h.write(crypto.dump_certificate_request(crypto.FILETYPE_ASN1, req))
+
+
+def read_certificate(cert_path):
+ """
+ Load the certificate from the provided path, and return a human readable version of it (TEXT mode).
+ :param str cert_path: the path to the certificate
+ :returns: the TEXT version of the certificate, as it would be displayed by openssl binary
+ """
+ with open(cert_path, 'rb') as file:
+ data = file.read()
+
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, data)
+ return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert).decode('utf-8')
+
+
+def load_sample_data_path(workspace):
+ """
+ Load the certbot configuration example designed to make OCSP tests, and return its path
+ :param str workspace: current test workspace directory path
+ :returns: the path to the loaded sample data directory
+ :rtype: str
+ """
+ original = pkg_resources.resource_filename('certbot_integration_tests', 'assets/sample-config')
+ copied = os.path.join(workspace, 'sample-config')
+ shutil.copytree(original, copied, symlinks=True)
+
+ if os.name == 'nt':
+ # Fix the symlinks on Windows since GIT is not creating them upon checkout
+ for lineage in ['a.encryption-example.com', 'b.encryption-example.com']:
+ current_live = os.path.join(copied, 'live', lineage)
+ for name in os.listdir(current_live):
+ if name != 'README':
+ current_file = os.path.join(current_live, name)
+ with open(current_file) as file_h:
+ src = file_h.read()
+ os.unlink(current_file)
+ os.symlink(os.path.join(current_live, src), current_file)
+
+ return copied
+
+
+def echo(keyword, path=None):
+ """
+ Generate a platform independent executable command
+ that echoes the given keyword into the given file.
+ :param keyword: the keyword to echo (must be a single keyword)
+ :param path: path to the file were keyword is echoed
+ :return: the executable command
+ """
+ if not re.match(r'^\w+$', keyword):
+ raise ValueError('Error, keyword `{0}` is not a single keyword.'
+ .format(keyword))
+ return '{0} -c "from __future__ import print_function; print(\'{1}\')"{2}'.format(
+ os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '')
diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py
new file mode 100644
index 000000000..2b1557928
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py
@@ -0,0 +1,53 @@
+import json
+import os
+import stat
+
+import pkg_resources
+import requests
+
+from certbot_integration_tests.utils.constants import MOCK_OCSP_SERVER_PORT
+
+PEBBLE_VERSION = 'v2.2.1'
+ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'assets')
+
+
+def fetch(workspace):
+ suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe'
+
+ pebble_path = _fetch_asset('pebble', suffix)
+ challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix)
+ pebble_config_path = _build_pebble_config(workspace)
+
+ return pebble_path, challtestsrv_path, pebble_config_path
+
+
+def _fetch_asset(asset, suffix):
+ asset_path = os.path.join(ASSETS_PATH, '{0}_{1}_{2}'.format(asset, PEBBLE_VERSION, suffix))
+ if not os.path.exists(asset_path):
+ asset_url = ('https://github.com/letsencrypt/pebble/releases/download/{0}/{1}_{2}'
+ .format(PEBBLE_VERSION, asset, suffix))
+ response = requests.get(asset_url)
+ response.raise_for_status()
+ with open(asset_path, 'wb') as file_h:
+ file_h.write(response.content)
+ os.chmod(asset_path, os.stat(asset_path).st_mode | stat.S_IEXEC)
+
+ return asset_path
+
+
+def _build_pebble_config(workspace):
+ config_path = os.path.join(workspace, 'pebble-config.json')
+ with open(config_path, 'w') as file_h:
+ file_h.write(json.dumps({
+ 'pebble': {
+ 'listenAddress': '0.0.0.0:14000',
+ 'managementListenAddress': '0.0.0.0:15000',
+ 'certificate': os.path.join(ASSETS_PATH, 'cert.pem'),
+ 'privateKey': os.path.join(ASSETS_PATH, 'key.pem'),
+ 'httpPort': 5002,
+ 'tlsPort': 5001,
+ 'ocspResponderURL': 'http://127.0.0.1:{0}'.format(MOCK_OCSP_SERVER_PORT),
+ },
+ }))
+
+ return config_path
diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py
new file mode 100755
index 000000000..9458560e8
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+"""
+This runnable module interfaces itself with the Pebble management interface in order
+to serve a mock OCSP responder during integration tests against Pebble.
+"""
+import datetime
+import re
+
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import serialization
+from cryptography.x509 import ocsp
+from dateutil import parser
+import requests
+from six.moves import BaseHTTPServer
+
+from certbot_integration_tests.utils.constants import MOCK_OCSP_SERVER_PORT
+from certbot_integration_tests.utils.constants import PEBBLE_MANAGEMENT_URL
+from certbot_integration_tests.utils.misc import GracefulTCPServer
+
+
+class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ def do_POST(self):
+ request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0', verify=False)
+ issuer_key = serialization.load_pem_private_key(request.content, None, default_backend())
+
+ request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/0', verify=False)
+ issuer_cert = x509.load_pem_x509_certificate(request.content, default_backend())
+
+ try:
+ content_len = int(self.headers.getheader('content-length', 0))
+ except AttributeError:
+ content_len = int(self.headers.get('Content-Length'))
+
+ ocsp_request = ocsp.load_der_ocsp_request(self.rfile.read(content_len))
+ response = requests.get('{0}/cert-status-by-serial/{1}'.format(
+ PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')), verify=False)
+
+ if not response.ok:
+ ocsp_response = ocsp.OCSPResponseBuilder.build_unsuccessful(ocsp.OCSPResponseStatus.UNAUTHORIZED)
+ else:
+ data = response.json()
+
+ now = datetime.datetime.utcnow()
+ cert = x509.load_pem_x509_certificate(data['Certificate'].encode(), default_backend())
+ if data['Status'] != 'Revoked':
+ ocsp_status, revocation_time, revocation_reason = ocsp.OCSPCertStatus.GOOD, None, None
+ else:
+ ocsp_status, revocation_reason = ocsp.OCSPCertStatus.REVOKED, x509.ReasonFlags.unspecified
+ revoked_at = re.sub(r'( \+\d{4}).*$', r'\1', data['RevokedAt']) # "... +0000 UTC" => "+0000"
+ revocation_time = parser.parse(revoked_at)
+
+ ocsp_response = ocsp.OCSPResponseBuilder().add_response(
+ cert=cert, issuer=issuer_cert, algorithm=hashes.SHA1(),
+ cert_status=ocsp_status,
+ this_update=now, next_update=now + datetime.timedelta(hours=1),
+ revocation_time=revocation_time, revocation_reason=revocation_reason
+ ).responder_id(
+ ocsp.OCSPResponderEncoding.NAME, issuer_cert
+ ).sign(issuer_key, hashes.SHA256())
+
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(ocsp_response.public_bytes(serialization.Encoding.DER))
+
+
+if __name__ == '__main__':
+ try:
+ GracefulTCPServer(('', MOCK_OCSP_SERVER_PORT), _ProxyHandler).serve_forever()
+ except KeyboardInterrupt:
+ pass
diff --git a/certbot-ci/certbot_integration_tests/utils/proxy.py b/certbot-ci/certbot_integration_tests/utils/proxy.py
new file mode 100644
index 000000000..3a16adebf
--- /dev/null
+++ b/certbot-ci/certbot_integration_tests/utils/proxy.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+import json
+import re
+import sys
+
+import requests
+from six.moves import BaseHTTPServer
+
+from certbot_integration_tests.utils.misc import GracefulTCPServer
+
+
+def _create_proxy(mapping):
+ class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ def do_GET(self):
+ headers = {key.lower(): value for key, value in self.headers.items()}
+ backend = [backend for pattern, backend in mapping.items()
+ if re.match(pattern, headers['host'])][0]
+ response = requests.get(backend + self.path, headers=headers)
+
+ self.send_response(response.status_code)
+ for key, value in response.headers.items():
+ self.send_header(key, value)
+ self.end_headers()
+ self.wfile.write(response.content)
+
+ return ProxyHandler
+
+
+if __name__ == '__main__':
+ http_port = int(sys.argv[1])
+ port_mapping = json.loads(sys.argv[2])
+ httpd = GracefulTCPServer(('', http_port), _create_proxy(port_mapping))
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ pass