From 56aeab4c29588703a6189c558a17c66ece656d97 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 6 Sep 2017 07:37:38 +0200
Subject: [PATCH 01/93] ci: add docker file to automatically build Jessie image
 for acme-dns-tiny

---
 gitlab-ci/docker/jessie/Dockerfile | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 gitlab-ci/docker/jessie/Dockerfile

diff --git a/gitlab-ci/docker/jessie/Dockerfile b/gitlab-ci/docker/jessie/Dockerfile
new file mode 100644
index 0000000..e9790c8
--- /dev/null
+++ b/gitlab-ci/docker/jessie/Dockerfile
@@ -0,0 +1,19 @@
+FROM jessie
+
+RUN apt-get update
+RUN apt-get upgrade
+
+# Minimal tools required by acme-dns-tiny CI
+RUN apt-get install -y \
+	python3-dnspython \
+	python3-coverage \
+	python3-pip
+
+# Allows run python3-coverage with same command than manual install by pip
+RUN update-alternatives --install \
+	/usr/bin/python3-coverage \
+	coverage \
+	/usr/bin/python3.4-coverage \
+	1
+
+RUN ln -s /etc/alternatives/coverage /usr/bin/coverage
-- 
GitLab


From 720a5938f099d7c13c6b4359694d099f07170cfa Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 6 Sep 2017 07:37:51 +0200
Subject: [PATCH 02/93] ci: add docker file to automatically build Jessie
 Backports image for acme-dns-tiny

---
 gitlab-ci/docker/jessie-backports/Dockerfile | 34 ++++++++++++++++++++
 1 file changed, 34 insertions(+)
 create mode 100644 gitlab-ci/docker/jessie-backports/Dockerfile

diff --git a/gitlab-ci/docker/jessie-backports/Dockerfile b/gitlab-ci/docker/jessie-backports/Dockerfile
new file mode 100644
index 0000000..627c156
--- /dev/null
+++ b/gitlab-ci/docker/jessie-backports/Dockerfile
@@ -0,0 +1,34 @@
+FROM jessie
+
+RUN apt-get update
+RUN apt-get upgrade
+
+# Add Jessie backports source
+RUN apt-get install -y \
+	software-properties-common
+
+RUN add-apt-repository \
+	"deb http://httpredir.debian.org/debian" \
+	"jessie-backports" \
+	"main"
+
+RUN apt-get update
+
+# Minimal tools required by acme-dns-tiny CI
+RUN apt-get install -y \
+	python3-coverage \
+	python3-pip
+
+RUN apt-get install -y \
+	-t jessie-backports \
+	python3-dnspython
+
+
+# Allows run python3-coverage with same command than manual install by pip
+RUN update-alternatives --install \
+	/usr/bin/python3-coverage \
+	coverage \
+	/usr/bin/python3.4-coverage \
+	1
+
+RUN ln -s /etc/alternatives/coverage /usr/bin/coverage
-- 
GitLab


From d1b38296d0dace734a243b3332fe12a93b7cf1be Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 6 Sep 2017 07:38:05 +0200
Subject: [PATCH 03/93] ci: add docker file to automatically build Stretch
 image for acme-dns-tiny

---
 gitlab-ci/docker/stretch/Dockerfile | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 gitlab-ci/docker/stretch/Dockerfile

diff --git a/gitlab-ci/docker/stretch/Dockerfile b/gitlab-ci/docker/stretch/Dockerfile
new file mode 100644
index 0000000..559cd0a
--- /dev/null
+++ b/gitlab-ci/docker/stretch/Dockerfile
@@ -0,0 +1,19 @@
+FROM stretch
+
+RUN apt-get update
+RUN apt-get upgrade
+
+# Minimal tools required by acme-dns-tiny CI
+RUN apt-get install -y \
+	python3-dnspython \
+	python3-coverage \
+	python3-pip
+
+# Allows run python3-coverage with same command than manual install by pip
+RUN update-alternatives --install \
+	/usr/bin/python3-coverage \
+	coverage \
+	/usr/bin/python3.4-coverage \
+	1
+
+RUN ln -s /etc/alternatives/coverage /usr/bin/coverage
-- 
GitLab


From 4a855b7f5594a3641e9a166db26b7ba9a7ca1b80 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 8 Sep 2017 22:27:48 +0200
Subject: [PATCH 04/93] docker: use official Debian images

---
 gitlab-ci/docker/jessie-backports/Dockerfile | 13 +------------
 gitlab-ci/docker/jessie/Dockerfile           |  2 +-
 gitlab-ci/docker/stretch/Dockerfile          |  2 +-
 3 files changed, 3 insertions(+), 14 deletions(-)

diff --git a/gitlab-ci/docker/jessie-backports/Dockerfile b/gitlab-ci/docker/jessie-backports/Dockerfile
index 627c156..520e3f8 100644
--- a/gitlab-ci/docker/jessie-backports/Dockerfile
+++ b/gitlab-ci/docker/jessie-backports/Dockerfile
@@ -1,19 +1,8 @@
-FROM jessie
+FROM debian:jessie-backports
 
 RUN apt-get update
 RUN apt-get upgrade
 
-# Add Jessie backports source
-RUN apt-get install -y \
-	software-properties-common
-
-RUN add-apt-repository \
-	"deb http://httpredir.debian.org/debian" \
-	"jessie-backports" \
-	"main"
-
-RUN apt-get update
-
 # Minimal tools required by acme-dns-tiny CI
 RUN apt-get install -y \
 	python3-coverage \
diff --git a/gitlab-ci/docker/jessie/Dockerfile b/gitlab-ci/docker/jessie/Dockerfile
index e9790c8..3fa7b07 100644
--- a/gitlab-ci/docker/jessie/Dockerfile
+++ b/gitlab-ci/docker/jessie/Dockerfile
@@ -1,4 +1,4 @@
-FROM jessie
+FROM debian:jessie
 
 RUN apt-get update
 RUN apt-get upgrade
diff --git a/gitlab-ci/docker/stretch/Dockerfile b/gitlab-ci/docker/stretch/Dockerfile
index 559cd0a..da04dfd 100644
--- a/gitlab-ci/docker/stretch/Dockerfile
+++ b/gitlab-ci/docker/stretch/Dockerfile
@@ -1,4 +1,4 @@
-FROM stretch
+FROM debian:stretch
 
 RUN apt-get update
 RUN apt-get upgrade
-- 
GitLab


From 525119aa9141d2b9417f6562e15c32fca3a3ba5a Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 8 Sep 2017 22:28:23 +0200
Subject: [PATCH 05/93] docker: jessie-backports and stretch can get
 configarparse from Debian directly

---
 gitlab-ci/docker/jessie-backports/Dockerfile | 1 +
 gitlab-ci/docker/stretch/Dockerfile          | 1 +
 2 files changed, 2 insertions(+)

diff --git a/gitlab-ci/docker/jessie-backports/Dockerfile b/gitlab-ci/docker/jessie-backports/Dockerfile
index 520e3f8..d4006d8 100644
--- a/gitlab-ci/docker/jessie-backports/Dockerfile
+++ b/gitlab-ci/docker/jessie-backports/Dockerfile
@@ -10,6 +10,7 @@ RUN apt-get install -y \
 
 RUN apt-get install -y \
 	-t jessie-backports \
+	python3-configargparse \
 	python3-dnspython
 
 
diff --git a/gitlab-ci/docker/stretch/Dockerfile b/gitlab-ci/docker/stretch/Dockerfile
index da04dfd..61fbe1d 100644
--- a/gitlab-ci/docker/stretch/Dockerfile
+++ b/gitlab-ci/docker/stretch/Dockerfile
@@ -7,6 +7,7 @@ RUN apt-get upgrade
 RUN apt-get install -y \
 	python3-dnspython \
 	python3-coverage \
+	python3-configargparse \
 	python3-pip
 
 # Allows run python3-coverage with same command than manual install by pip
-- 
GitLab


From c23bcd217262be641e8bd782cd869d6db284b27c Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 8 Sep 2017 22:31:03 +0200
Subject: [PATCH 06/93] gitlab-ci.yml: move it from standard place to gitlab-ci
 dir

---
 .gitlab-ci.yml => gitlab-ci/gitlab-ci.yml | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename .gitlab-ci.yml => gitlab-ci/gitlab-ci.yml (100%)

diff --git a/.gitlab-ci.yml b/gitlab-ci/gitlab-ci.yml
similarity index 100%
rename from .gitlab-ci.yml
rename to gitlab-ci/gitlab-ci.yml
-- 
GitLab


From bbcbbb1370bf40d7003a325823e40c7f87f69257 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sat, 9 Sep 2017 14:03:29 +0200
Subject: [PATCH 07/93] gitlab-ci: add Stretch image test

---
 gitlab-ci/gitlab-ci.yml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/gitlab-ci/gitlab-ci.yml b/gitlab-ci/gitlab-ci.yml
index a7aa58c..0db4ab5 100644
--- a/gitlab-ci/gitlab-ci.yml
+++ b/gitlab-ci/gitlab-ci.yml
@@ -18,3 +18,10 @@ jessie_backport:
     - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_delete
     - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_delete.py
     - coverage html
+
+stretch:
+  image: adt-stretch_dnspython3_1.15
+  script:
+    - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_delete
+    - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_delete.py
+    - coverage html
-- 
GitLab


From b31e9bbb75b36d3a98381d6f3834e82b0030484e Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sat, 9 Sep 2017 14:16:21 +0200
Subject: [PATCH 08/93] docker: update-alternative fix install coverage binary

---
 gitlab-ci/docker/jessie-backports/Dockerfile | 6 ++----
 gitlab-ci/docker/stretch/Dockerfile          | 6 ++----
 2 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/gitlab-ci/docker/jessie-backports/Dockerfile b/gitlab-ci/docker/jessie-backports/Dockerfile
index d4006d8..250c066 100644
--- a/gitlab-ci/docker/jessie-backports/Dockerfile
+++ b/gitlab-ci/docker/jessie-backports/Dockerfile
@@ -16,9 +16,7 @@ RUN apt-get install -y \
 
 # Allows run python3-coverage with same command than manual install by pip
 RUN update-alternatives --install \
-	/usr/bin/python3-coverage \
+	/usr/bin/coverage \
 	coverage \
-	/usr/bin/python3.4-coverage \
+	/usr/bin/python3-coverage \
 	1
-
-RUN ln -s /etc/alternatives/coverage /usr/bin/coverage
diff --git a/gitlab-ci/docker/stretch/Dockerfile b/gitlab-ci/docker/stretch/Dockerfile
index 61fbe1d..e553c16 100644
--- a/gitlab-ci/docker/stretch/Dockerfile
+++ b/gitlab-ci/docker/stretch/Dockerfile
@@ -12,9 +12,7 @@ RUN apt-get install -y \
 
 # Allows run python3-coverage with same command than manual install by pip
 RUN update-alternatives --install \
-	/usr/bin/python3-coverage \
+	/usr/bin/coverage \
 	coverage \
-	/usr/bin/python3.4-coverage \
+	/usr/bin/python3-coverage \
 	1
-
-RUN ln -s /etc/alternatives/coverage /usr/bin/coverage
-- 
GitLab


From df3ca289e2654bf04d990af0db9129285d05e6e6 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 23 Jan 2018 18:29:51 +0100
Subject: [PATCH 09/93] ci: remove all tests/reports as we have to do havy new
 developments before reaching new API v2

---
 gitlab-ci/gitlab-ci.yml | 27 ---------------------------
 1 file changed, 27 deletions(-)
 delete mode 100644 gitlab-ci/gitlab-ci.yml

diff --git a/gitlab-ci/gitlab-ci.yml b/gitlab-ci/gitlab-ci.yml
deleted file mode 100644
index 0db4ab5..0000000
--- a/gitlab-ci/gitlab-ci.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-after_script:
-    - sleep 10
-
-jessie:
-  image: adt-jessie_dnspython3_1.11
-  before_script:
-    - pip3 install --upgrade -r tests/requirements.txt
-  script:
-    - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_delete
-    - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_delete.py
-    - coverage html
-
-jessie_backport:
-  image: adt-jessie_dnspython3_1.15-bpo
-  before_script:
-    - pip3 install --upgrade -r tests/requirements.txt
-  script:
-    - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_delete
-    - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_delete.py
-    - coverage html
-
-stretch:
-  image: adt-stretch_dnspython3_1.15
-  script:
-    - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_delete
-    - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_delete.py
-    - coverage html
-- 
GitLab


From 7ba17f3966d29fb87139dafd0d6cccc0b893d915 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 23 Jan 2018 20:51:16 +0100
Subject: [PATCH 10/93] newAccount: first implementation of new JWS and new
 Account

---
 acme_dns_tiny.py | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 5e506d9..57d1f66 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -40,7 +40,12 @@ def get_crt(config, log=LOGGER):
         nonlocal jws_nonce
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         protected = copy.deepcopy(jws_header)
-        protected["nonce"] = jws_nonce or urlopen(config["acmednstiny"]["ACMEDirectory"]).getheader("Replay-Nonce", None)
+        protected["nonce"] = jws_nonce or urlopen(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+        protected["url"] = url
+        if url == acme_config("newAccount"):
+            del protected["kid"]
+        else:
+            del protected["jwk"]
         protected64 = _b64(json.dumps(protected).encode("utf8"))
         signature = _openssl("dgst", ["-sha256", "-sign", config["acmednstiny"]["AccountKeyFile"]],
                              "{0}.{1}".format(protected64, payload64).encode("utf8"))
@@ -68,7 +73,7 @@ def get_crt(config, log=LOGGER):
     log.info("Read ACME directory.")
     directory = urlopen(config["acmednstiny"]["ACMEDirectory"])
     acme_config = json.loads(directory.read().decode("utf8"))
-    current_terms = acme_config.get("meta", {}).get("terms-of-service")
+    current_terms = acme_config.get("meta", {}).get("termsOfService")
 
     log.info("Prepare DNS keyring and resolver.")
     keyring = dns.tsigkeyring.from_text({config["TSIGKeyring"]["KeyName"]: config["TSIGKeyring"]["KeyValue"]})
@@ -98,6 +103,7 @@ def get_crt(config, log=LOGGER):
             "kty": "RSA",
             "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
         },
+        "kid": None,
     }
     accountkey_json = json.dumps(jws_header["jwk"], sort_keys=True, separators=(",", ":"))
     thumbprint = _b64(hashlib.sha256(accountkey_json.encode("utf8")).digest())
@@ -116,7 +122,6 @@ def get_crt(config, log=LOGGER):
                 domains.add(san[4:])
 
     log.info("Registering ACME Account.")
-    reg_info = {"resource": "new-reg"}
     if current_terms is not None:
         reg_info["agreement"] = current_terms
     reg_info["contact"] = []
@@ -129,21 +134,22 @@ def get_crt(config, log=LOGGER):
     if len(reg_info["contact"]) == 0:
         del reg_info["contact"]
 
-    code, result, headers = _send_signed_request(acme_config["new-reg"], reg_info)
+    code, result, headers = _send_signed_request(acme_config["newAccount"], reg_info)
     if code == 201:
         account_url = dict(headers).get("Location")
         log.info("Registered! (account: '{0}')".format(account_url))
         reg_received_contact = reg_info.get("contact")
-    elif code == 409:
+    elif code == 200:
         account_url = dict(headers).get("Location")
         log.info("Already registered! (account: '{0}')".format(account_url))
-        # Client should send empty payload to query account information
-        code, result, headers = _send_signed_request(account_url, {"resource":"reg"})
+        reg_info["onlyReturnExisting"] = True
+        code, result, headers = _send_signed_request(acme_config["newAccount"], reg_info)
         account_info = json.loads(result.decode("utf8"))
         reg_info["agreement"] = account_info.get("agreement")
         reg_received_contact = account_info.get("contact")
     else:
         raise ValueError("Error registering: {0} {1}".format(code, result))
+    jws_header["kid"] = account_url
 
     log.info("Update contact information and terms of service agreement if needed.")
     if current_terms is None:
-- 
GitLab


From 0549b51e3cf8b6960bbbf3870cdc604b639aa549 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 24 Jan 2018 00:26:02 +0100
Subject: [PATCH 11/93] newAccount: adjust account
 update/info/termsOfServiceAgreement

---
 acme_dns_tiny.py | 55 ++++++++++++++++++++++++------------------------
 1 file changed, 28 insertions(+), 27 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 57d1f66..10a568f 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -73,7 +73,7 @@ def get_crt(config, log=LOGGER):
     log.info("Read ACME directory.")
     directory = urlopen(config["acmednstiny"]["ACMEDirectory"])
     acme_config = json.loads(directory.read().decode("utf8"))
-    current_terms = acme_config.get("meta", {}).get("termsOfService")
+    terms_service = acme_config.get("meta", {}).get("termsOfService")
 
     log.info("Prepare DNS keyring and resolver.")
     keyring = dns.tsigkeyring.from_text({config["TSIGKeyring"]["KeyName"]: config["TSIGKeyring"]["KeyValue"]})
@@ -122,46 +122,47 @@ def get_crt(config, log=LOGGER):
                 domains.add(san[4:])
 
     log.info("Registering ACME Account.")
-    if current_terms is not None:
-        reg_info["agreement"] = current_terms
-    reg_info["contact"] = []
-    reg_mailto = "mailto:{0}".format(config["acmednstiny"].get("MailContact"))
-    reg_phone = "tel:{0}".format(config["acmednstiny"].get("PhoneContact"))
+    account_request["termsOfServiceAgreed"] = True
+    account_request["contact"] = []
+    account_mailto = "mailto:{0}".format(config["acmednstiny"].get("MailContact"))
+    account_phone = "tel:{0}".format(config["acmednstiny"].get("PhoneContact"))
     if config["acmednstiny"].get("MailContact") is not None:
-        reg_info["contact"].append(reg_mailto)
+        account_request["contact"].append(account_mailto)
     if config["acmednstiny"].get("PhoneContact") is not None:
-        reg_info["contact"].append(reg_phone)
-    if len(reg_info["contact"]) == 0:
-        del reg_info["contact"]
+        account_request["contact"].append(account_phone)
+    if len(account_request["contact"]) == 0:
+        del account_request["contact"]
 
-    code, result, headers = _send_signed_request(acme_config["newAccount"], reg_info)
+    code, result, headers = _send_signed_request(acme_config["newAccount"], account_request)
     if code == 201:
         account_url = dict(headers).get("Location")
         log.info("Registered! (account: '{0}')".format(account_url))
-        reg_received_contact = reg_info.get("contact")
+        account_info["termsOfServiceAgreed"] = True
+        account_info["contact"] = account_request["contact"]
     elif code == 200:
         account_url = dict(headers).get("Location")
         log.info("Already registered! (account: '{0}')".format(account_url))
-        reg_info["onlyReturnExisting"] = True
-        code, result, headers = _send_signed_request(acme_config["newAccount"], reg_info)
+
+        code, result, headers = _send_signed_request(account_url, {})
         account_info = json.loads(result.decode("utf8"))
-        reg_info["agreement"] = account_info.get("agreement")
-        reg_received_contact = account_info.get("contact")
     else:
         raise ValueError("Error registering: {0} {1}".format(code, result))
     jws_header["kid"] = account_url
 
-    log.info("Update contact information and terms of service agreement if needed.")
-    if current_terms is None:
-        current_terms = _get_url_link(headers, 'terms-of-service')
-    if (reg_info.get("agreement") != current_terms
-        or reg_mailto not in reg_received_contact
-        or reg_phone not in reg_received_contact):
-        reg_info["resource"] = "reg"
-        reg_info["agreement"] = current_terms
-        code, result, headers = _send_signed_request(account_url, reg_info)
-        if code == 202:
-            log.info("Account updated (terms of service agreed: '{0}')".format(reg_info.get("agreement")))
+    log.info("Update contact information if needed.")
+    if (account_info["termsOfServiceAgreed"] == False
+        or account_mailto not in account_info["contact"]
+        or account_phone not in account_info["contact"]):
+        code, result, headers = _send_signed_request(account_url, account_request)
+        if code == 200:
+            log.info("Account updated with latest contact informations.")
+        elif code == 403:
+            error_info = json.loads(result.decode("utf8"))
+            error_msg = "Error register update: {0} {1}".format(code, result)
+            if error_info["type"] == "urn:ietf:params:acme:error:userActionRequired":
+                terms_service = _get_url_link(headers, "terms-of-service")
+                error_msg = "Automatic agreement of Terms of service failed ({0}). Read terms ({1}), then follow your CA instructions: {2}".format(error_info["detail"], terms_service, error_info["instance"])
+            raise ValueError(error_msg)
         else:
             raise ValueError("Error register update: {0} {1}".format(code, result))
 
-- 
GitLab


From cd50bf9e587ac7fedce3d005cbb15a946d27d4aa Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 24 Jan 2018 22:23:28 +0100
Subject: [PATCH 12/93] account: first JWS accpeted and account created

---
 acme_dns_tiny.py | 46 ++++++++++++++++++++++------------------------
 1 file changed, 22 insertions(+), 24 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 10a568f..dfb22e9 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -42,7 +42,7 @@ def get_crt(config, log=LOGGER):
         protected = copy.deepcopy(jws_header)
         protected["nonce"] = jws_nonce or urlopen(acme_config["newNonce"]).getheader("Replay-Nonce", None)
         protected["url"] = url
-        if url == acme_config("newAccount"):
+        if url == acme_config["newAccount"]:
             del protected["kid"]
         else:
             del protected["jwk"]
@@ -50,8 +50,7 @@ def get_crt(config, log=LOGGER):
         signature = _openssl("dgst", ["-sha256", "-sign", config["acmednstiny"]["AccountKeyFile"]],
                              "{0}.{1}".format(protected64, payload64).encode("utf8"))
         data = json.dumps({
-            "header": jws_header, "protected": protected64,
-            "payload": payload64, "signature": _b64(signature),
+            "protected": protected64, "payload": payload64,"signature": _b64(signature)
         })
         try:
             resp = urlopen(url, data.encode("utf8"))
@@ -122,50 +121,49 @@ def get_crt(config, log=LOGGER):
                 domains.add(san[4:])
 
     log.info("Registering ACME Account.")
+    account_request = {}
     account_request["termsOfServiceAgreed"] = True
     account_request["contact"] = []
-    account_mailto = "mailto:{0}".format(config["acmednstiny"].get("MailContact"))
-    account_phone = "tel:{0}".format(config["acmednstiny"].get("PhoneContact"))
     if config["acmednstiny"].get("MailContact") is not None:
-        account_request["contact"].append(account_mailto)
+        account_request["contact"].append("mailto:{0}".format(config["acmednstiny"].get("MailContact")))
     if config["acmednstiny"].get("PhoneContact") is not None:
-        account_request["contact"].append(account_phone)
+        account_request["contact"].append("tel:{0}".format(config["acmednstiny"].get("PhoneContact")))
     if len(account_request["contact"]) == 0:
         del account_request["contact"]
 
     code, result, headers = _send_signed_request(acme_config["newAccount"], account_request)
+    account_info = {}
     if code == 201:
-        account_url = dict(headers).get("Location")
-        log.info("Registered! (account: '{0}')".format(account_url))
+        jws_header["kid"] = dict(headers).get("Location")
+        log.info("Registered! (account: '{0}')".format(jws_header["kid"]))
         account_info["termsOfServiceAgreed"] = True
         account_info["contact"] = account_request["contact"]
     elif code == 200:
-        account_url = dict(headers).get("Location")
-        log.info("Already registered! (account: '{0}')".format(account_url))
+        jws_header["kid"] = dict(headers).get("Location")
+        log.info("Already registered! (account: '{0}')".format(jws_header["kid"]))
 
-        code, result, headers = _send_signed_request(account_url, {})
+        code, result, headers = _send_signed_request(jws_header["kid"], {})
         account_info = json.loads(result.decode("utf8"))
     else:
         raise ValueError("Error registering: {0} {1}".format(code, result))
-    jws_header["kid"] = account_url
 
     log.info("Update contact information if needed.")
-    if (account_info["termsOfServiceAgreed"] == False
-        or account_mailto not in account_info["contact"]
-        or account_phone not in account_info["contact"]):
-        code, result, headers = _send_signed_request(account_url, account_request)
+    if (set(account_request["contact"]) != set(account_info["contact"])):
+        code, result, headers = _send_signed_request(jws_header["kid"], account_request)
         if code == 200:
             log.info("Account updated with latest contact informations.")
-        elif code == 403:
-            error_info = json.loads(result.decode("utf8"))
-            error_msg = "Error register update: {0} {1}".format(code, result)
-            if error_info["type"] == "urn:ietf:params:acme:error:userActionRequired":
-                terms_service = _get_url_link(headers, "terms-of-service")
-                error_msg = "Automatic agreement of Terms of service failed ({0}). Read terms ({1}), then follow your CA instructions: {2}".format(error_info["detail"], terms_service, error_info["instance"])
-            raise ValueError(error_msg)
         else:
             raise ValueError("Error register update: {0} {1}".format(code, result))
 
+    # when will we be notified to update agreements ?
+    #elif code == 403:
+    #    error_info = json.loads(result.decode("utf8"))
+    #    error_msg = "Error register update: {0} {1}".format(code, result)
+    #    if error_info["type"] == "urn:ietf:params:acme:error:userActionRequired":
+    #        terms_service = _get_url_link(headers, "terms-of-service")
+    #        error_msg = "Automatic agreement of Terms of service failed ({0}). Read terms ({1}), then follow your CA instructions: {2}".format(error_info["detail"], terms_service, error_info["instance"])
+    #    raise ValueError(error_msg)
+
     # verify each domain
     for domain in domains:
         log.info("Verifying domain: {0}".format(domain))
-- 
GitLab


From 3a204698204e68b9c8f0fef7537a76a31434a5d8 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 24 Jan 2018 23:51:29 +0100
Subject: [PATCH 13/93] v2: first implementation of new certificate issuance

---
 acme_dns_tiny.py | 96 ++++++++++++++++++++++++++----------------------
 1 file changed, 53 insertions(+), 43 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index dfb22e9..e1f4048 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -155,29 +155,35 @@ def get_crt(config, log=LOGGER):
         else:
             raise ValueError("Error register update: {0} {1}".format(code, result))
 
-    # when will we be notified to update agreements ?
-    #elif code == 403:
-    #    error_info = json.loads(result.decode("utf8"))
-    #    error_msg = "Error register update: {0} {1}".format(code, result)
-    #    if error_info["type"] == "urn:ietf:params:acme:error:userActionRequired":
-    #        terms_service = _get_url_link(headers, "terms-of-service")
-    #        error_msg = "Automatic agreement of Terms of service failed ({0}). Read terms ({1}), then follow your CA instructions: {2}".format(error_info["detail"], terms_service, error_info["instance"])
-    #    raise ValueError(error_msg)
+    # new order
+    log.info("Certification issuance: ask for a new Order")
+    new_order = { "identifiers": [{"type": "dns", "value": domain} for domain in domains],
+              "notAfter": "2018-01-25T:04:00:00Z"}
+    code, result, headers = _send_signed_request(acme_config["newOrder"], new_order)
+    order = json.loads(result.decode("utf8"))
+    if code == 201:
+        order_location = dict(headers).get("Location")
+        log.info("Order created: ")
+    elif (code == 403
+        and order["type"] == "urn:ietf:params:acme:error:userActionRequired"):
+        terms_service = _get_url_link(headers, "terms-of-service")
+        raise ValueError("Order creation failed ({0}). Read Terms of Service ({1}), then follow your CA instructions: {2}".format(order["detail"], terms_service, order["instance"]))
+    else:
+        raise ValueError("Error getting new Order: {0} {1}".format(code, result))
 
-    # verify each domain
-    for domain in domains:
-        log.info("Verifying domain: {0}".format(domain))
+    # complete each authorization challenge
+    for authz in order["authorizations"]:
+        log.info("Complete authz: {0}".format(authz))
 
         # get new challenge
-        code, result, headers = _send_signed_request(acme_config["new-authz"], {
-            "resource": "new-authz",
-            "identifier": {"type": "dns", "value": domain},
-        })
-        if code != 201:
-            raise ValueError("Error requesting challenges: {0} {1}".format(code, result))
+        resp = urlopen(authz)
+        authorization = json.loads(resp.read().decode("utf8"))
+        if resp.getcode() != 200:
+            raise ValueError("Error requesting challenges: {0} {1}".format(resp.getcode(), authorization))
+        domain = authorization["identifier"]["value"]
 
-        log.info("Create and install DNS TXT challenge resource.")
-        challenge = [c for c in json.loads(result.decode("utf8"))["challenges"] if c["type"] == "dns-01"][0]
+        log.info("Create and install DNS TXT challenge resource for: {0}".format(domain))
+        challenge = [c for c in authorization["challenges"] if c["type"] == "dns-01"][0]
         token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge["token"])
         keyauthorization = "{0}.{1}".format(token, thumbprint)
         keydigest64 = _b64(hashlib.sha256(keyauthorization.encode("utf8")).digest())
@@ -210,10 +216,7 @@ def get_crt(config, log=LOGGER):
                     time.sleep(2)
 
         log.info("Ask ACME server to perform checks.")
-        code, result, headers = _send_signed_request(challenge["uri"], {
-            "resource": "challenge",
-            "keyAuthorization": keyauthorization,
-        })
+        code, result, headers = _send_signed_request(challenge["uri"], {"keyAuthorization": keyauthorization})
         if code != 202:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
 
@@ -237,29 +240,36 @@ def get_crt(config, log=LOGGER):
         finally:
             _update_dns(dnsrr_set, "delete")
 
-    log.info("Ask to sign certificate.")
-    csr_der = _openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"])
-    code, result, headers = _send_signed_request(acme_config["new-cert"], {
-        "resource": "new-cert",
-        "csr": _b64(csr_der),
-    })
+    log.info("Finalizing the order...")
+    csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
+    code, result, headers = _send_signed_request(order["finalize"], {"csr": csr_der})
     if code != 201:
-        raise ValueError("Error signing certificate: {0} {1}".format(code, result))
-    certificate = os.linesep.join(textwrap.wrap(base64.b64encode(result).decode("utf8"), 64))
+        raise ValueError("Error finalizing the order: {0} {1}".format(code, result))
 
-    # get the parent certificate which had created this one
-    certificate_parent_url = _get_url_link(headers, 'up')
-    resp = urlopen(certificate_parent_url)
-    if resp.getcode() not in [200, 201]:
-        raise ValueError("Error getting certificate chain from {0}: {1} {2}".format(
-            certificate_parent_url, code, resp.read()))
-    intermediary_certificate = os.linesep.join(textwrap.wrap(base64.b64encode(resp.read()).decode("utf8"), 64))
+    while True:
+        try:
+            resp = urlopen(order_location)
+            finalize = json.loads(resp.read().decode("utf8"))
+        except IOError as e:
+            raise ValueError("Error finalizing order: {0} {1}".format(
+                e.code, json.loads(e.read().decode("utf8"))))
+        if finalize["status"] == "processing":
+            time.sleep(2)
+        elif finalize["status"] == "valid":
+            log.info("Order finalized!")
+            break
+        else:
+            raise ValueError("Finalizing order {0} got errors: {1}".format(
+                domain, finalize))
     
-    chainlist = ["-----BEGIN CERTIFICATE-----{0}{1}{0}-----END CERTIFICATE-----{0}".format(
-        os.linesep, cert) for cert in [certificate, intermediary_certificate]]
+    resp = urlopen(finalize["certificate"])
+    if resp.code() != 200:
+        raise ValueError("Finalizing order {0} got errors: {1}".format(
+            resp.getcode(), resp.read.decode("utf8")))
+    certchain = os.linesep.join(textwrap.wrap(base64.b64encode(resp.read()).decode("utf8"), 64))
     
-    log.info("Certificate signed and received.")
-    return "".join(chainlist)
+    log.info("Certificate signed and chain received.")
+    return "".join(certchain)
 
 def main(argv):
     parser = argparse.ArgumentParser(
@@ -282,7 +292,7 @@ See example.ini file to configure correctly this script.
     args = parser.parse_args(argv)
 
     config = ConfigParser()
-    config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging.api.letsencrypt.org/directory",
+    config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v2.api.letsencrypt.org/directory",
                                       "CheckChallengeDelay": 2},
                       "DNS": {"Port": "53"}})
     config.read(args.configfile)
-- 
GitLab


From ae5839990df5087bde470ec648192dcedd17ae1b Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 24 Jan 2018 23:54:28 +0100
Subject: [PATCH 14/93] v2: remove useless tools since we receive chain by
 default

---
 acme_dns_tiny.py | 13 ++-----------
 1 file changed, 2 insertions(+), 11 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index e1f4048..fe22caa 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -60,14 +60,6 @@ def get_crt(config, log=LOGGER):
             jws_nonce = resp.getheader("Replay-Nonce", None)
             return resp.getcode(), resp.read(), resp.getheaders()
 
-    # helper function to get url from Link HTTP headers
-    def _get_url_link(headers, rel):
-        log.info("Looking for Link with rel='{0}' in headers".format(rel))
-        linkheaders = [link.strip() for link in dict(headers)["Link"].split(',')]
-        url = [re.match(r'<(?P<url>.*)>.*;rel=(' + re.escape(rel) + r'|("([a-z][a-z0-9\.\-]*\s+)*' + re.escape(rel) + r'[\s"]))', link).groupdict()
-                        for link in linkheaders][0]["url"]
-        return url
-
     # main code
     log.info("Read ACME directory.")
     directory = urlopen(config["acmednstiny"]["ACMEDirectory"])
@@ -166,8 +158,7 @@ def get_crt(config, log=LOGGER):
         log.info("Order created: ")
     elif (code == 403
         and order["type"] == "urn:ietf:params:acme:error:userActionRequired"):
-        terms_service = _get_url_link(headers, "terms-of-service")
-        raise ValueError("Order creation failed ({0}). Read Terms of Service ({1}), then follow your CA instructions: {2}".format(order["detail"], terms_service, order["instance"]))
+        raise ValueError("Order creation failed ({0}). Read Terms of Service ({1}), then follow your CA instructions: {2}".format(order["detail"], dict(headers)["Link"], order["instance"]))
     else:
         raise ValueError("Error getting new Order: {0} {1}".format(code, result))
 
@@ -268,7 +259,7 @@ def get_crt(config, log=LOGGER):
             resp.getcode(), resp.read.decode("utf8")))
     certchain = os.linesep.join(textwrap.wrap(base64.b64encode(resp.read()).decode("utf8"), 64))
     
-    log.info("Certificate signed and chain received.")
+    log.info("Certificate signed and chain received: {0}".format(finalize["certificate"]))
     return "".join(certchain)
 
 def main(argv):
-- 
GitLab


From 11ca268542236817de57139765e2106c6f835686 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Thu, 25 Jan 2018 00:03:31 +0100
Subject: [PATCH 15/93] v2: update challenge status verification

---
 acme_dns_tiny.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index fe22caa..95528ee 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -207,15 +207,15 @@ def get_crt(config, log=LOGGER):
                     time.sleep(2)
 
         log.info("Ask ACME server to perform checks.")
-        code, result, headers = _send_signed_request(challenge["uri"], {"keyAuthorization": keyauthorization})
-        if code != 202:
+        code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
+        if code != 200:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
 
         log.info("Waiting challenge to be verified.")
         try:
             while True:
                 try:
-                    resp = urlopen(challenge["uri"])
+                    resp = urlopen(challenge["url"])
                     challenge_status = json.loads(resp.read().decode("utf8"))
                 except IOError as e:
                     raise ValueError("Error checking challenge: {0} {1}".format(
-- 
GitLab


From b0c0a1690c3ef96d03ed2948cf86cd9b652423b6 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Thu, 25 Jan 2018 00:40:15 +0100
Subject: [PATCH 16/93] v2: first working version

---
 acme_dns_tiny.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 95528ee..f45021d 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -234,7 +234,7 @@ def get_crt(config, log=LOGGER):
     log.info("Finalizing the order...")
     csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
     code, result, headers = _send_signed_request(order["finalize"], {"csr": csr_der})
-    if code != 201:
+    if code != 200:
         raise ValueError("Error finalizing the order: {0} {1}".format(code, result))
 
     while True:
@@ -254,13 +254,13 @@ def get_crt(config, log=LOGGER):
                 domain, finalize))
     
     resp = urlopen(finalize["certificate"])
-    if resp.code() != 200:
+    if resp.getcode() != 200:
         raise ValueError("Finalizing order {0} got errors: {1}".format(
             resp.getcode(), resp.read.decode("utf8")))
-    certchain = os.linesep.join(textwrap.wrap(base64.b64encode(resp.read()).decode("utf8"), 64))
+    certchain = resp.read().decode("utf8")
     
     log.info("Certificate signed and chain received: {0}".format(finalize["certificate"]))
-    return "".join(certchain)
+    return certchain
 
 def main(argv):
     parser = argparse.ArgumentParser(
-- 
GitLab


From 3f501d8a76294898ed3b256ab0b33cb455214722 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 28 Jan 2018 22:24:32 +0100
Subject: [PATCH 17/93] v2: update example.ini with new configuration (v2
 default URL and contact info)

---
 acme_dns_tiny.py |  9 ++-------
 example.ini      | 16 ++++++++++------
 2 files changed, 12 insertions(+), 13 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index f45021d..d3f6db6 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -115,11 +115,7 @@ def get_crt(config, log=LOGGER):
     log.info("Registering ACME Account.")
     account_request = {}
     account_request["termsOfServiceAgreed"] = True
-    account_request["contact"] = []
-    if config["acmednstiny"].get("MailContact") is not None:
-        account_request["contact"].append("mailto:{0}".format(config["acmednstiny"].get("MailContact")))
-    if config["acmednstiny"].get("PhoneContact") is not None:
-        account_request["contact"].append("tel:{0}".format(config["acmednstiny"].get("PhoneContact")))
+    account_request["contact"] = config["acmednstiny"].get("Contacts").split(';')
     if len(account_request["contact"]) == 0:
         del account_request["contact"]
 
@@ -149,8 +145,7 @@ def get_crt(config, log=LOGGER):
 
     # new order
     log.info("Certification issuance: ask for a new Order")
-    new_order = { "identifiers": [{"type": "dns", "value": domain} for domain in domains],
-              "notAfter": "2018-01-25T:04:00:00Z"}
+    new_order = { "identifiers": [{"type": "dns", "value": domain} for domain in domains]}
     code, result, headers = _send_signed_request(acme_config["newOrder"], new_order)
     order = json.loads(result.decode("utf8"))
     if code == 201:
diff --git a/example.ini b/example.ini
index 7d693df..d317abc 100644
--- a/example.ini
+++ b/example.ini
@@ -3,14 +3,18 @@
 AccountKeyFile = account.key
 # Required readable CSR file
 CSRFile = domain.csr
-# Optional ACME directory url (default: https://acme-staging.api.letsencrypt.org/directory)
-ACMEDirectory = https://acme-staging.api.letsencrypt.org/directory
+# Optional ACME directory url (default: https://acme-staging-v2.api.letsencrypt.org/directory)
+ACMEDirectory = https://acme-staging-v2.api.letsencrypt.org/directory
 # Optional time in seconds to wait between DNS update and challenge check (default: 3)
 CheckChallengeDelay = 3
-# Optional Contact info to send to the ACME provider
-MailContact = mail@example.com
-# Note that Let's Encrypt servers disallow use of phone numbers
-PhoneContact = +11111111111
+# Optional To be able to be reached by ACME provider (e.g. to warn about
+# certificate expicration), you can provide some contact informations.
+# Contacts setting is a list of contact URI separated by semicolon (;).
+# If ACME provider support contact informations, it must at least support mailto
+# URI and can support more of contact.
+# For the mailto URI, the email address part must contains only one address
+# without header fields (see [RFC6068]).
+Contacts = mailto:mail@example.com;mailto:mail2@example.org
 
 [TSIGKeyring]
 # Required TSIG key name
-- 
GitLab


From fe95f01615f1501972c2769f242cd04e3b594140 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 28 Jan 2018 23:12:39 +0100
Subject: [PATCH 18/93] v2: fix empty contacts management and default URL

---
 acme_dns_tiny.py | 6 +++---
 example.ini      | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index d3f6db6..bcc916f 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -115,8 +115,8 @@ def get_crt(config, log=LOGGER):
     log.info("Registering ACME Account.")
     account_request = {}
     account_request["termsOfServiceAgreed"] = True
-    account_request["contact"] = config["acmednstiny"].get("Contacts").split(';')
-    if len(account_request["contact"]) == 0:
+    account_request["contact"] = config["acmednstiny"].get("Contacts", "").split(';')
+    if account_request["contact"] == "":
         del account_request["contact"]
 
     code, result, headers = _send_signed_request(acme_config["newAccount"], account_request)
@@ -278,7 +278,7 @@ See example.ini file to configure correctly this script.
     args = parser.parse_args(argv)
 
     config = ConfigParser()
-    config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v2.api.letsencrypt.org/directory",
+    config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v02.api.letsencrypt.org/directory",
                                       "CheckChallengeDelay": 2},
                       "DNS": {"Port": "53"}})
     config.read(args.configfile)
diff --git a/example.ini b/example.ini
index d317abc..b4ae13d 100644
--- a/example.ini
+++ b/example.ini
@@ -3,8 +3,8 @@
 AccountKeyFile = account.key
 # Required readable CSR file
 CSRFile = domain.csr
-# Optional ACME directory url (default: https://acme-staging-v2.api.letsencrypt.org/directory)
-ACMEDirectory = https://acme-staging-v2.api.letsencrypt.org/directory
+# Optional ACME directory url (default: https://acme-staging-v02.api.letsencrypt.org/directory)
+ACMEDirectory = https://acme-staging-v02.api.letsencrypt.org/directory
 # Optional time in seconds to wait between DNS update and challenge check (default: 3)
 CheckChallengeDelay = 3
 # Optional To be able to be reached by ACME provider (e.g. to warn about
-- 
GitLab


From 93e282711c3107aa596a3d6277451005163d57d0 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 30 Jan 2018 18:18:53 +0100
Subject: [PATCH 19/93] check_dns: don't raise error if last check worked

---
 acme_dns_tiny.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index bcc916f..d5112b5 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -194,10 +194,9 @@ def get_crt(config, log=LOGGER):
             except dns.exception.DNSException as dnsexception:
                 log.info("Info: retry, because a DNS error occurred while checking challenge: {0} : {1}".format(type(dnsexception).__name__, dnsexception))
             finally:
-                if number_check_fail >= 10:
-                    raise ValueError("Error checking challenge, value not found: {0}".format(keydigest64))
-
                 if challenge_verified is False:
+                    if number_check_fail >= 10:
+                        raise ValueError("Error checking challenge, value not found: {0}".format(keydigest64))
                     number_check_fail = number_check_fail + 1
                     time.sleep(2)
 
-- 
GitLab


From 4db9c7786efa204aef70e1d2e6a6061d0403cb50 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 30 Jan 2018 18:21:54 +0100
Subject: [PATCH 20/93] v2: account are not deleted anymore, but deactivated

---
 tools/{acme_account_delete.py => acme_account_deactivate.py} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename tools/{acme_account_delete.py => acme_account_deactivate.py} (100%)

diff --git a/tools/acme_account_delete.py b/tools/acme_account_deactivate.py
similarity index 100%
rename from tools/acme_account_delete.py
rename to tools/acme_account_deactivate.py
-- 
GitLab


From 9b931575bd3f7381a2d4efe0806592ada665a32a Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 30 Jan 2018 18:52:09 +0100
Subject: [PATCH 21/93] v2: update account deactivate tool for the ACME V2

---
 tools/acme_account_deactivate.py | 64 ++++++++++++++++----------------
 1 file changed, 33 insertions(+), 31 deletions(-)

diff --git a/tools/acme_account_deactivate.py b/tools/acme_account_deactivate.py
index 77f99c7..04bbca6 100644
--- a/tools/acme_account_deactivate.py
+++ b/tools/acme_account_deactivate.py
@@ -1,8 +1,8 @@
-import os, argparse, subprocess, json, base64, binascii, re, copy, logging
+import sys, os, argparse, subprocess, json, base64, binascii, re, copy, logging
 from urllib.request import urlopen
 from urllib.error import HTTPError
 
-LOGGER = logging.getLogger("acme_account_delete")
+LOGGER = logging.getLogger("acme_account_deactivate")
 LOGGER.addHandler(logging.StreamHandler())
 LOGGER.setLevel(logging.INFO)
 
@@ -25,13 +25,17 @@ def account_delete(accountkeypath, acme_directory, log=LOGGER):
         nonlocal jws_nonce
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         protected = copy.deepcopy(jws_header)
-        protected["nonce"] = jws_nonce or urlopen(acme_directory).getheader("Replay-Nonce", None)
+        protected["nonce"] = jws_nonce or urlopen(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+        protected["url"] = url
+        if url == acme_config["newAccount"]:
+            del protected["kid"]
+        else:
+            del protected["jwk"]
         protected64 = _b64(json.dumps(protected).encode("utf8"))
         signature = _openssl("dgst", ["-sha256", "-sign", accountkeypath],
                              "{0}.{1}".format(protected64, payload64).encode("utf8"))
         data = json.dumps({
-            "header": jws_header, "protected": protected64,
-            "payload": payload64, "signature": _b64(signature),
+            "protected": protected64, "payload": payload64,"signature": _b64(signature)
         })
         try:
             resp = urlopen(url, data.encode("utf8"))
@@ -41,8 +45,11 @@ def account_delete(accountkeypath, acme_directory, log=LOGGER):
             jws_nonce = resp.getheader("Replay-Nonce", None)
             return resp.getcode(), resp.read(), resp.getheaders()
 
-    # parse account key to get public key
-    log.info("Parsing account key...")
+    log.info("Reading ACME directory.")
+    directory = urlopen(acme_directory)
+    acme_config = json.loads(directory.read().decode("utf8"))
+
+    log.info("Parsing account key.")
     accountkey = _openssl("rsa", ["-in", accountkeypath, "-noout", "-text"])
     pub_hex, pub_exp = re.search(
         r"modulus:\r?\n\s+00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)",
@@ -56,40 +63,35 @@ def account_delete(accountkeypath, acme_directory, log=LOGGER):
             "kty": "RSA",
             "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
         },
+        "kid": None,
     }
-    
-    # get ACME server configuration from the directory
-    directory = urlopen(acme_directory)
-    acme_config = json.loads(directory.read().decode("utf8"))
     jws_nonce = None
     
-    log.info("Register account to get account URL.") 
-    code, result, headers = _send_signed_request(acme_config["new-reg"], {
-        "resource": "new-reg"
-    })
+    log.info("Ask CA provider account url.")
+    account_request = {}
+    account_request["onlyReturnExisting"] = True
 
-    if code == 201:
-        account_url = dict(headers).get("Location")
-        log.info("Registered! (account: '{0}')".format(account_url))
-    elif code == 409:
-        account_url = dict(headers).get("Location")
-        log.info("Already registered! (account: '{0}')".format(account_url))
+    code, result, headers = _send_signed_request(acme_config["newAccount"], account_request)
+    if code == 200:
+        jws_header["kid"] = dict(headers).get("Location")
+    else:
+        raise ValueError("Error looking or account URL: {0} {1}".format(code, result))
 
-    log.info("Delete account...")
-    code, result, headers = _send_signed_request(account_url, {
-        "resource": "reg",
-        "delete": True,
-    })
+    log.info("Deactivating account...")
+    code, result, headers = _send_signed_request(jws_header["kid"], {"status": "deactivated"})
 
-    if code not in [200,202]:
-        raise ValueError("Error deleting account key: {0} {1}".format(code, result))
-    log.info("Account key deleted !")
+    if code == 200:
+        log.info("Account key deactivated !")
+    else:
+        raise ValueError("Error while deactivating the account key: {0} {1}".format(code, result))
 
 def main(argv):
     parser = argparse.ArgumentParser(
         formatter_class=argparse.RawDescriptionHelpFormatter,
         description="""
-This script *deletes* your account from an ACME server.
+This script permanently *deactivate* your account from an ACME server.
+You should revoke your certificates *before* using this script,
+as the server won't accept any further request with this account.
 
 It will need to have access to your private account key, so
 PLEASE READ THROUGH IT!
@@ -97,7 +99,7 @@ It's around 150 lines, so it won't take long.
 
 === Example Usage ===
 Remove account.key from staging Let's Encrypt:
-python3 acme_account_delete.py --account-key account.key --acme-directory https://acme-staging.api.letsencrypt.org/directory
+python3 acme_account_deactivate.py --account-key account.key --acme-directory https://acme-staging-v02.api.letsencrypt.org/directory
 """
     )
     parser.add_argument("--account-key", required = True, help="path to the private account key to delete")
-- 
GitLab


From 510995790f576244bcaccf54c1149ede9eb2b217 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 30 Jan 2018 21:19:33 +0100
Subject: [PATCH 22/93] v2: test rename delete to deactivate

---
 ...est_acme_account_delete.py => test_acme_account_deactivate.py} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename tests/{test_acme_account_delete.py => test_acme_account_deactivate.py} (100%)

diff --git a/tests/test_acme_account_delete.py b/tests/test_acme_account_deactivate.py
similarity index 100%
rename from tests/test_acme_account_delete.py
rename to tests/test_acme_account_deactivate.py
-- 
GitLab


From 507688a75b1b4bf01967b7bb01e1c09a43ca34e0 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 30 Jan 2018 21:57:24 +0100
Subject: [PATCH 23/93] tests: v2 update deactivation test

---
 tests/README.md                       |  2 +-
 tests/config_factory.py               | 31 ++++++++++++++++++++++-----
 tests/test_acme_account_deactivate.py | 31 +++++++++++++++------------
 3 files changed, 44 insertions(+), 20 deletions(-)

diff --git a/tests/README.md b/tests/README.md
index 710e32d..fe94be3 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -21,7 +21,7 @@ explains how to setup and test acme-tiny yourself.
 
 ## List of environment variables
 
-  * `GITLABCI_CAURL`: URL of a staging ACME server
+  * `GITLABCI_ACMEDIRECTORY_V2`: URL of a staging V2 ACME server
   * `GITLABCI_CHALLENGEDELAY`: time to wait between dns update and self-check (set it to `0` to cover a bit more code)
   * `GITLABCI_DNSHOST`: domain name to reach of your DNS server (e.g. `adorsaz.ch`)
   * `GITLABCI_DNSHOSTIP`: IP address to reach of your DNS server
diff --git a/tests/config_factory.py b/tests/config_factory.py
index 4a487dc..7fc01f2 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -4,7 +4,7 @@ from subprocess import Popen
 
 # domain with server.py running on it for testing
 DOMAIN = os.getenv("GITLABCI_DOMAIN")
-ACMEDIRECTORY = os.getenv("GITLABCI_ACMEDIRECTORY", "https://acme-staging.api.letsencrypt.org/directory")
+ACMEDIRECTORY = os.getenv("GITLABCI_ACMEDIRECTORY_V2", "https://acme-staging-v02.api.letsencrypt.org/directory")
 CHALLENGEDELAY = os.getenv("GITLABCI_CHALLENGEDELAY", "3")
 DNSHOST = os.getenv("GITLABCI_DNSHOST")
 DNSHOSTIP = os.getenv("GITLABCI_DNSHOSTIP")
@@ -50,8 +50,7 @@ def generate_acme_dns_tiny_config():
     config.read("./example.ini".format(DOMAIN))
     config["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
     config["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
-    config["acmednstiny"]["MailContact"] = "mail@example.com"
-    del config["acmednstiny"]["PhoneContact"]
+    config["acmednstiny"]["Contacts"] = "mailto:mail@example.com"
     config["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
     config["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
     config["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM
@@ -133,8 +132,30 @@ def generate_acme_account_rollover_config():
     }
 
 # generate an account key to delete it
-def generate_acme_account_delete_config():
+def generate_acme_account_deactivate_config():
     # account key
     account_key = NamedTemporaryFile()
     Popen(["openssl", "genrsa", "-out", account_key.name, "2048"]).wait()
-    return account_key
+
+    # default test configuration
+    config = configparser.ConfigParser()
+    config.read("./example.ini".format(DOMAIN))
+    config["acmednstiny"]["AccountKeyFile"] = account_key.name
+    config["acmednstiny"]["CSRFile"] = account_key.name
+    config["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
+    config["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
+    config["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
+    config["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
+    config["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM
+    config["DNS"]["Host"] = DNSHOST
+    config["DNS"]["Port"] = DNSPORT
+    config["DNS"]["Zone"] = DNSZONE
+
+    deactivateConfig = NamedTemporaryFile()
+    with open(deactivateConfig.name, 'w') as configfile:
+        config.write(configfile)
+
+    return {
+        "config": deactivateConfig.name,
+        "key": account_key
+    }
diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index 4649a36..02d6ccc 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -1,32 +1,35 @@
 import unittest, os
 import acme_dns_tiny
-from tests.config_factory import generate_acme_account_delete_config
-import tools.acme_account_delete
+from tests.config_factory import generate_acme_account_deactivate_config
+import tools.acme_account_deactivate
 
-ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY", "https://acme-staging.api.letsencrypt.org/directory")
+ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY_V2", "https://acme-staging-v02.api.letsencrypt.org/directory")
 
-class TestACMEAccountDelete(unittest.TestCase):
-    "Tests for acme_account_delete"
+class TestACMEAccountDeactivate(unittest.TestCase):
+    "Tests for acme_account_deactivate"
 
     @classmethod
     def setUpClass(self):
-        self.accountkey = generate_acme_account_delete_config()
-        super(TestACMEAccountDelete, self).setUpClass()
+        configs = generate_acme_account_deactivate_config()
+        self.account_key = configs["key"]
+        acme_dns_tiny.main([self.configs['config'].name])
+        super(TestACMEAccountDeactivate, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
         # close temp files correctly
+        self.configs['config'].close
         self.accountkey.close()
-        super(TestACMEAccountDelete, self).tearDownClass()
+        super(TestACMEAccountDeactivate, self).tearDownClass()
 
-    def test_success_account_delete(self):
-        """ Test success account key delete """
-        with self.assertLogs(level='INFO') as accountdeletelog:
-            tools.acme_account_delete.main(["--account-key", self.accountkey.name,
+    def test_success_account_deactivate(self):
+        """ Test success account key deactivate """
+        with self.assertLogs(level='INFO') as accountdeactivatelog:
+            tools.acme_account_deactivate.main(["--account-key", self.accountkey.name,
                                             "--acme-directory", ACMEDirectory])
-        self.assertIn("INFO:acme_account_delete:Account key deleted !",
-            accountdeletelog.output)
+        self.assertIn("INFO:acme_account_deactivate:Account key deactivated !",
+            accountdeactivatelog.output)
 
 if __name__ == "__main__":
     unittest.main()
-- 
GitLab


From 863c5f4a98153e4d9970e3f7fa3ab635719d586f Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 30 Jan 2018 22:00:10 +0100
Subject: [PATCH 24/93] v2: test acme_dns update deactive call

---
 tests/test_acme_dns_tiny.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index ff3d171..7d3c383 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -3,7 +3,7 @@ from io import StringIO
 import dns.version
 import acme_dns_tiny
 from tests.config_factory import generate_acme_dns_tiny_config
-from tools.acme_account_delete import account_delete
+from tools.acme_account_deactivate import account_deactivate
 
 ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY", "https://acme-staging.api.letsencrypt.org/directory")
 
@@ -22,8 +22,8 @@ class TestACMEDNSTiny(unittest.TestCase):
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
-        # delete account key registration at end of tests
-        account_delete(self.configs["accountkey"].name, ACMEDirectory)
+        # deactivate account key registration at end of tests
+        account_deactivate(self.configs["accountkey"].name, ACMEDirectory)
         # close temp files correctly
         for tmpfile in self.configs:
             self.configs[tmpfile].close()
-- 
GitLab


From c1bf0c00d83ffb6f3c922661bfeb69d92d1fe8aa Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 30 Jan 2018 22:02:16 +0100
Subject: [PATCH 25/93] v2: account key rollover test update it's dependance on
 account deactivate (not working currently)

---
 tests/test_acme_account_rollover.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/test_acme_account_rollover.py b/tests/test_acme_account_rollover.py
index 635d43f..3e1b2f3 100644
--- a/tests/test_acme_account_rollover.py
+++ b/tests/test_acme_account_rollover.py
@@ -1,7 +1,7 @@
 import unittest, os
 import acme_dns_tiny
 from tests.config_factory import generate_acme_account_rollover_config
-from tools.acme_account_delete import account_delete
+from tools.acme_account_deactivate import account_deactivate
 import tools.acme_account_rollover
 
 ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY", "https://acme-staging.api.letsencrypt.org/directory")
@@ -17,8 +17,8 @@ class TestACMEAccountRollover(unittest.TestCase):
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
-        # delete account key registration at end of tests
-        account_delete(self.configs["newaccountkey"].name, ACMEDirectory)
+        # deactivate account key registration at end of tests
+        account_deactivate(self.configs["newaccountkey"].name, ACMEDirectory)
         # close temp files correctly
         for tmpfile in self.configs:
             self.configs[tmpfile].close()
-- 
GitLab


From 667eb0b7319c0aba71d9108b1c2be6210447883f Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 31 Jan 2018 21:00:34 +0100
Subject: [PATCH 26/93] v2: update key rollover tool

---
 tools/acme_account_rollover.py | 63 +++++++++++++++++++---------------
 1 file changed, 35 insertions(+), 28 deletions(-)

diff --git a/tools/acme_account_rollover.py b/tools/acme_account_rollover.py
index 8d7be1c..47d6b58 100644
--- a/tools/acme_account_rollover.py
+++ b/tools/acme_account_rollover.py
@@ -1,4 +1,4 @@
-import os, argparse, subprocess, os, json, base64, binascii, hashlib, re, copy, logging
+import sys, os, argparse, subprocess, json, base64, binascii, hashlib, re, copy, logging
 from urllib.request import urlopen
 from urllib.error import HTTPError
 
@@ -35,27 +35,36 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
                 "kty": "RSA",
                 "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
             },
+            "kid": None
         }
         return jws_header
 
-    # helper function to sign request with specified key
-    def _sign_request(accountkeypath, jwsheader, payload):
+    # helper function to sign request with specified key path
+    def _sign_request(url, keypath, payload):
         nonlocal jws_nonce
         payload64 = _b64(json.dumps(payload).encode("utf8"))
-        protected = copy.deepcopy(jwsheader)
-        protected["nonce"] = jws_nonce or urlopen(acme_directory).getheader("Replay-Nonce", None)
+        if keypath == accountkeypath:
+            protected = copy.deepcopy(jws_header)
+            protected["nonce"] = jws_nonce or urlopen(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+        elif keypath == new_accountkeypath:
+            protected = copy.deepcopy(new_jws_header)
+        if (keypath == new_accountkeypath
+            or url == acme_config["newAccount"]):
+            del protected["kid"]
+        else:
+            del protected["jwk"]
+        protected["url"] = url
         protected64 = _b64(json.dumps(protected).encode("utf8"))
-        signature = _openssl("dgst", ["-sha256", "-sign", accountkeypath],
+        signature = _openssl("dgst", ["-sha256", "-sign", keypath],
                              "{0}.{1}".format(protected64, payload64).encode("utf8"))
-        signedjws = {
-            "header": jwsheader, "protected": protected64,
-            "payload": payload64, "signature": _b64(signature),
-        }
+        signedjws = json.dumps({
+            "protected": protected64, "payload": payload64,"signature": _b64(signature)
+        })
         return signedjws
 
     # helper function make signed requests
-    def _send_signed_request(accountkeypath, jwsheader, url, payload):
-        data = json.dumps(_sign_request(accountkeypath, jwsheader, payload))
+    def _send_signed_request(url, keypath, payload):
+        data = json.dumps(_sign_request(url, keypath, payload))
         try:
             resp = urlopen(url, data.encode("utf8"))
         except HTTPError as httperror:
@@ -64,33 +73,31 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
             jws_nonce = resp.getheader("Replay-Nonce", None)
             return resp.getcode(), resp.read(), resp.getheaders()
 
+    log.info("Reading ACME directory.")
+    directory = urlopen(acme_directory)
+    acme_config = json.loads(directory.read().decode("utf8"))
+
     log.info("Parsing current account key...")
     jws_header = _jws_header(accountkeypath)
 
     log.info("Parsing new account key...")
     new_jws_header = _jws_header(new_accountkeypath)
 
-    # get ACME server configuration from the directory
-    directory = urlopen(acme_directory)
-    acme_config = json.loads(directory.read().decode("utf8"))
     jws_nonce = None
 
-    log.info("Register account to get account URL.")
-    code, result, headers = _send_signed_request(accountkeypath, jws_header, acme_config["new-reg"], {
-        "resource": "new-reg"
-    })
-
-    if code not in [201, 409]:
-        raise ValueError("Error getting account URL: {0} {1}".format(code,result))
-    account_url = dict(headers).get("Location")
+    log.info("Ask CA provider account url.")
+    code, result, headers = _send_signed_request(acme_config["newAccount"], accountkeypath, {
+        "onlyReturnExisting": True })
+    if code == 200:
+        jws_header["kid"] = dict(headers).get("Location")
+    else:
+        raise ValueError("Error looking or account URL: {0} {1}".format(code, result))
 
     log.info("Rolls over account key...")
-    outer_payload = _sign_request(new_accountkeypath, new_jws_header, {
-        "url": acme_config["key-change"], # currently needed by boulder implementation in inner payload
+    outer_payload = _sign_request(jws_header["kid"], new_accountkeypath, {
         "account": account_url,
-        "newKey": new_jws_header["jwk"]})
-    outer_payload["resource"] = "key-change" # currently needed by boulder implementation
-    code, result, headers = _send_signed_request(accountkeypath, jws_header, acme_config["key-change"], outer_payload)
+        "newKey": new_jws_header["jwk"] })
+    code, result, headers = _send_signed_request(jws_header["kid"], accountkeypath, outer_payload)
 
     if code != 200:
         raise ValueError("Error rolling over account key: {0} {1}".format(code, result))
-- 
GitLab


From b4333aa8fd0b0282b732f03832fbd7c39a251f5a Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 31 Jan 2018 21:05:56 +0100
Subject: [PATCH 27/93] v2: update tests key rollover

---
 tests/config_factory.py             | 23 ++++++++++++++++++++++-
 tests/test_acme_account_rollover.py |  3 ++-
 2 files changed, 24 insertions(+), 2 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index 7fc01f2..efd46db 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -122,11 +122,32 @@ def generate_acme_account_rollover_config():
     # Old account key
     old_account_key = NamedTemporaryFile()
     Popen(["openssl", "genrsa", "-out", old_account_key.name, "2048"]).wait()
+
     # New account key
     new_account_key = NamedTemporaryFile()
     Popen(["openssl", "genrsa", "-out", new_account_key.name, "2048"]).wait()
+
+    # default test configuration
+    config = configparser.ConfigParser()
+    config.read("./example.ini".format(DOMAIN))
+    config["acmednstiny"]["AccountKeyFile"] = account_key.name
+    config["acmednstiny"]["CSRFile"] = account_key.name
+    config["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
+    config["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
+    config["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
+    config["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
+    config["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM
+    config["DNS"]["Host"] = DNSHOST
+    config["DNS"]["Port"] = DNSPORT
+    config["DNS"]["Zone"] = DNSZONE
+
+    deactivateConfig = NamedTemporaryFile()
+    with open(deactivateConfig.name, 'w') as configfile:
+        config.write(configfile)
+
     return {
-        # keys (returned to keep files on system)
+        # config and keys (returned to keep files on system)
+        "config": config,
         "oldaccountkey": old_account_key,
         "newaccountkey": new_account_key
     }
diff --git a/tests/test_acme_account_rollover.py b/tests/test_acme_account_rollover.py
index 3e1b2f3..57c3062 100644
--- a/tests/test_acme_account_rollover.py
+++ b/tests/test_acme_account_rollover.py
@@ -4,7 +4,7 @@ from tests.config_factory import generate_acme_account_rollover_config
 from tools.acme_account_deactivate import account_deactivate
 import tools.acme_account_rollover
 
-ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY", "https://acme-staging.api.letsencrypt.org/directory")
+ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY_V2", "https://acme-staging-v02.api.letsencrypt.org/directory")
 
 class TestACMEAccountRollover(unittest.TestCase):
     "Tests for acme_account_rollover"
@@ -12,6 +12,7 @@ class TestACMEAccountRollover(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         self.configs = generate_acme_account_rollover_config()
+        acme_dns_tiny.main([self.configs['config'].name])
         super(TestACMEAccountRollover, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
-- 
GitLab


From 840c8fa9017a2b5cd748a501019399b2e126f3cf Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 31 Jan 2018 21:17:06 +0100
Subject: [PATCH 28/93] v2: enable back unit tests

---
 gitlab-ci/gitlab-ci.yml | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)
 create mode 100644 gitlab-ci/gitlab-ci.yml

diff --git a/gitlab-ci/gitlab-ci.yml b/gitlab-ci/gitlab-ci.yml
new file mode 100644
index 0000000..e51bb8b
--- /dev/null
+++ b/gitlab-ci/gitlab-ci.yml
@@ -0,0 +1,27 @@
+after_script:
+    - sleep 10
+
+jessie:
+  image: adt-jessie_dnspython3_1.11
+  before_script:
+    - pip3 install --upgrade -r tests/requirements.txt
+  script:
+    - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_deactivate
+    - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_deactivate.py
+    - coverage html
+
+jessie_backport:
+  image: adt-jessie_dnspython3_1.15-bpo
+  before_script:
+    - pip3 install --upgrade -r tests/requirements.txt
+  script:
+    - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_deactivate
+    - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_deactivate.py
+    - coverage html
+
+stretch:
+  image: adt-stretch_dnspython3_1.15
+  script:
+    - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_deactivate
+    - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_deactivate.py
+    - coverage html
-- 
GitLab


From 8278bbf9609391e3debc1150dca342824da7533f Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 31 Jan 2018 21:19:10 +0100
Subject: [PATCH 29/93] v2: test acme-dns-tiny fix URL directory

---
 tests/test_acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 7d3c383..8b955fa 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -5,7 +5,7 @@ import acme_dns_tiny
 from tests.config_factory import generate_acme_dns_tiny_config
 from tools.acme_account_deactivate import account_deactivate
 
-ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY", "https://acme-staging.api.letsencrypt.org/directory")
+ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY_V2", "https://acme-staging-v02.api.letsencrypt.org/directory")
 
 class TestACMEDNSTiny(unittest.TestCase):
     "Tests for acme_dns_tiny.get_crt()"
-- 
GitLab


From 7337aa4b1924d6d7dc6514e5047339ddd4c7e70d Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 31 Jan 2018 21:34:33 +0100
Subject: [PATCH 30/93] v2: fix account deactivate method name

---
 tools/acme_account_deactivate.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tools/acme_account_deactivate.py b/tools/acme_account_deactivate.py
index 04bbca6..8a9d8b6 100644
--- a/tools/acme_account_deactivate.py
+++ b/tools/acme_account_deactivate.py
@@ -6,7 +6,7 @@ LOGGER = logging.getLogger("acme_account_deactivate")
 LOGGER.addHandler(logging.StreamHandler())
 LOGGER.setLevel(logging.INFO)
 
-def account_delete(accountkeypath, acme_directory, log=LOGGER):
+def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
     # helper function base64 encode as defined in acme spec
     def _b64(b):
         return base64.urlsafe_b64encode(b).decode("utf8").rstrip("=")
@@ -102,7 +102,7 @@ Remove account.key from staging Let's Encrypt:
 python3 acme_account_deactivate.py --account-key account.key --acme-directory https://acme-staging-v02.api.letsencrypt.org/directory
 """
     )
-    parser.add_argument("--account-key", required = True, help="path to the private account key to delete")
+    parser.add_argument("--account-key", required = True, help="path to the private account key to deactivate")
     parser.add_argument("--acme-directory", required = True, help="ACME directory URL of the ACME server where to remove the key")
     parser.add_argument("--quiet", action="store_const",
                         const=logging.ERROR,
@@ -110,7 +110,7 @@ python3 acme_account_deactivate.py --account-key account.key --acme-directory ht
     args = parser.parse_args(argv)
 
     LOGGER.setLevel(args.quiet or LOGGER.level)
-    account_delete(args.account_key, args.acme_directory)
+    account_deactivate(args.account_key, args.acme_directory)
 
 if __name__ == "__main__":  # pragma: no cover
     main(sys.argv[1:])
-- 
GitLab


From 4265563bfb6bd3a0da2b11691c7d1be8c00339bd Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 2 Feb 2018 17:51:55 +0100
Subject: [PATCH 31/93] gitlab-ci: add -y option to upgrade command

---
 gitlab-ci/docker/jessie-backports/Dockerfile | 2 +-
 gitlab-ci/docker/jessie/Dockerfile           | 2 +-
 gitlab-ci/docker/stretch/Dockerfile          | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/gitlab-ci/docker/jessie-backports/Dockerfile b/gitlab-ci/docker/jessie-backports/Dockerfile
index 250c066..54c6ae8 100644
--- a/gitlab-ci/docker/jessie-backports/Dockerfile
+++ b/gitlab-ci/docker/jessie-backports/Dockerfile
@@ -1,7 +1,7 @@
 FROM debian:jessie-backports
 
 RUN apt-get update
-RUN apt-get upgrade
+RUN apt-get upgrade -y
 
 # Minimal tools required by acme-dns-tiny CI
 RUN apt-get install -y \
diff --git a/gitlab-ci/docker/jessie/Dockerfile b/gitlab-ci/docker/jessie/Dockerfile
index 3fa7b07..0e3dd0a 100644
--- a/gitlab-ci/docker/jessie/Dockerfile
+++ b/gitlab-ci/docker/jessie/Dockerfile
@@ -1,7 +1,7 @@
 FROM debian:jessie
 
 RUN apt-get update
-RUN apt-get upgrade
+RUN apt-get upgrade -y
 
 # Minimal tools required by acme-dns-tiny CI
 RUN apt-get install -y \
diff --git a/gitlab-ci/docker/stretch/Dockerfile b/gitlab-ci/docker/stretch/Dockerfile
index e553c16..3706dc7 100644
--- a/gitlab-ci/docker/stretch/Dockerfile
+++ b/gitlab-ci/docker/stretch/Dockerfile
@@ -1,7 +1,7 @@
 FROM debian:stretch
 
 RUN apt-get update
-RUN apt-get upgrade
+RUN apt-get upgrade -y
 
 # Minimal tools required by acme-dns-tiny CI
 RUN apt-get install -y \
-- 
GitLab


From 06423cab14b73e6f19aabf6638344e21e50489c3 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 2 Feb 2018 17:53:04 +0100
Subject: [PATCH 32/93] v2: fix config management in acme deactivate test

---
 tests/config_factory.py               | 2 +-
 tests/test_acme_account_deactivate.py | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index efd46db..e404342 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -177,6 +177,6 @@ def generate_acme_account_deactivate_config():
         config.write(configfile)
 
     return {
-        "config": deactivateConfig.name,
+        "config": deactivateConfig,
         "key": account_key
     }
diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index 02d6ccc..a889137 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -11,15 +11,16 @@ class TestACMEAccountDeactivate(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         configs = generate_acme_account_deactivate_config()
+        self.config = configs["config"]
         self.account_key = configs["key"]
-        acme_dns_tiny.main([self.configs['config'].name])
+        acme_dns_tiny.main([self.config.name])
         super(TestACMEAccountDeactivate, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
         # close temp files correctly
-        self.configs['config'].close
+        self.config.close()
         self.accountkey.close()
         super(TestACMEAccountDeactivate, self).tearDownClass()
 
-- 
GitLab


From 0ed39133b8f6248684bddde0266af2c1f49e1cb4 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 2 Feb 2018 20:46:12 +0100
Subject: [PATCH 33/93] v2: fix acme account rollover

---
 tools/acme_account_rollover.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/acme_account_rollover.py b/tools/acme_account_rollover.py
index 47d6b58..469b154 100644
--- a/tools/acme_account_rollover.py
+++ b/tools/acme_account_rollover.py
@@ -95,7 +95,7 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
 
     log.info("Rolls over account key...")
     outer_payload = _sign_request(jws_header["kid"], new_accountkeypath, {
-        "account": account_url,
+        "account": jws_header["kid"],
         "newKey": new_jws_header["jwk"] })
     code, result, headers = _send_signed_request(jws_header["kid"], accountkeypath, outer_payload)
 
-- 
GitLab


From c5b6531482b821ffaa8851297dfd668f185a95e2 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 2 Feb 2018 20:56:42 +0100
Subject: [PATCH 34/93] v2: fix config factory for account rollover

---
 tests/config_factory.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index e404342..bdeca35 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -130,8 +130,8 @@ def generate_acme_account_rollover_config():
     # default test configuration
     config = configparser.ConfigParser()
     config.read("./example.ini".format(DOMAIN))
-    config["acmednstiny"]["AccountKeyFile"] = account_key.name
-    config["acmednstiny"]["CSRFile"] = account_key.name
+    config["acmednstiny"]["AccountKeyFile"] = old_account_key.name
+    config["acmednstiny"]["CSRFile"] = old_account_key.name
     config["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
     config["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
     config["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
-- 
GitLab


From f23e018edf425e745e9f14d5f935990d0b6aeca2 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 2 Feb 2018 21:07:12 +0100
Subject: [PATCH 35/93] v2: finalizing order requires to wait at least for
 Retry-After seconds

---
 acme_dns_tiny.py | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index d5112b5..3dba8e9 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -228,24 +228,23 @@ def get_crt(config, log=LOGGER):
     log.info("Finalizing the order...")
     csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
     code, result, headers = _send_signed_request(order["finalize"], {"csr": csr_der})
-    if code != 200:
-        raise ValueError("Error finalizing the order: {0} {1}".format(code, result))
+    finalize = json.loads(result.decode("utf8"))
 
     while True:
-        try:
-            resp = urlopen(order_location)
-            finalize = json.loads(resp.read().decode("utf8"))
-        except IOError as e:
-            raise ValueError("Error finalizing order: {0} {1}".format(
-                e.code, json.loads(e.read().decode("utf8"))))
         if finalize["status"] == "processing":
-            time.sleep(2)
+            time.sleep(resp.getheader("Retry-After", 2)
         elif finalize["status"] == "valid":
             log.info("Order finalized!")
             break
         else:
             raise ValueError("Finalizing order {0} got errors: {1}".format(
                 domain, finalize))
+        try:
+            resp = urlopen(order_location)
+            finalize = json.loads(resp.read().decode("utf8"))
+        except IOError as e:
+            raise ValueError("Error finalizing order: {0} {1}".format(
+                e.code, json.loads(e.read().decode("utf8"))))
     
     resp = urlopen(finalize["certificate"])
     if resp.getcode() != 200:
-- 
GitLab


From 9eb3a0f3d0c97eaab0430eb6117114e62b073b3c Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 2 Feb 2018 21:10:40 +0100
Subject: [PATCH 36/93] v2: fix typo

---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 3dba8e9..167c8f4 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -232,7 +232,7 @@ def get_crt(config, log=LOGGER):
 
     while True:
         if finalize["status"] == "processing":
-            time.sleep(resp.getheader("Retry-After", 2)
+            time.sleep(resp.getheader("Retry-After", 2))
         elif finalize["status"] == "valid":
             log.info("Order finalized!")
             break
-- 
GitLab


From e436bc46e3dba4e03f8585c24fd36b1daf5080f9 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Fri, 2 Feb 2018 21:31:30 +0100
Subject: [PATCH 37/93] v2: add some informations to debug tests

---
 acme_dns_tiny.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 167c8f4..2e8721c 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -150,7 +150,7 @@ def get_crt(config, log=LOGGER):
     order = json.loads(result.decode("utf8"))
     if code == 201:
         order_location = dict(headers).get("Location")
-        log.info("Order created: ")
+        log.info("Order created: {0}".format(order_location))
     elif (code == 403
         and order["type"] == "urn:ietf:params:acme:error:userActionRequired"):
         raise ValueError("Order creation failed ({0}). Read Terms of Service ({1}), then follow your CA instructions: {2}".format(order["detail"], dict(headers)["Link"], order["instance"]))
@@ -159,7 +159,7 @@ def get_crt(config, log=LOGGER):
 
     # complete each authorization challenge
     for authz in order["authorizations"]:
-        log.info("Complete authz: {0}".format(authz))
+        log.info("Completing authz: {0}".format(authz))
 
         # get new challenge
         resp = urlopen(authz)
@@ -226,6 +226,9 @@ def get_crt(config, log=LOGGER):
             _update_dns(dnsrr_set, "delete")
 
     log.info("Finalizing the order...")
+    resp = urlopen(order_location)
+    finalize = json.loads(resp.read().decode("utf8"))
+    log.info("Before sending request, order is: {0}".format(finalize))
     csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
     code, result, headers = _send_signed_request(order["finalize"], {"csr": csr_der})
     finalize = json.loads(result.decode("utf8"))
-- 
GitLab


From c2c2ade1300f4fc7ab3516f4a921441398ea4c0a Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sat, 3 Feb 2018 10:12:11 +0100
Subject: [PATCH 38/93] v2: check order status before completing challenges

Tests will continue to fail, because the LE server send the same order for each request.
We'll need to modify the tests configuration to use a new authorization key for each test.
---
 acme_dns_tiny.py | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 2e8721c..27fb4a1 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -150,7 +150,9 @@ def get_crt(config, log=LOGGER):
     order = json.loads(result.decode("utf8"))
     if code == 201:
         order_location = dict(headers).get("Location")
-        log.info("Order created: {0}".format(order_location))
+        log.info("Order received: {0}".format(order_location))
+        if order["status"] != "pending":
+            raise ValueError("Order status is not pending, we can't use it: {0}".format(order))
     elif (code == 403
         and order["type"] == "urn:ietf:params:acme:error:userActionRequired"):
         raise ValueError("Order creation failed ({0}). Read Terms of Service ({1}), then follow your CA instructions: {2}".format(order["detail"], dict(headers)["Link"], order["instance"]))
@@ -228,12 +230,19 @@ def get_crt(config, log=LOGGER):
     log.info("Finalizing the order...")
     resp = urlopen(order_location)
     finalize = json.loads(resp.read().decode("utf8"))
-    log.info("Before sending request, order is: {0}".format(finalize))
     csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
     code, result, headers = _send_signed_request(order["finalize"], {"csr": csr_der})
-    finalize = json.loads(result.decode("utf8"))
+    if code != 200:
+        raise ValueError("Error while sending the CSR: {0} {1}".format(code, result))
 
     while True:
+        try:
+            resp = urlopen(order_location)
+            finalize = json.loads(resp.read().decode("utf8"))
+        except IOError as e:
+            raise ValueError("Error finalizing order: {0} {1}".format(
+                e.code, json.loads(e.read().decode("utf8"))))
+
         if finalize["status"] == "processing":
             time.sleep(resp.getheader("Retry-After", 2))
         elif finalize["status"] == "valid":
@@ -242,12 +251,6 @@ def get_crt(config, log=LOGGER):
         else:
             raise ValueError("Finalizing order {0} got errors: {1}".format(
                 domain, finalize))
-        try:
-            resp = urlopen(order_location)
-            finalize = json.loads(resp.read().decode("utf8"))
-        except IOError as e:
-            raise ValueError("Error finalizing order: {0} {1}".format(
-                e.code, json.loads(e.read().decode("utf8"))))
     
     resp = urlopen(finalize["certificate"])
     if resp.getcode() != 200:
-- 
GitLab


From 5685a410c25c23a06c590579369185fd143c5224 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 6 Feb 2018 17:26:22 +0100
Subject: [PATCH 39/93] v2: try workaround order status by adding a delay
 between each unit test

---
 tests/test_acme_dns_tiny.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 8b955fa..afd6fe1 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -1,4 +1,4 @@
-import unittest, sys, os, subprocess
+import unittest, sys, os, subprocess, time
 from io import StringIO
 import dns.version
 import acme_dns_tiny
@@ -29,6 +29,11 @@ class TestACMEDNSTiny(unittest.TestCase):
             self.configs[tmpfile].close()
         super(TestACMEDNSTiny, self).tearDownClass()
 
+    # Add a sleeping time between each test, to avoid issue with order status
+    @classmethod
+    def tearDown(self):
+        time.sleep(5);
+
     # helper function to run openssl command
     def openssl(self, command, options, communicate=None):
         openssl = subprocess.Popen(["openssl", command] + options,
-- 
GitLab


From 439fd7f32083b7dca9d17c289d62df8528e4bb75 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 15:29:07 +0100
Subject: [PATCH 40/93] v2: strength a bit more the search regexp which looks
 for common name

On Stretch, openssl output contains spaces every where
---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 27fb4a1..c75cbfd 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -103,7 +103,7 @@ def get_crt(config, log=LOGGER):
     log.info("Parsing CSR looking for domains.")
     csr = _openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-noout", "-text"]).decode("utf8")
     domains = set([])
-    common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", csr)
+    common_name = re.search(r"Subject:\s*?CN\s*?=\s*?([^\s,;/]+)", csr)
     if common_name is not None:
         domains.add(common_name.group(1))
     subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \r?\n +([^\r\n]+)\r?\n", csr, re.MULTILINE | re.DOTALL)
-- 
GitLab


From df52f3c91c3adafd339bacfdc9434d3b5d03b513 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 15:38:46 +0100
Subject: [PATCH 41/93] v2: fix set up for account key rollover test

---
 tests/config_factory.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index bdeca35..00f7aa1 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -141,13 +141,13 @@ def generate_acme_account_rollover_config():
     config["DNS"]["Port"] = DNSPORT
     config["DNS"]["Zone"] = DNSZONE
 
-    deactivateConfig = NamedTemporaryFile()
-    with open(deactivateConfig.name, 'w') as configfile:
+    rolloverConfig = NamedTemporaryFile()
+    with open(rolloverConfig.name, 'w') as configfile:
         config.write(configfile)
 
     return {
         # config and keys (returned to keep files on system)
-        "config": config,
+        "config": rolloverConfig,
         "oldaccountkey": old_account_key,
         "newaccountkey": new_account_key
     }
-- 
GitLab


From 2348b6da3967121074d2fac0a58e4392fe7f4603 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 16:07:09 +0100
Subject: [PATCH 42/93] v2: tests: refactor config generators and fix tests for
 deactivation and rollover

---
 tests/config_factory.py | 113 +++++++++++++++-------------------------
 1 file changed, 42 insertions(+), 71 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index 00f7aa1..85729b3 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -14,23 +14,47 @@ TSIGKEYNAME = os.getenv("GITLABCI_TSIGKEYNAME")
 TSIGKEYVALUE = os.getenv("GITLABCI_TSIGKEYVALUE")
 TSIGALGORITHM = os.getenv("GITLABCI_TSIGALGORITHM")
 
-# generate account and domain keys
-def generate_acme_dns_tiny_config():
-    # good account key
+# generate simple config
+def generate_config():
+    # Account key
     account_key = NamedTemporaryFile()
     Popen(["openssl", "genrsa", "-out", account_key.name, "2048"]).wait()
 
-    # weak 1024 bit account key
-    weak_key = NamedTemporaryFile()
-    Popen(["openssl", "genrsa", "-out", weak_key.name, "1024"]).wait()
-
-    # good domain key
+    # Domain key and CSR
     domain_key = NamedTemporaryFile()
     domain_csr = NamedTemporaryFile()
     Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key.name,
         "-subj", "/CN={0}".format(DOMAIN), "-out", domain_csr.name]).wait()
 
-    # subject alt-name domain
+    # acme-dns-tiny configuration
+    parser = configparser.ConfigParser()
+    parser.read("./example.ini")
+    parser["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
+    parser["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
+    parser["acmednstiny"]["Contacts"] = "mailto:mail@example.com"
+    parser["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
+    parser["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
+    parser["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM
+    parser["DNS"]["Host"] = DNSHOST
+    parser["DNS"]["Port"] = DNSPORT
+    parser["DNS"]["Zone"] = DNSZONE
+
+    config = NamedTemporaryFile()
+    with open(config.name, 'w') as configfile:
+        parser.write(configfile)
+
+    return account_key, domain_key, domain_csr, config
+
+# generate account and domain keys
+def generate_acme_dns_tiny_config():
+    # Simple good configuration
+    account_key, domain_key, domain_csr, goodCName = generate_config();
+
+    # weak 1024 bit account key
+    weak_key = NamedTemporaryFile()
+    Popen(["openssl", "genrsa", "-out", weak_key.name, "1024"]).wait()
+
+    # CSR using subject alt-name domain instead of CN (common name)
     san_csr = NamedTemporaryFile()
     san_conf = NamedTemporaryFile()
     san_conf.write(open("/etc/ssl/openssl.cnf").read().encode("utf8"))
@@ -40,29 +64,14 @@ def generate_acme_dns_tiny_config():
         "-subj", "/", "-reqexts", "SAN", "-config", san_conf.name,
         "-out", san_csr.name]).wait()
 
-    # account-signed domain csr
+    # CSR signed with the account key
     account_csr = NamedTemporaryFile()
     Popen(["openssl", "req", "-new", "-sha256", "-key", account_key.name,
         "-subj", "/CN={0}".format(DOMAIN), "-out", account_csr.name]).wait()
 
-    # Default test configuration
+    # Create config parser from the good default config to generate custom configs
     config = configparser.ConfigParser()
-    config.read("./example.ini".format(DOMAIN))
-    config["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
-    config["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
-    config["acmednstiny"]["Contacts"] = "mailto:mail@example.com"
-    config["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
-    config["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
-    config["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM
-    config["DNS"]["Host"] = DNSHOST
-    config["DNS"]["Port"] = DNSPORT
-    config["DNS"]["Zone"] = DNSZONE
-
-    goodCName = NamedTemporaryFile()
-    config["acmednstiny"]["AccountKeyFile"] = account_key.name
-    config["acmednstiny"]["CSRFile"] = domain_csr.name
-    with open(goodCName.name, 'w') as configfile:
-        config.write(configfile)
+    config.read(goodCName.name)
 
     dnsHostIP = NamedTemporaryFile()
     config["DNS"]["Host"] = DNSHOSTIP
@@ -119,64 +128,26 @@ def generate_acme_dns_tiny_config():
 
 # generate two account keys to roll over them
 def generate_acme_account_rollover_config():
-    # Old account key
-    old_account_key = NamedTemporaryFile()
-    Popen(["openssl", "genrsa", "-out", old_account_key.name, "2048"]).wait()
+    # Old account is directly created by the config generator
+    old_account_key, domain_key, domain_csr, config = generate_config()
 
     # New account key
     new_account_key = NamedTemporaryFile()
     Popen(["openssl", "genrsa", "-out", new_account_key.name, "2048"]).wait()
 
-    # default test configuration
-    config = configparser.ConfigParser()
-    config.read("./example.ini".format(DOMAIN))
-    config["acmednstiny"]["AccountKeyFile"] = old_account_key.name
-    config["acmednstiny"]["CSRFile"] = old_account_key.name
-    config["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
-    config["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
-    config["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
-    config["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
-    config["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM
-    config["DNS"]["Host"] = DNSHOST
-    config["DNS"]["Port"] = DNSPORT
-    config["DNS"]["Zone"] = DNSZONE
-
-    rolloverConfig = NamedTemporaryFile()
-    with open(rolloverConfig.name, 'w') as configfile:
-        config.write(configfile)
-
     return {
         # config and keys (returned to keep files on system)
-        "config": rolloverConfig,
+        "config": config,
         "oldaccountkey": old_account_key,
         "newaccountkey": new_account_key
     }
 
 # generate an account key to delete it
 def generate_acme_account_deactivate_config():
-    # account key
-    account_key = NamedTemporaryFile()
-    Popen(["openssl", "genrsa", "-out", account_key.name, "2048"]).wait()
-
-    # default test configuration
-    config = configparser.ConfigParser()
-    config.read("./example.ini".format(DOMAIN))
-    config["acmednstiny"]["AccountKeyFile"] = account_key.name
-    config["acmednstiny"]["CSRFile"] = account_key.name
-    config["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
-    config["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
-    config["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
-    config["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
-    config["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM
-    config["DNS"]["Host"] = DNSHOST
-    config["DNS"]["Port"] = DNSPORT
-    config["DNS"]["Zone"] = DNSZONE
-
-    deactivateConfig = NamedTemporaryFile()
-    with open(deactivateConfig.name, 'w') as configfile:
-        config.write(configfile)
+    # Account key is created by the by the config generator
+    account_key, domain_key, domain_csr, config = generate_config()
 
     return {
-        "config": deactivateConfig,
+        "config": config,
         "key": account_key
     }
-- 
GitLab


From 6c3a1e0edc0b9a9e4d83a17c509c634307e61ec6 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 16:24:27 +0100
Subject: [PATCH 43/93] v2: fix temporary files removed too fast

---
 tests/config_factory.py               | 30 +++++++++++++--------------
 tests/test_acme_account_deactivate.py |  6 +++---
 tests/test_acme_account_rollover.py   |  2 +-
 tests/test_acme_dns_tiny.py           |  2 +-
 4 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index 85729b3..a46d7d0 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -17,12 +17,12 @@ TSIGALGORITHM = os.getenv("GITLABCI_TSIGALGORITHM")
 # generate simple config
 def generate_config():
     # Account key
-    account_key = NamedTemporaryFile()
+    account_key = NamedTemporaryFile(delete=False)
     Popen(["openssl", "genrsa", "-out", account_key.name, "2048"]).wait()
 
     # Domain key and CSR
-    domain_key = NamedTemporaryFile()
-    domain_csr = NamedTemporaryFile()
+    domain_key = NamedTemporaryFile(delete=False)
+    domain_csr = NamedTemporaryFile(delete=False)
     Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key.name,
         "-subj", "/CN={0}".format(DOMAIN), "-out", domain_csr.name]).wait()
 
@@ -39,7 +39,7 @@ def generate_config():
     parser["DNS"]["Port"] = DNSPORT
     parser["DNS"]["Zone"] = DNSZONE
 
-    config = NamedTemporaryFile()
+    config = NamedTemporaryFile(delete=False)
     with open(config.name, 'w') as configfile:
         parser.write(configfile)
 
@@ -51,12 +51,12 @@ def generate_acme_dns_tiny_config():
     account_key, domain_key, domain_csr, goodCName = generate_config();
 
     # weak 1024 bit account key
-    weak_key = NamedTemporaryFile()
+    weak_key = NamedTemporaryFile(delete=False)
     Popen(["openssl", "genrsa", "-out", weak_key.name, "1024"]).wait()
 
     # CSR using subject alt-name domain instead of CN (common name)
-    san_csr = NamedTemporaryFile()
-    san_conf = NamedTemporaryFile()
+    san_csr = NamedTemporaryFile(delete=False)
+    san_conf = NamedTemporaryFile(delete=False)
     san_conf.write(open("/etc/ssl/openssl.cnf").read().encode("utf8"))
     san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0},DNS:www.{0}\n".format(DOMAIN).encode("utf8"))
     san_conf.seek(0)
@@ -65,7 +65,7 @@ def generate_acme_dns_tiny_config():
         "-out", san_csr.name]).wait()
 
     # CSR signed with the account key
-    account_csr = NamedTemporaryFile()
+    account_csr = NamedTemporaryFile(delete=False)
     Popen(["openssl", "req", "-new", "-sha256", "-key", account_key.name,
         "-subj", "/CN={0}".format(DOMAIN), "-out", account_csr.name]).wait()
 
@@ -73,36 +73,36 @@ def generate_acme_dns_tiny_config():
     config = configparser.ConfigParser()
     config.read(goodCName.name)
 
-    dnsHostIP = NamedTemporaryFile()
+    dnsHostIP = NamedTemporaryFile(delete=False)
     config["DNS"]["Host"] = DNSHOSTIP
     with open(dnsHostIP.name, 'w') as configfile:
         config.write(configfile)
     config["DNS"]["Host"] = DNSHOST
 
-    goodSAN = NamedTemporaryFile()
+    goodSAN = NamedTemporaryFile(delete=False)
     config["acmednstiny"]["AccountKeyFile"] = account_key.name
     config["acmednstiny"]["CSRFile"] = san_csr.name
     with open(goodSAN.name, 'w') as configfile:
         config.write(configfile)
 
-    weakKey = NamedTemporaryFile()
+    weakKey = NamedTemporaryFile(delete=False)
     config["acmednstiny"]["AccountKeyFile"] = weak_key.name
     config["acmednstiny"]["CSRFile"] = domain_csr.name
     with open(weakKey.name, 'w') as configfile:
         config.write(configfile)
 
-    accountAsDomain = NamedTemporaryFile()
+    accountAsDomain = NamedTemporaryFile(delete=False)
     config["acmednstiny"]["AccountKeyFile"] = account_key.name
     config["acmednstiny"]["CSRFile"] = account_csr.name
     with open(accountAsDomain.name, 'w') as configfile:
         config.write(configfile)
 
-    invalidTSIGName = NamedTemporaryFile()
+    invalidTSIGName = NamedTemporaryFile(delete=False)
     config["TSIGKeyring"]["KeyName"] = "{0}.invalid".format(TSIGKEYNAME)
     with open(invalidTSIGName.name, 'w') as configfile:
         config.write(configfile)
 
-    missingDNS = NamedTemporaryFile()
+    missingDNS = NamedTemporaryFile(delete=False)
     config["DNS"] = {}
     with open(missingDNS.name, 'w') as configfile:
         config.write(configfile)
@@ -132,7 +132,7 @@ def generate_acme_account_rollover_config():
     old_account_key, domain_key, domain_csr, config = generate_config()
 
     # New account key
-    new_account_key = NamedTemporaryFile()
+    new_account_key = NamedTemporaryFile(delete=False)
     Popen(["openssl", "genrsa", "-out", new_account_key.name, "2048"]).wait()
 
     return {
diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index a889137..a06743e 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -19,9 +19,9 @@ class TestACMEAccountDeactivate(unittest.TestCase):
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
-        # close temp files correctly
-        self.config.close()
-        self.accountkey.close()
+        # Remove temporary files
+        os.remove(self.config.name)
+        os.remove(self.account_key)
         super(TestACMEAccountDeactivate, self).tearDownClass()
 
     def test_success_account_deactivate(self):
diff --git a/tests/test_acme_account_rollover.py b/tests/test_acme_account_rollover.py
index 57c3062..a6411ef 100644
--- a/tests/test_acme_account_rollover.py
+++ b/tests/test_acme_account_rollover.py
@@ -22,7 +22,7 @@ class TestACMEAccountRollover(unittest.TestCase):
         account_deactivate(self.configs["newaccountkey"].name, ACMEDirectory)
         # close temp files correctly
         for tmpfile in self.configs:
-            self.configs[tmpfile].close()
+            os.remove(self.configs[tmpfile])
         super(TestACMEAccountRollover, self).tearDownClass()
 
     def test_success_account_rollover(self):
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index afd6fe1..668de8b 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -26,7 +26,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         account_deactivate(self.configs["accountkey"].name, ACMEDirectory)
         # close temp files correctly
         for tmpfile in self.configs:
-            self.configs[tmpfile].close()
+            os.remove(self.configs[tmpfile])
         super(TestACMEDNSTiny, self).tearDownClass()
 
     # Add a sleeping time between each test, to avoid issue with order status
-- 
GitLab


From f9f1774064b00ad170baf95367c8eda9528c5b52 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 17:05:32 +0100
Subject: [PATCH 44/93] v2: tests: fix use of temporary files (we can directly
 pass names now)

---
 tests/config_factory.py               | 34 +++++++++++++--------------
 tests/test_acme_account_deactivate.py |  6 ++---
 tests/test_acme_account_rollover.py   |  8 +++----
 tests/test_acme_dns_tiny.py           | 18 +++++++-------
 4 files changed, 33 insertions(+), 33 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index a46d7d0..ba25903 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -43,7 +43,7 @@ def generate_config():
     with open(config.name, 'w') as configfile:
         parser.write(configfile)
 
-    return account_key, domain_key, domain_csr, config
+    return account_key.name, domain_key.name, domain_csr.name, config.name
 
 # generate account and domain keys
 def generate_acme_dns_tiny_config():
@@ -60,18 +60,18 @@ def generate_acme_dns_tiny_config():
     san_conf.write(open("/etc/ssl/openssl.cnf").read().encode("utf8"))
     san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0},DNS:www.{0}\n".format(DOMAIN).encode("utf8"))
     san_conf.seek(0)
-    Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key.name,
+    Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key,
         "-subj", "/", "-reqexts", "SAN", "-config", san_conf.name,
         "-out", san_csr.name]).wait()
 
     # CSR signed with the account key
     account_csr = NamedTemporaryFile(delete=False)
-    Popen(["openssl", "req", "-new", "-sha256", "-key", account_key.name,
+    Popen(["openssl", "req", "-new", "-sha256", "-key", account_key,
         "-subj", "/CN={0}".format(DOMAIN), "-out", account_csr.name]).wait()
 
     # Create config parser from the good default config to generate custom configs
     config = configparser.ConfigParser()
-    config.read(goodCName.name)
+    config.read(goodCName)
 
     dnsHostIP = NamedTemporaryFile(delete=False)
     config["DNS"]["Host"] = DNSHOSTIP
@@ -80,19 +80,19 @@ def generate_acme_dns_tiny_config():
     config["DNS"]["Host"] = DNSHOST
 
     goodSAN = NamedTemporaryFile(delete=False)
-    config["acmednstiny"]["AccountKeyFile"] = account_key.name
+    config["acmednstiny"]["AccountKeyFile"] = account_key
     config["acmednstiny"]["CSRFile"] = san_csr.name
     with open(goodSAN.name, 'w') as configfile:
         config.write(configfile)
 
     weakKey = NamedTemporaryFile(delete=False)
     config["acmednstiny"]["AccountKeyFile"] = weak_key.name
-    config["acmednstiny"]["CSRFile"] = domain_csr.name
+    config["acmednstiny"]["CSRFile"] = domain_csr
     with open(weakKey.name, 'w') as configfile:
         config.write(configfile)
 
     accountAsDomain = NamedTemporaryFile(delete=False)
-    config["acmednstiny"]["AccountKeyFile"] = account_key.name
+    config["acmednstiny"]["AccountKeyFile"] = account_key
     config["acmednstiny"]["CSRFile"] = account_csr.name
     with open(accountAsDomain.name, 'w') as configfile:
         config.write(configfile)
@@ -110,20 +110,20 @@ def generate_acme_dns_tiny_config():
     return {
         # configs
         "goodCName": goodCName,
-        "dnsHostIP": dnsHostIP,
-        "goodSAN": goodSAN,
-        "weakKey": weakKey,
-        "accountAsDomain": accountAsDomain,
-        "invalidTSIGName": invalidTSIGName,
-        "missingDNS": missingDNS,
+        "dnsHostIP": dnsHostIP.name,
+        "goodSAN": goodSAN.name,
+        "weakKey": weakKey.name,
+        "accountAsDomain": accountAsDomain.name,
+        "invalidTSIGName": invalidTSIGName.name,
+        "missingDNS": missingDNS.name,
         # keys (returned to keep files on system)
         "accountkey": account_key,
-        "weakkey": weak_key,
+        "weakkey": weak_key.name,
         "domainkey": domain_key,
         # csr (returned to keep files on system)
         "domaincsr": domain_csr,
-        "sancsr": san_csr,
-        "accountcsr": account_csr
+        "sancsr": san_csr.name,
+        "accountcsr": account_csr.name
     }
 
 # generate two account keys to roll over them
@@ -139,7 +139,7 @@ def generate_acme_account_rollover_config():
         # config and keys (returned to keep files on system)
         "config": config,
         "oldaccountkey": old_account_key,
-        "newaccountkey": new_account_key
+        "newaccountkey": new_account_key.name
     }
 
 # generate an account key to delete it
diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index a06743e..ec140bb 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -13,21 +13,21 @@ class TestACMEAccountDeactivate(unittest.TestCase):
         configs = generate_acme_account_deactivate_config()
         self.config = configs["config"]
         self.account_key = configs["key"]
-        acme_dns_tiny.main([self.config.name])
+        acme_dns_tiny.main([self.config])
         super(TestACMEAccountDeactivate, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
         # Remove temporary files
-        os.remove(self.config.name)
+        os.remove(self.config)
         os.remove(self.account_key)
         super(TestACMEAccountDeactivate, self).tearDownClass()
 
     def test_success_account_deactivate(self):
         """ Test success account key deactivate """
         with self.assertLogs(level='INFO') as accountdeactivatelog:
-            tools.acme_account_deactivate.main(["--account-key", self.accountkey.name,
+            tools.acme_account_deactivate.main(["--account-key", self.account_key,
                                             "--acme-directory", ACMEDirectory])
         self.assertIn("INFO:acme_account_deactivate:Account key deactivated !",
             accountdeactivatelog.output)
diff --git a/tests/test_acme_account_rollover.py b/tests/test_acme_account_rollover.py
index a6411ef..ab63703 100644
--- a/tests/test_acme_account_rollover.py
+++ b/tests/test_acme_account_rollover.py
@@ -12,14 +12,14 @@ class TestACMEAccountRollover(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         self.configs = generate_acme_account_rollover_config()
-        acme_dns_tiny.main([self.configs['config'].name])
+        acme_dns_tiny.main([self.configs['config']])
         super(TestACMEAccountRollover, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
         # deactivate account key registration at end of tests
-        account_deactivate(self.configs["newaccountkey"].name, ACMEDirectory)
+        account_deactivate(self.configs["newaccountkey"], ACMEDirectory)
         # close temp files correctly
         for tmpfile in self.configs:
             os.remove(self.configs[tmpfile])
@@ -28,8 +28,8 @@ class TestACMEAccountRollover(unittest.TestCase):
     def test_success_account_rollover(self):
         """ Test success account key rollover """
         with self.assertLogs(level='INFO') as accountrolloverlog:
-            tools.acme_account_rollover.main(["--current", self.configs['oldaccountkey'].name,
-                                          	    "--new", self.configs['newaccountkey'].name,
+            tools.acme_account_rollover.main(["--current", self.configs['oldaccountkey'],
+                                          	    "--new", self.configs['newaccountkey'],
                                           	    "--acme-directory", ACMEDirectory])
         self.assertIn("INFO:acme_account_rollover:Account keys rolled over !",
             accountrolloverlog.output)
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 668de8b..bca10b3 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -23,7 +23,7 @@ class TestACMEDNSTiny(unittest.TestCase):
     @classmethod
     def tearDownClass(self):
         # deactivate account key registration at end of tests
-        account_deactivate(self.configs["accountkey"].name, ACMEDirectory)
+        account_deactivate(self.configs["accountkey"], ACMEDirectory)
         # close temp files correctly
         for tmpfile in self.configs:
             os.remove(self.configs[tmpfile])
@@ -60,7 +60,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         old_stdout = sys.stdout
         sys.stdout = StringIO()
         
-        acme_dns_tiny.main([self.configs['goodCName'].name])
+        acme_dns_tiny.main([self.configs['goodCName']])
         certchain = sys.stdout.getvalue()
         
         sys.stdout.close()
@@ -74,7 +74,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         sys.stdout = StringIO()
         
         with self.assertLogs(level='INFO') as adnslog:
-            acme_dns_tiny.main([self.configs['dnsHostIP'].name])
+            acme_dns_tiny.main([self.configs['dnsHostIP']])
         self.assertIn("INFO:acme_dns_tiny:A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if exists or directly the DNS Host configuration.",
             adnslog.output)
         certchain = sys.stdout.getvalue()
@@ -89,7 +89,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         old_stdout = sys.stdout
         sys.stdout = StringIO()
         
-        acme_dns_tiny.main([self.configs['goodSAN'].name])
+        acme_dns_tiny.main([self.configs['goodSAN']])
         certchain = sys.stdout.getvalue()
         
         sys.stdout.close()
@@ -100,7 +100,7 @@ class TestACMEDNSTiny(unittest.TestCase):
     def test_success_cli(self):
         """ Successfully issue a certificate via command line interface """
         certout, err = subprocess.Popen([
-            "python3", "acme_dns_tiny.py", self.configs['goodCName'].name
+            "python3", "acme_dns_tiny.py", self.configs['goodCName']
         ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
         
         certchain = certout.decode("utf8")
@@ -111,25 +111,25 @@ class TestACMEDNSTiny(unittest.TestCase):
         """ Let's Encrypt rejects weak keys """
         self.assertRaisesRegex(ValueError,
                                "key too small",
-                               acme_dns_tiny.main, [self.configs['weakKey'].name])
+                               acme_dns_tiny.main, [self.configs['weakKey']])
 
     def test_account_key_domain(self):
         """ Can't use the account key for the CSR """
         self.assertRaisesRegex(ValueError,
                                "certificate public key must be different than account key",
-                               acme_dns_tiny.main, [self.configs['accountAsDomain'].name])
+                               acme_dns_tiny.main, [self.configs['accountAsDomain']])
 
     def test_failure_dns_update_tsigkeyname(self):
         """ Fail to update DNS records by invalid TSIG Key name """
         self.assertRaisesRegex(ValueError,
                                "Error updating DNS",
-                               acme_dns_tiny.main, [self.configs['invalidTSIGName'].name])
+                               acme_dns_tiny.main, [self.configs['invalidTSIGName']])
 
     def test_failure_notcompleted_configuration(self):
         """ Configuration file have to be completed """
         self.assertRaisesRegex(ValueError,
                                "Some required settings are missing\.",
-                               acme_dns_tiny.main, [self.configs['missingDNS'].name])
+                               acme_dns_tiny.main, [self.configs['missingDNS']])
 
 if __name__ == "__main__":
     unittest.main()
-- 
GitLab


From be1cb41a66811a64ebf818401b29e43686d7e20d Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 18:19:30 +0100
Subject: [PATCH 45/93] v2: tests: fix configuration generator refactor

---
 tests/config_factory.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index ba25903..1a93655 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -29,6 +29,8 @@ def generate_config():
     # acme-dns-tiny configuration
     parser = configparser.ConfigParser()
     parser.read("./example.ini")
+    config["acmednstiny"]["AccountKeyFile"] = account_key.name
+    config["acmednstiny"]["CSRFile"] = domain_csr.name
     parser["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
     parser["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
     parser["acmednstiny"]["Contacts"] = "mailto:mail@example.com"
-- 
GitLab


From a40146bf86396a9664480ab04ef97baadfd7c0e6 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 18:21:55 +0100
Subject: [PATCH 46/93] =?UTF-8?q?test:=20fix=20copy/paste=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 tests/config_factory.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index 1a93655..bc15f30 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -29,8 +29,8 @@ def generate_config():
     # acme-dns-tiny configuration
     parser = configparser.ConfigParser()
     parser.read("./example.ini")
-    config["acmednstiny"]["AccountKeyFile"] = account_key.name
-    config["acmednstiny"]["CSRFile"] = domain_csr.name
+    parser["acmednstiny"]["AccountKeyFile"] = account_key.name
+    parser["acmednstiny"]["CSRFile"] = domain_csr.name
     parser["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
     parser["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
     parser["acmednstiny"]["Contacts"] = "mailto:mail@example.com"
-- 
GitLab


From 0c2136b4e499966184e34e170b798e78a15ff8ed Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 20:58:52 +0100
Subject: [PATCH 47/93] tools: add python3 as environment

---
 tools/acme_account_deactivate.py | 1 +
 tools/acme_account_rollover.py   | 1 +
 2 files changed, 2 insertions(+)

diff --git a/tools/acme_account_deactivate.py b/tools/acme_account_deactivate.py
index 8a9d8b6..6c539cc 100644
--- a/tools/acme_account_deactivate.py
+++ b/tools/acme_account_deactivate.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python3
 import sys, os, argparse, subprocess, json, base64, binascii, re, copy, logging
 from urllib.request import urlopen
 from urllib.error import HTTPError
diff --git a/tools/acme_account_rollover.py b/tools/acme_account_rollover.py
index 469b154..fe82459 100644
--- a/tools/acme_account_rollover.py
+++ b/tools/acme_account_rollover.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python3
 import sys, os, argparse, subprocess, json, base64, binascii, hashlib, re, copy, logging
 from urllib.request import urlopen
 from urllib.error import HTTPError
-- 
GitLab


From 4970f2ce8618a68a26a18f30bbe9cf5d25ed52d4 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 20:59:22 +0100
Subject: [PATCH 48/93] test: account rollover: fix to avoid multiple dump of
 the JSON object

---
 tools/acme_account_rollover.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/tools/acme_account_rollover.py b/tools/acme_account_rollover.py
index fe82459..403e1fc 100644
--- a/tools/acme_account_rollover.py
+++ b/tools/acme_account_rollover.py
@@ -58,13 +58,14 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
         protected64 = _b64(json.dumps(protected).encode("utf8"))
         signature = _openssl("dgst", ["-sha256", "-sign", keypath],
                              "{0}.{1}".format(protected64, payload64).encode("utf8"))
-        signedjws = json.dumps({
+        signedjws = {
             "protected": protected64, "payload": payload64,"signature": _b64(signature)
-        })
+        }
         return signedjws
 
     # helper function make signed requests
     def _send_signed_request(url, keypath, payload):
+        nonlocal jws_nonce
         data = json.dumps(_sign_request(url, keypath, payload))
         try:
             resp = urlopen(url, data.encode("utf8"))
-- 
GitLab


From 2a65ff75d0c0aab4253e95f42d7e795dfbb77904 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 20:59:58 +0100
Subject: [PATCH 49/93] =?UTF-8?q?test:=20account=20rollover:=20after=20key?=
 =?UTF-8?q?=20rolled,=20we=20continue=20to=20need=20to=20deactivate=20with?=
 =?UTF-8?q?=20the=20original=20key,=20strange=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 tests/test_acme_account_rollover.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_acme_account_rollover.py b/tests/test_acme_account_rollover.py
index ab63703..f3818bb 100644
--- a/tests/test_acme_account_rollover.py
+++ b/tests/test_acme_account_rollover.py
@@ -19,7 +19,7 @@ class TestACMEAccountRollover(unittest.TestCase):
     @classmethod
     def tearDownClass(self):
         # deactivate account key registration at end of tests
-        account_deactivate(self.configs["newaccountkey"], ACMEDirectory)
+        account_deactivate(self.configs["oldaccountkey"], ACMEDirectory)
         # close temp files correctly
         for tmpfile in self.configs:
             os.remove(self.configs[tmpfile])
-- 
GitLab


From 66c47ea1313d83f2fdab168b681b152e663c017d Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 21:28:19 +0100
Subject: [PATCH 50/93] tests: fix acme_account_deactivate

---
 tests/test_acme_account_deactivate.py | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index ec140bb..d99c139 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -10,24 +10,22 @@ class TestACMEAccountDeactivate(unittest.TestCase):
 
     @classmethod
     def setUpClass(self):
-        configs = generate_acme_account_deactivate_config()
-        self.config = configs["config"]
-        self.account_key = configs["key"]
-        acme_dns_tiny.main([self.config])
+        self.configs = generate_acme_account_deactivate_config()
+        acme_dns_tiny.main([self.configs['config']])
         super(TestACMEAccountDeactivate, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
     @classmethod
     def tearDownClass(self):
         # Remove temporary files
-        os.remove(self.config)
-        os.remove(self.account_key)
+        os.remove(self.configs['config'])
+        os.remove(self.configs['key'])
         super(TestACMEAccountDeactivate, self).tearDownClass()
 
     def test_success_account_deactivate(self):
         """ Test success account key deactivate """
         with self.assertLogs(level='INFO') as accountdeactivatelog:
-            tools.acme_account_deactivate.main(["--account-key", self.account_key,
+            tools.acme_account_deactivate.main(["--account-key", self.configs['key'],
                                             "--acme-directory", ACMEDirectory])
         self.assertIn("INFO:acme_account_deactivate:Account key deactivated !",
             accountdeactivatelog.output)
-- 
GitLab


From 61790d2cfbce724ca0b3274d7f4acb434143a4b4 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 25 Feb 2018 21:39:51 +0100
Subject: [PATCH 51/93] tests: fix acme_account_deactivate

---
 tests/test_acme_account_deactivate.py | 7 ++++++-
 tests/test_acme_account_rollover.py   | 7 ++++++-
 tests/test_acme_dns_tiny.py           | 4 ++--
 3 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index d99c139..2e53182 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -1,4 +1,4 @@
-import unittest, os
+import unittest, os, time
 import acme_dns_tiny
 from tests.config_factory import generate_acme_account_deactivate_config
 import tools.acme_account_deactivate
@@ -22,6 +22,11 @@ class TestACMEAccountDeactivate(unittest.TestCase):
         os.remove(self.configs['key'])
         super(TestACMEAccountDeactivate, self).tearDownClass()
 
+    # Add a sleeping time between each test, to avoid issues with order/challenge status
+    @classmethod
+    def setUp(self):
+        time.sleep(5);
+
     def test_success_account_deactivate(self):
         """ Test success account key deactivate """
         with self.assertLogs(level='INFO') as accountdeactivatelog:
diff --git a/tests/test_acme_account_rollover.py b/tests/test_acme_account_rollover.py
index f3818bb..ea0e343 100644
--- a/tests/test_acme_account_rollover.py
+++ b/tests/test_acme_account_rollover.py
@@ -1,4 +1,4 @@
-import unittest, os
+import unittest, os, time
 import acme_dns_tiny
 from tests.config_factory import generate_acme_account_rollover_config
 from tools.acme_account_deactivate import account_deactivate
@@ -25,6 +25,11 @@ class TestACMEAccountRollover(unittest.TestCase):
             os.remove(self.configs[tmpfile])
         super(TestACMEAccountRollover, self).tearDownClass()
 
+    # Add a sleeping time between each test, to avoid issues with order/challenge status
+    @classmethod
+    def setUp(self):
+        time.sleep(5);
+
     def test_success_account_rollover(self):
         """ Test success account key rollover """
         with self.assertLogs(level='INFO') as accountrolloverlog:
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index bca10b3..d54a536 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -29,9 +29,9 @@ class TestACMEDNSTiny(unittest.TestCase):
             os.remove(self.configs[tmpfile])
         super(TestACMEDNSTiny, self).tearDownClass()
 
-    # Add a sleeping time between each test, to avoid issue with order status
+    # Add a sleeping time between each test, to avoid issues with order/challenge status
     @classmethod
-    def tearDown(self):
+    def setUp(self):
         time.sleep(5);
 
     # helper function to run openssl command
-- 
GitLab


From a21921f0cbe7b1bf572e4f72e080fd975abc2815 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 26 Feb 2018 21:36:35 +0100
Subject: [PATCH 52/93] test account deactivate: don't fail if a
 non-regesitering error occured

---
 tests/test_acme_account_deactivate.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index 2e53182..24e4a07 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -11,7 +11,12 @@ class TestACMEAccountDeactivate(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         self.configs = generate_acme_account_deactivate_config()
-        acme_dns_tiny.main([self.configs['config']])
+        try:
+            acme_dns_tiny.main([self.configs['config']])
+        except ValueError as err:
+            if str(err).startswith("Error register"):
+                raise ValueError("Fail test as account has not been registered correctly: {0}".format(err))
+        
         super(TestACMEAccountDeactivate, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
-- 
GitLab


From b5e965181edc2d2d14b3522e65bdf8b487069c4c Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 27 Feb 2018 20:48:03 +0100
Subject: [PATCH 53/93] HTTP communications now set User-Agent header with
 acme-dns-tiny info

---
 acme_dns_tiny.py                 | 20 +++++++++++---------
 tools/acme_account_deactivate.py | 10 ++++++----
 tools/acme_account_rollover.py   | 10 ++++++----
 3 files changed, 23 insertions(+), 17 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index c75cbfd..8f532c1 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -2,7 +2,7 @@
 import os, argparse, subprocess, json, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging
 import dns.resolver, dns.tsigkeyring, dns.update
 from configparser import ConfigParser
-from urllib.request import urlopen
+import urllib.request
 from urllib.error import HTTPError
 
 LOGGER = logging.getLogger('acme_dns_tiny')
@@ -40,7 +40,7 @@ def get_crt(config, log=LOGGER):
         nonlocal jws_nonce
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         protected = copy.deepcopy(jws_header)
-        protected["nonce"] = jws_nonce or urlopen(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+        protected["nonce"] = jws_nonce or webclient.open(acme_config["newNonce"]).getheader("Replay-Nonce", None)
         protected["url"] = url
         if url == acme_config["newAccount"]:
             del protected["kid"]
@@ -53,7 +53,7 @@ def get_crt(config, log=LOGGER):
             "protected": protected64, "payload": payload64,"signature": _b64(signature)
         })
         try:
-            resp = urlopen(url, data.encode("utf8"))
+            resp = webclient.open(url, data.encode("utf8"))
         except HTTPError as httperror:
             resp = httperror
         finally:
@@ -61,8 +61,10 @@ def get_crt(config, log=LOGGER):
             return resp.getcode(), resp.read(), resp.getheaders()
 
     # main code
+    webclient = urllib.request.build_opener()
+    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0')]
     log.info("Read ACME directory.")
-    directory = urlopen(config["acmednstiny"]["ACMEDirectory"])
+    directory = webclient.open(config["acmednstiny"]["ACMEDirectory"])
     acme_config = json.loads(directory.read().decode("utf8"))
     terms_service = acme_config.get("meta", {}).get("termsOfService")
 
@@ -164,7 +166,7 @@ def get_crt(config, log=LOGGER):
         log.info("Completing authz: {0}".format(authz))
 
         # get new challenge
-        resp = urlopen(authz)
+        resp = webclient.open(authz)
         authorization = json.loads(resp.read().decode("utf8"))
         if resp.getcode() != 200:
             raise ValueError("Error requesting challenges: {0} {1}".format(resp.getcode(), authorization))
@@ -211,7 +213,7 @@ def get_crt(config, log=LOGGER):
         try:
             while True:
                 try:
-                    resp = urlopen(challenge["url"])
+                    resp = webclient.open(challenge["url"])
                     challenge_status = json.loads(resp.read().decode("utf8"))
                 except IOError as e:
                     raise ValueError("Error checking challenge: {0} {1}".format(
@@ -228,7 +230,7 @@ def get_crt(config, log=LOGGER):
             _update_dns(dnsrr_set, "delete")
 
     log.info("Finalizing the order...")
-    resp = urlopen(order_location)
+    resp = webclient.open(order_location)
     finalize = json.loads(resp.read().decode("utf8"))
     csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
     code, result, headers = _send_signed_request(order["finalize"], {"csr": csr_der})
@@ -237,7 +239,7 @@ def get_crt(config, log=LOGGER):
 
     while True:
         try:
-            resp = urlopen(order_location)
+            resp = webclient.open(order_location)
             finalize = json.loads(resp.read().decode("utf8"))
         except IOError as e:
             raise ValueError("Error finalizing order: {0} {1}".format(
@@ -252,7 +254,7 @@ def get_crt(config, log=LOGGER):
             raise ValueError("Finalizing order {0} got errors: {1}".format(
                 domain, finalize))
     
-    resp = urlopen(finalize["certificate"])
+    resp = webclient.open(finalize["certificate"])
     if resp.getcode() != 200:
         raise ValueError("Finalizing order {0} got errors: {1}".format(
             resp.getcode(), resp.read.decode("utf8")))
diff --git a/tools/acme_account_deactivate.py b/tools/acme_account_deactivate.py
index 6c539cc..181051e 100644
--- a/tools/acme_account_deactivate.py
+++ b/tools/acme_account_deactivate.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 import sys, os, argparse, subprocess, json, base64, binascii, re, copy, logging
-from urllib.request import urlopen
+import urllib.request
 from urllib.error import HTTPError
 
 LOGGER = logging.getLogger("acme_account_deactivate")
@@ -26,7 +26,7 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
         nonlocal jws_nonce
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         protected = copy.deepcopy(jws_header)
-        protected["nonce"] = jws_nonce or urlopen(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+        protected["nonce"] = jws_nonce or webclient.open(acme_config["newNonce"]).getheader("Replay-Nonce", None)
         protected["url"] = url
         if url == acme_config["newAccount"]:
             del protected["kid"]
@@ -39,15 +39,17 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
             "protected": protected64, "payload": payload64,"signature": _b64(signature)
         })
         try:
-            resp = urlopen(url, data.encode("utf8"))
+            resp = webclient.open(url, data.encode("utf8"))
         except HTTPError as httperror:
             resp = httperror
         finally:
             jws_nonce = resp.getheader("Replay-Nonce", None)
             return resp.getcode(), resp.read(), resp.getheaders()
 
+    webclient = urllib.request.build_opener();
+    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0/account_deactivate')]
     log.info("Reading ACME directory.")
-    directory = urlopen(acme_directory)
+    directory = webclient.open(acme_directory)
     acme_config = json.loads(directory.read().decode("utf8"))
 
     log.info("Parsing account key.")
diff --git a/tools/acme_account_rollover.py b/tools/acme_account_rollover.py
index 403e1fc..b50910a 100644
--- a/tools/acme_account_rollover.py
+++ b/tools/acme_account_rollover.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 import sys, os, argparse, subprocess, json, base64, binascii, hashlib, re, copy, logging
-from urllib.request import urlopen
+import urllib.request
 from urllib.error import HTTPError
 
 LOGGER = logging.getLogger("acme_account_rollover")
@@ -46,7 +46,7 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         if keypath == accountkeypath:
             protected = copy.deepcopy(jws_header)
-            protected["nonce"] = jws_nonce or urlopen(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+            protected["nonce"] = jws_nonce or webclient.open(acme_config["newNonce"]).getheader("Replay-Nonce", None)
         elif keypath == new_accountkeypath:
             protected = copy.deepcopy(new_jws_header)
         if (keypath == new_accountkeypath
@@ -68,15 +68,17 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
         nonlocal jws_nonce
         data = json.dumps(_sign_request(url, keypath, payload))
         try:
-            resp = urlopen(url, data.encode("utf8"))
+            resp = webclient.open(url, data.encode("utf8"))
         except HTTPError as httperror:
             resp = httperror
         finally:
             jws_nonce = resp.getheader("Replay-Nonce", None)
             return resp.getcode(), resp.read(), resp.getheaders()
 
+    webclient = urllib.request.build_opener();
+    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0/account_rollover')]
     log.info("Reading ACME directory.")
-    directory = urlopen(acme_directory)
+    directory = webclient.open(acme_directory)
     acme_config = json.loads(directory.read().decode("utf8"))
 
     log.info("Parsing current account key...")
-- 
GitLab


From 1d7a64cd4e371b202847cf8984d5b908e0928b09 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 27 Feb 2018 21:12:00 +0100
Subject: [PATCH 54/93] test: add wildcard domain tests

---
 tests/config_factory.py     | 37 ++++++++++++++++++++++++++++++-------
 tests/test_acme_dns_tiny.py | 26 ++++++++++++++++++++++++++
 2 files changed, 56 insertions(+), 7 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index bc15f30..5621cef 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -52,6 +52,11 @@ def generate_acme_dns_tiny_config():
     # Simple good configuration
     account_key, domain_key, domain_csr, goodCName = generate_config();
 
+    # CSR for good configuration with wildcard domain
+    wilddomain_csr = NamedTemporaryFile(delete=False)
+    Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key,
+           "-subj", "/CN=*.{0}".format(DOMAIN), "-out", wilddomain_csr.name]).wait()
+
     # weak 1024 bit account key
     weak_key = NamedTemporaryFile(delete=False)
     Popen(["openssl", "genrsa", "-out", weak_key.name, "1024"]).wait()
@@ -66,6 +71,16 @@ def generate_acme_dns_tiny_config():
         "-subj", "/", "-reqexts", "SAN", "-config", san_conf.name,
         "-out", san_csr.name]).wait()
 
+    # CSR using wildcard in subject alt-name domain
+    wildsan_csr = NamedTemporaryFile(delete=False)
+    wildsan_conf = NamedTemporaryFile(delete=False)
+    wildsan_conf.write(open("/etc/ssl/openssl.cnf").read().encode("utf8"))
+    wildsan_conf.write("\n[SAN]\nsubjectAltName=DNS:{0},DNS:*.{0}\n".format(DOMAIN).encode("utf8"))
+    wildsan_conf.seek(0)
+    Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key,
+           "-subj", "/", "-reqexts", "SAN", "-config", wildsan_conf.name,
+           "-out", wildsan_csr.name]).wait()
+
     # CSR signed with the account key
     account_csr = NamedTemporaryFile(delete=False)
     Popen(["openssl", "req", "-new", "-sha256", "-key", account_key,
@@ -75,6 +90,12 @@ def generate_acme_dns_tiny_config():
     config = configparser.ConfigParser()
     config.read(goodCName)
 
+    wildCName = NamedTemporaryFile(delete=False)
+    config["acmednstiny"]["AccountKeyFile"] = account_key
+    config["acmednstiny"]["CSRFile"] = wilddomain_csr.name
+    with open(wildCName.name, 'w') as configfile:
+        config.write(configfile)
+
     dnsHostIP = NamedTemporaryFile(delete=False)
     config["DNS"]["Host"] = DNSHOSTIP
     with open(dnsHostIP.name, 'w') as configfile:
@@ -87,6 +108,12 @@ def generate_acme_dns_tiny_config():
     with open(goodSAN.name, 'w') as configfile:
         config.write(configfile)
 
+    wildSAN = NamedTemporaryFile(delete=False)
+    config["acmednstiny"]["AccountKeyFile"] = account_key
+    config["acmednstiny"]["CSRFile"] = wildsan_csr.name
+    with open(wildSAN.name, 'w') as configfile:
+        config.write(configfile)
+
     weakKey = NamedTemporaryFile(delete=False)
     config["acmednstiny"]["AccountKeyFile"] = weak_key.name
     config["acmednstiny"]["CSRFile"] = domain_csr
@@ -112,20 +139,16 @@ def generate_acme_dns_tiny_config():
     return {
         # configs
         "goodCName": goodCName,
+        "wildCName": wildCName.name,
         "dnsHostIP": dnsHostIP.name,
         "goodSAN": goodSAN.name,
+        "wildSAN": wildSAN.name,
         "weakKey": weakKey.name,
         "accountAsDomain": accountAsDomain.name,
         "invalidTSIGName": invalidTSIGName.name,
         "missingDNS": missingDNS.name,
-        # keys (returned to keep files on system)
+        # key (just to simply remove the account from staging server)
         "accountkey": account_key,
-        "weakkey": weak_key.name,
-        "domainkey": domain_key,
-        # csr (returned to keep files on system)
-        "domaincsr": domain_csr,
-        "sancsr": san_csr.name,
-        "accountcsr": account_csr.name
     }
 
 # generate two account keys to roll over them
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index d54a536..b85fe32 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -68,6 +68,19 @@ class TestACMEDNSTiny(unittest.TestCase):
         
         self.assertCertificateChain(certchain)
 
+    def test_success_wild_cn(self):
+        """ Successfully issue a certificate via a wildcard common name """
+        old_stdout = sys.stdout
+        sys.stdout = StringIO()
+
+        acme_dns_tiny.main([self.configs['wildCName']])
+        certchain = sys.stdout.getvalue()
+
+        sys.stdout.close()
+        sys.stdout = old_stdout
+
+        self.assertCertificateChain(certchain)
+
     def test_success_dnshost_ip(self):
         """ When DNS Host is an IP, DNS resolution have to fail without error """
         old_stdout = sys.stdout
@@ -97,6 +110,19 @@ class TestACMEDNSTiny(unittest.TestCase):
         
         self.assertCertificateChain(certchain)
 
+    def test_success_wildsan(self):
+        """ Successfully issue a certificate via wildcard in subject alt name """
+        old_stdout = sys.stdout
+        sys.stdout = StringIO()
+
+        acme_dns_tiny.main([self.configs['wildSAN']])
+        certchain = sys.stdout.getvalue()
+
+        sys.stdout.close()
+        sys.stdout = old_stdout
+
+        self.assertCertificateChain(certchain)
+
     def test_success_cli(self):
         """ Successfully issue a certificate via command line interface """
         certout, err = subprocess.Popen([
-- 
GitLab


From d12f6aa9e185f30063b2f5179405da83a5db91ff Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 27 Feb 2018 21:24:55 +0100
Subject: [PATCH 55/93] remove all tests workaround: timing workaround is
 already available with the "CheckChallengeDelay" form the configuration.

---
 tests/test_acme_account_deactivate.py | 12 +-----------
 tests/test_acme_account_rollover.py   |  5 -----
 tests/test_acme_dns_tiny.py           |  5 -----
 3 files changed, 1 insertion(+), 21 deletions(-)

diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index 24e4a07..230d441 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -11,12 +11,7 @@ class TestACMEAccountDeactivate(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         self.configs = generate_acme_account_deactivate_config()
-        try:
-            acme_dns_tiny.main([self.configs['config']])
-        except ValueError as err:
-            if str(err).startswith("Error register"):
-                raise ValueError("Fail test as account has not been registered correctly: {0}".format(err))
-        
+        acme_dns_tiny.main([self.configs['config']])
         super(TestACMEAccountDeactivate, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
@@ -27,11 +22,6 @@ class TestACMEAccountDeactivate(unittest.TestCase):
         os.remove(self.configs['key'])
         super(TestACMEAccountDeactivate, self).tearDownClass()
 
-    # Add a sleeping time between each test, to avoid issues with order/challenge status
-    @classmethod
-    def setUp(self):
-        time.sleep(5);
-
     def test_success_account_deactivate(self):
         """ Test success account key deactivate """
         with self.assertLogs(level='INFO') as accountdeactivatelog:
diff --git a/tests/test_acme_account_rollover.py b/tests/test_acme_account_rollover.py
index ea0e343..e7fed76 100644
--- a/tests/test_acme_account_rollover.py
+++ b/tests/test_acme_account_rollover.py
@@ -25,11 +25,6 @@ class TestACMEAccountRollover(unittest.TestCase):
             os.remove(self.configs[tmpfile])
         super(TestACMEAccountRollover, self).tearDownClass()
 
-    # Add a sleeping time between each test, to avoid issues with order/challenge status
-    @classmethod
-    def setUp(self):
-        time.sleep(5);
-
     def test_success_account_rollover(self):
         """ Test success account key rollover """
         with self.assertLogs(level='INFO') as accountrolloverlog:
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index b85fe32..3aa563b 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -29,11 +29,6 @@ class TestACMEDNSTiny(unittest.TestCase):
             os.remove(self.configs[tmpfile])
         super(TestACMEDNSTiny, self).tearDownClass()
 
-    # Add a sleeping time between each test, to avoid issues with order/challenge status
-    @classmethod
-    def setUp(self):
-        time.sleep(5);
-
     # helper function to run openssl command
     def openssl(self, command, options, communicate=None):
         openssl = subprocess.Popen(["openssl", command] + options,
-- 
GitLab


From 3cbd54aba2d60e8aa3585b870a64d207094a8daa Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 27 Feb 2018 21:27:33 +0100
Subject: [PATCH 56/93] acme-dns-tiny: typos in log info

---
 acme_dns_tiny.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 8f532c1..e3bdcdf 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -184,13 +184,13 @@ def get_crt(config, log=LOGGER):
         except dns.exception.DNSException as dnsexception:
             raise ValueError("Error updating DNS records: {0} : {1}".format(type(dnsexception).__name__, str(dnsexception)))
 
-        log.info("Wait {0} then start self challenge checks.".format(config["acmednstiny"].getint("CheckChallengeDelay")))
+        log.info("Waiting for {0} seconds before starting self challenge check.".format(config["acmednstiny"].getint("CheckChallengeDelay")))
         time.sleep(config["acmednstiny"].getint("CheckChallengeDelay"))
         challenge_verified = False
         number_check_fail = 1
         while challenge_verified is False:
             try:
-                log.info('Try {0}: Check ressource with value "{1}" exits on nameservers: {2}'.format(number_check_fail, keydigest64, resolver.nameservers))
+                log.info('Try {0}: Check resource with value "{1}" exits on nameservers: {2}'.format(number_check_fail, keydigest64, resolver.nameservers))
                 challenges = resolver.query(dnsrr_domain, rdtype="TXT")
                 for response in challenges.rrset:
                     log.info(".. Found value {0}".format(response.to_text()))
-- 
GitLab


From f5038808692aff402f58e98096a23183e6b6dff6 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 28 Feb 2018 06:57:09 +0100
Subject: [PATCH 57/93] add back workaround for account deactivation test.

This workaround this issue: when starting this test after the two others, ACME staging server seems to have again in cache some TXT record and return error to get certificate.
As this test just use acme-dns-tiny to register the account key, we just check the registration is working.
---
 tests/test_acme_account_deactivate.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/tests/test_acme_account_deactivate.py b/tests/test_acme_account_deactivate.py
index 230d441..063ea6e 100644
--- a/tests/test_acme_account_deactivate.py
+++ b/tests/test_acme_account_deactivate.py
@@ -11,7 +11,12 @@ class TestACMEAccountDeactivate(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         self.configs = generate_acme_account_deactivate_config()
-        acme_dns_tiny.main([self.configs['config']])
+        try:
+            acme_dns_tiny.main([self.configs['config']])
+        except ValueError as err:
+            if str(err).startswith("Error register"):
+                raise ValueError("Fail test as account has not been registered correctly: {0}".format(err))
+
         super(TestACMEAccountDeactivate, self).setUpClass()
 
     # To clean ACME staging server and close correctly temporary files
-- 
GitLab


From f54379123dcd83d1bfd2205e72d1f6bb243c3894 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 28 Feb 2018 08:11:47 +0100
Subject: [PATCH 58/93] add Accept-Language support

---
 acme_dns_tiny.py |  5 +++--
 example.ini      | 11 +++++++++--
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index e3bdcdf..318cace 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -62,7 +62,8 @@ def get_crt(config, log=LOGGER):
 
     # main code
     webclient = urllib.request.build_opener()
-    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0')]
+    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0'), ('Accept-Language', config["acmednstiny"].get("Language", "en"))]
+
     log.info("Read ACME directory.")
     directory = webclient.open(config["acmednstiny"]["ACMEDirectory"])
     acme_config = json.loads(directory.read().decode("utf8"))
@@ -285,7 +286,7 @@ See example.ini file to configure correctly this script.
 
     config = ConfigParser()
     config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v02.api.letsencrypt.org/directory",
-                                      "CheckChallengeDelay": 2},
+                                      "CheckChallengeDelay": 3},
                       "DNS": {"Port": "53"}})
     config.read(args.configfile)
 
diff --git a/example.ini b/example.ini
index b4ae13d..acb1375 100644
--- a/example.ini
+++ b/example.ini
@@ -3,9 +3,11 @@
 AccountKeyFile = account.key
 # Required readable CSR file
 CSRFile = domain.csr
-# Optional ACME directory url (default: https://acme-staging-v02.api.letsencrypt.org/directory)
+# Optional ACME directory url
+# Default: https://acme-staging-v02.api.letsencrypt.org/directory
 ACMEDirectory = https://acme-staging-v02.api.letsencrypt.org/directory
-# Optional time in seconds to wait between DNS update and challenge check (default: 3)
+# Optional time in seconds to wait between DNS update and challenge check
+# Default: 3
 CheckChallengeDelay = 3
 # Optional To be able to be reached by ACME provider (e.g. to warn about
 # certificate expicration), you can provide some contact informations.
@@ -14,7 +16,12 @@ CheckChallengeDelay = 3
 # URI and can support more of contact.
 # For the mailto URI, the email address part must contains only one address
 # without header fields (see [RFC6068]).
+# Default: none
 Contacts = mailto:mail@example.com;mailto:mail2@example.org
+# Optional to give hint to the ACME server about your prefered language for errors given by their server
+# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language for more informations
+# Default: en
+Language = en
 
 [TSIGKeyring]
 # Required TSIG key name
-- 
GitLab


From 590b8e47784e9615aae87e0804a5fcd671f28b8e Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 28 Feb 2018 19:09:35 +0100
Subject: [PATCH 59/93] Log informations updated

---
 acme_dns_tiny.py | 50 +++++++++++++++++++++++-------------------------
 1 file changed, 24 insertions(+), 26 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 318cace..f7384cf 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -64,7 +64,7 @@ def get_crt(config, log=LOGGER):
     webclient = urllib.request.build_opener()
     webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0'), ('Accept-Language', config["acmednstiny"].get("Language", "en"))]
 
-    log.info("Read ACME directory.")
+    log.info("Fetch informations from the ACME directory.")
     directory = webclient.open(config["acmednstiny"]["ACMEDirectory"])
     acme_config = json.loads(directory.read().decode("utf8"))
     terms_service = acme_config.get("meta", {}).get("termsOfService")
@@ -78,12 +78,12 @@ def get_crt(config, log=LOGGER):
         nameserver = [ipv4_rrset.to_text() for ipv4_rrset in dns.resolver.query(config["DNS"]["Host"], rdtype="A")]
         nameserver = nameserver + [ipv6_rrset.to_text() for ipv6_rrset in dns.resolver.query(config["DNS"]["Host"], rdtype="AAAA")]
     except dns.exception.DNSException as e:
-        log.info("A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if exists or directly the DNS Host configuration.")
+        log.info("A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if one exists or directly the DNS Host configuration.")
     if not nameserver:
         nameserver = [config["DNS"]["Host"]]
     resolver.nameservers = nameserver
 
-    log.info("Parsing account key looking for public key.")
+    log.info("Read account key.")
     accountkey = _openssl("rsa", ["-in", config["acmednstiny"]["AccountKeyFile"], "-noout", "-text"])
     pub_hex, pub_exp = re.search(
         r"modulus:\r?\n\s+00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)",
@@ -103,7 +103,7 @@ def get_crt(config, log=LOGGER):
     thumbprint = _b64(hashlib.sha256(accountkey_json.encode("utf8")).digest())
     jws_nonce = None
 
-    log.info("Parsing CSR looking for domains.")
+    log.info("Read CSR to find domains to validate.")
     csr = _openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-noout", "-text"]).decode("utf8")
     domains = set([])
     common_name = re.search(r"Subject:\s*?CN\s*?=\s*?([^\s,;/]+)", csr)
@@ -115,7 +115,7 @@ def get_crt(config, log=LOGGER):
             if san.startswith("DNS:"):
                 domains.add(san[4:])
 
-    log.info("Registering ACME Account.")
+    log.info("Register ACME Account.")
     account_request = {}
     account_request["termsOfServiceAgreed"] = True
     account_request["contact"] = config["acmednstiny"].get("Contacts", "").split(';')
@@ -131,29 +131,29 @@ def get_crt(config, log=LOGGER):
         account_info["contact"] = account_request["contact"]
     elif code == 200:
         jws_header["kid"] = dict(headers).get("Location")
-        log.info("Already registered! (account: '{0}')".format(jws_header["kid"]))
+        log.debug("  - Account is already registered: '{0}'".format(jws_header["kid"]))
 
         code, result, headers = _send_signed_request(jws_header["kid"], {})
         account_info = json.loads(result.decode("utf8"))
     else:
-        raise ValueError("Error registering: {0} {1}".format(code, result))
+        raise ValueError("Error registering account: {0} {1}".format(code, result))
 
     log.info("Update contact information if needed.")
     if (set(account_request["contact"]) != set(account_info["contact"])):
         code, result, headers = _send_signed_request(jws_header["kid"], account_request)
         if code == 200:
-            log.info("Account updated with latest contact informations.")
+            log.debug("  - Account updated with latest contact informations.")
         else:
-            raise ValueError("Error register update: {0} {1}".format(code, result))
+            raise ValueError("Error registering updates for the account: {0} {1}".format(code, result))
 
     # new order
-    log.info("Certification issuance: ask for a new Order")
+    log.info("Request to the ACME server an order to validate domains.")
     new_order = { "identifiers": [{"type": "dns", "value": domain} for domain in domains]}
     code, result, headers = _send_signed_request(acme_config["newOrder"], new_order)
     order = json.loads(result.decode("utf8"))
     if code == 201:
         order_location = dict(headers).get("Location")
-        log.info("Order received: {0}".format(order_location))
+        log.debug("  - Order received: {0}".format(order_location))
         if order["status"] != "pending":
             raise ValueError("Order status is not pending, we can't use it: {0}".format(order))
     elif (code == 403
@@ -164,16 +164,16 @@ def get_crt(config, log=LOGGER):
 
     # complete each authorization challenge
     for authz in order["authorizations"]:
-        log.info("Completing authz: {0}".format(authz))
+        log.info("Process challenge for authorization: {0}".format(authz))
 
         # get new challenge
         resp = webclient.open(authz)
         authorization = json.loads(resp.read().decode("utf8"))
         if resp.getcode() != 200:
-            raise ValueError("Error requesting challenges: {0} {1}".format(resp.getcode(), authorization))
+            raise ValueError("Error fetching challenges: {0} {1}".format(resp.getcode(), authorization))
         domain = authorization["identifier"]["value"]
 
-        log.info("Create and install DNS TXT challenge resource for: {0}".format(domain))
+        log.info("Install DNS TXT resource for domain: {0}".format(domain))
         challenge = [c for c in authorization["challenges"] if c["type"] == "dns-01"][0]
         token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge["token"])
         keyauthorization = "{0}.{1}".format(token, thumbprint)
@@ -191,13 +191,13 @@ def get_crt(config, log=LOGGER):
         number_check_fail = 1
         while challenge_verified is False:
             try:
-                log.info('Try {0}: Check resource with value "{1}" exits on nameservers: {2}'.format(number_check_fail, keydigest64, resolver.nameservers))
+                log.debug('Self test (try: {0}): Check resource with value "{1}" exits on nameservers: {2}'.format(number_check_fail, keydigest64, resolver.nameservers))
                 challenges = resolver.query(dnsrr_domain, rdtype="TXT")
                 for response in challenges.rrset:
-                    log.info(".. Found value {0}".format(response.to_text()))
+                    log.debug("  - Found value {0}".format(response.to_text()))
                     challenge_verified = challenge_verified or response.to_text() == '"{0}"'.format(keydigest64)
             except dns.exception.DNSException as dnsexception:
-                log.info("Info: retry, because a DNS error occurred while checking challenge: {0} : {1}".format(type(dnsexception).__name__, dnsexception))
+                log.debug("  - Will retry as a DNS error occurred while checking challenge: {0} : {1}".format(type(dnsexception).__name__, dnsexception))
             finally:
                 if challenge_verified is False:
                     if number_check_fail >= 10:
@@ -205,32 +205,30 @@ def get_crt(config, log=LOGGER):
                     number_check_fail = number_check_fail + 1
                     time.sleep(2)
 
-        log.info("Ask ACME server to perform checks.")
+        log.info("Ask ACME server to validate thise challenge.")
         code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
         if code != 200:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
-
-        log.info("Waiting challenge to be verified.")
         try:
             while True:
                 try:
                     resp = webclient.open(challenge["url"])
                     challenge_status = json.loads(resp.read().decode("utf8"))
                 except IOError as e:
-                    raise ValueError("Error checking challenge: {0} {1}".format(
+                    raise ValueError("Error during challenge validation: {0} {1}".format(
                         e.code, json.loads(e.read().decode("utf8"))))
                 if challenge_status["status"] == "pending":
                     time.sleep(2)
                 elif challenge_status["status"] == "valid":
-                    log.info("Domain {0} verified!".format(domain))
+                    log.info("ACME has verified challenge for domain: {0}".format(domain))
                     break
                 else:
-                    raise ValueError("{0} challenge did not pass: {1}".format(
+                    raise ValueError("Challenge for domain {0} did not pass: {1}".format(
                         domain, challenge_status))
         finally:
             _update_dns(dnsrr_set, "delete")
 
-    log.info("Finalizing the order...")
+    log.info("Request to finalize the order (all chalenge have been completed)")
     resp = webclient.open(order_location)
     finalize = json.loads(resp.read().decode("utf8"))
     csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
@@ -269,8 +267,8 @@ def main(argv):
         formatter_class=argparse.RawDescriptionHelpFormatter,
         description="""
 This script automates the process of getting a signed TLS certificate
-chain from Let's Encrypt using the ACME protocol and its DNS verification.
-It will need to have access to your private account key and dns server
+chain from any CA using the ACME protocol and its DNS verification.
+It will need to have access to your private ACME account key and dns server
 so PLEASE READ THROUGH IT!
 It's around 300 lines, so it won't take long.
 
-- 
GitLab


From 9ae199e9f52af9788f51145ba999aadeef9d650f Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 28 Feb 2018 19:10:02 +0100
Subject: [PATCH 60/93] Manage a little better terms of service

---
 acme_dns_tiny.py | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index f7384cf..114f6bc 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -67,7 +67,7 @@ def get_crt(config, log=LOGGER):
     log.info("Fetch informations from the ACME directory.")
     directory = webclient.open(config["acmednstiny"]["ACMEDirectory"])
     acme_config = json.loads(directory.read().decode("utf8"))
-    terms_service = acme_config.get("meta", {}).get("termsOfService")
+    terms_service = acme_config.get("meta", {}).get("termsOfService", "")
 
     log.info("Prepare DNS keyring and resolver.")
     keyring = dns.tsigkeyring.from_text({config["TSIGKeyring"]["KeyName"]: config["TSIGKeyring"]["KeyValue"]})
@@ -114,10 +114,14 @@ def get_crt(config, log=LOGGER):
         for san in subject_alt_names.group(1).split(", "):
             if san.startswith("DNS:"):
                 domains.add(san[4:])
+    if len(domains) == 0:
+        raise ValueError("Didn't find any domain to validate in the provided CSR.")
 
     log.info("Register ACME Account.")
     account_request = {}
-    account_request["termsOfServiceAgreed"] = True
+    if terms_service != "":
+        account_request["termsOfServiceAgreed"] = True
+        log.warning("Terms of service exists and will be automatically agreed, please read them: {0}".format(terms_service))
     account_request["contact"] = config["acmednstiny"].get("Contacts", "").split(';')
     if account_request["contact"] == "":
         del account_request["contact"]
@@ -126,9 +130,8 @@ def get_crt(config, log=LOGGER):
     account_info = {}
     if code == 201:
         jws_header["kid"] = dict(headers).get("Location")
-        log.info("Registered! (account: '{0}')".format(jws_header["kid"]))
-        account_info["termsOfServiceAgreed"] = True
-        account_info["contact"] = account_request["contact"]
+        log.debug("  - Registered a new account: '{0}'".format(jws_header["kid"]))
+        account_info = json.loads(result.decode("utf8"))
     elif code == 200:
         jws_header["kid"] = dict(headers).get("Location")
         log.debug("  - Account is already registered: '{0}'".format(jws_header["kid"]))
-- 
GitLab


From d20e96e58325ed320a78f2839067dbb5f4ce206d Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 28 Feb 2018 22:14:11 +0100
Subject: [PATCH 61/93] update test asserts according to new log messages

---
 tests/test_acme_dns_tiny.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 3aa563b..9e15dd8 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -83,8 +83,8 @@ class TestACMEDNSTiny(unittest.TestCase):
         
         with self.assertLogs(level='INFO') as adnslog:
             acme_dns_tiny.main([self.configs['dnsHostIP']])
-        self.assertIn("INFO:acme_dns_tiny:A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if exists or directly the DNS Host configuration.",
-            adnslog.output)
+        self.assertIn("A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if one exists or directly the DNS Host configuration."
+            , adnslog.output)
         certchain = sys.stdout.getvalue()
         
         sys.stdout.close()
-- 
GitLab


From fac4bee27ddc91a30b99a2735946049dae151cc8 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 28 Feb 2018 22:30:01 +0100
Subject: [PATCH 62/93] fix the last fix

---
 tests/test_acme_dns_tiny.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 9e15dd8..9cf9af6 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -83,8 +83,8 @@ class TestACMEDNSTiny(unittest.TestCase):
         
         with self.assertLogs(level='INFO') as adnslog:
             acme_dns_tiny.main([self.configs['dnsHostIP']])
-        self.assertIn("A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if one exists or directly the DNS Host configuration."
-            , adnslog.output)
+        self.assertIn("INFO:acme_dns_tiny:A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if one exists or directly the DNS Host configuration.",
+            adnslog.output)
         certchain = sys.stdout.getvalue()
         
         sys.stdout.close()
-- 
GitLab


From 631c44d5f80c4d28a36f0c159ac98012f08b644a Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 14 Mar 2018 18:59:26 +0100
Subject: [PATCH 63/93] acme-dns-tiny: add a wait delay just before real check
 server to be sure that ACME DNS entries are updated

---
 acme_dns_tiny.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 114f6bc..1bfd269 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -208,7 +208,8 @@ def get_crt(config, log=LOGGER):
                     number_check_fail = number_check_fail + 1
                     time.sleep(2)
 
-        log.info("Ask ACME server to validate thise challenge.")
+        log.info("Waiting for {0} seconds before asking ACME server to validate challenge.".format(max(5, config["acmednstiny"].getint("CheckChallengeDelay"))))
+        max(5, time.sleep(config["acmednstiny"].getint("CheckChallengeDelay")))
         code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
         if code != 200:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
-- 
GitLab


From dfcb899bea1d1b142311974b42cae6601ebbf7c8 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 14 Mar 2018 22:19:58 +0100
Subject: [PATCH 64/93] Update acme_dns_tiny.py, previous commit has inverted
 max order call.

---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 1bfd269..09e3137 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -209,7 +209,7 @@ def get_crt(config, log=LOGGER):
                     time.sleep(2)
 
         log.info("Waiting for {0} seconds before asking ACME server to validate challenge.".format(max(5, config["acmednstiny"].getint("CheckChallengeDelay"))))
-        max(5, time.sleep(config["acmednstiny"].getint("CheckChallengeDelay")))
+        time.sleep(max(5, config["acmednstiny"].getint("CheckChallengeDelay")))
         code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
         if code != 200:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
-- 
GitLab


From e2ddab12e6a168a26ce060127e6b97bf36c1fab2 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Wed, 14 Mar 2018 22:35:36 +0100
Subject: [PATCH 65/93] Update acme_dns_tiny.py to update the magic waiting
 time so all CI tests pass

---
 acme_dns_tiny.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 09e3137..689838a 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -208,8 +208,8 @@ def get_crt(config, log=LOGGER):
                     number_check_fail = number_check_fail + 1
                     time.sleep(2)
 
-        log.info("Waiting for {0} seconds before asking ACME server to validate challenge.".format(max(5, config["acmednstiny"].getint("CheckChallengeDelay"))))
-        time.sleep(max(5, config["acmednstiny"].getint("CheckChallengeDelay")))
+        log.info("Waiting for {0} seconds before asking ACME server to validate challenge.".format(max(10, config["acmednstiny"].getint("CheckChallengeDelay"))))
+        time.sleep(max(10, config["acmednstiny"].getint("CheckChallengeDelay")))
         code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
         if code != 200:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
-- 
GitLab


From a7db82e848f37478cca74ea6e02aa5af4b39710b Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 18 Mar 2018 10:24:26 +0100
Subject: [PATCH 66/93] acme-dns-tiny: add option "csr" argument to use instead
 of the one in the configuration file

---
 acme_dns_tiny.py | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 689838a..c37a0de 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -269,20 +269,17 @@ def get_crt(config, log=LOGGER):
 def main(argv):
     parser = argparse.ArgumentParser(
         formatter_class=argparse.RawDescriptionHelpFormatter,
-        description="""
-This script automates the process of getting a signed TLS certificate
-chain from any CA using the ACME protocol and its DNS verification.
-It will need to have access to your private ACME account key and dns server
-so PLEASE READ THROUGH IT!
-It's around 300 lines, so it won't take long.
+        description="Tiny ACME client to get TLS certificate by responding to DNS challenges.",
+        epilog="""As the script requires access to your private ACME account key and dns server,
+so PLEASE READ THROUGH IT (it's about 300 lines, so it won't take long) !
 
-===Example Usage===
-python3 acme_dns_tiny.py ./example.ini > chain.crt
-See example.ini file to configure correctly this script.
-===================
-"""
+Example: requests certificate chain and store it in chain.crt
+  python3 acme_dns_tiny.py ./example.ini > chain.crt
+
+See example.ini file to configure correctly this script."""
     )
     parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors")
+    parser.add_argument("--csr", help="specifies CSR file path to use instead of the CSRFile option from the configuration file.")
     parser.add_argument("configfile", help="path to your configuration file")
     args = parser.parse_args(argv)
 
@@ -292,6 +289,9 @@ See example.ini file to configure correctly this script.
                       "DNS": {"Port": "53"}})
     config.read(args.configfile)
 
+    if args.csr :
+        config["acmednstiny"]["csrfile"] = args.csrfile
+
     if (set(["accountkeyfile", "csrfile", "acmedirectory", "checkchallengedelay"]) - set(config.options("acmednstiny"))
         or set(["keyname", "keyvalue", "algorithm"]) - set(config.options("TSIGKeyring"))
         or set(["zone", "host", "port"]) - set(config.options("DNS"))):
-- 
GitLab


From c53c6a33ac8846d6c779c543508a124b2780ccae Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 18 Mar 2018 10:26:01 +0100
Subject: [PATCH 67/93] example.ini: add information about the CSRFile which
 can now be optional

---
 example.ini | 1 +
 1 file changed, 1 insertion(+)

diff --git a/example.ini b/example.ini
index acb1375..c9d37f1 100644
--- a/example.ini
+++ b/example.ini
@@ -2,6 +2,7 @@
 # Required readable ACME account key
 AccountKeyFile = account.key
 # Required readable CSR file
+# Note: if you use the "--csr" optional argument, this setting is not read and can be omitted
 CSRFile = domain.csr
 # Optional ACME directory url
 # Default: https://acme-staging-v02.api.letsencrypt.org/directory
-- 
GitLab


From 3330fc7ebc4581b7e38aca13ef3f713938a5fb99 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 18 Mar 2018 10:27:23 +0100
Subject: [PATCH 68/93] test-acme-dns-tiny: add a test to check the --csr
 argument is well used

---
 tests/config_factory.py     | 11 ++++++++---
 tests/test_acme_dns_tiny.py | 10 ++++++++++
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index 5621cef..bd7ede4 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -90,8 +90,12 @@ def generate_acme_dns_tiny_config():
     config = configparser.ConfigParser()
     config.read(goodCName)
 
+    goodCNameWithoutCSR = NamedTemporaryFile(delete=False)
+    del config["acmednstiny"]["CSRFile"]
+    with open(goodCNameWithoutCSR.name, 'w') as configfile:
+        config.write(configfile)
+
     wildCName = NamedTemporaryFile(delete=False)
-    config["acmednstiny"]["AccountKeyFile"] = account_key
     config["acmednstiny"]["CSRFile"] = wilddomain_csr.name
     with open(wildCName.name, 'w') as configfile:
         config.write(configfile)
@@ -103,13 +107,11 @@ def generate_acme_dns_tiny_config():
     config["DNS"]["Host"] = DNSHOST
 
     goodSAN = NamedTemporaryFile(delete=False)
-    config["acmednstiny"]["AccountKeyFile"] = account_key
     config["acmednstiny"]["CSRFile"] = san_csr.name
     with open(goodSAN.name, 'w') as configfile:
         config.write(configfile)
 
     wildSAN = NamedTemporaryFile(delete=False)
-    config["acmednstiny"]["AccountKeyFile"] = account_key
     config["acmednstiny"]["CSRFile"] = wildsan_csr.name
     with open(wildSAN.name, 'w') as configfile:
         config.write(configfile)
@@ -139,6 +141,7 @@ def generate_acme_dns_tiny_config():
     return {
         # configs
         "goodCName": goodCName,
+        "goodCNameWithoutCSR": goodCNameWithoutCSR,
         "wildCName": wildCName.name,
         "dnsHostIP": dnsHostIP.name,
         "goodSAN": goodSAN.name,
@@ -149,6 +152,8 @@ def generate_acme_dns_tiny_config():
         "missingDNS": missingDNS.name,
         # key (just to simply remove the account from staging server)
         "accountkey": account_key,
+        # CName CSR file to use with goodCNameWithoutCSR
+        "cnameCSR": domain_csr,
     }
 
 # generate two account keys to roll over them
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 9cf9af6..ce0d826 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -128,6 +128,16 @@ class TestACMEDNSTiny(unittest.TestCase):
         
         self.assertCertificateChain(certchain)
 
+    def test_success_cli_with_csr_option(self):
+        """ Successfully issue a certificate via command line interface using CSR option"""
+        certout, err = subprocess.Popen([
+            "python3", "acme_dns_tiny.py", ['--csr', sefl.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']]
+        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+
+        certchain = certout.decode("utf8")
+
+        self.assertCertificateChain(certchain)
+
     def test_weak_key(self):
         """ Let's Encrypt rejects weak keys """
         self.assertRaisesRegex(ValueError,
-- 
GitLab


From 36a6c6a9180014d843d7e15faaea42527a01ad65 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 18 Mar 2018 11:04:02 +0100
Subject: [PATCH 69/93] test-acme-dns-tiny: fix typo and remove correctly
 option from config object

---
 tests/config_factory.py     | 2 +-
 tests/test_acme_dns_tiny.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index bd7ede4..abb4ad6 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -91,7 +91,7 @@ def generate_acme_dns_tiny_config():
     config.read(goodCName)
 
     goodCNameWithoutCSR = NamedTemporaryFile(delete=False)
-    del config["acmednstiny"]["CSRFile"]
+    config.remove_option("acmednstiny", "CSRFile")
     with open(goodCNameWithoutCSR.name, 'w') as configfile:
         config.write(configfile)
 
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index ce0d826..c415689 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -131,7 +131,7 @@ class TestACMEDNSTiny(unittest.TestCase):
     def test_success_cli_with_csr_option(self):
         """ Successfully issue a certificate via command line interface using CSR option"""
         certout, err = subprocess.Popen([
-            "python3", "acme_dns_tiny.py", ['--csr', sefl.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']]
+            "python3", "acme_dns_tiny.py", ['--csr', self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']]
         ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
 
         certchain = certout.decode("utf8")
-- 
GitLab


From ae0f632789aa7780c2f2637b0bbe637ed596f1f0 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 18 Mar 2018 11:20:17 +0100
Subject: [PATCH 70/93] config_factory: didn't give correct new configuration
 file name

---
 tests/config_factory.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/config_factory.py b/tests/config_factory.py
index abb4ad6..d6281af 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -141,7 +141,7 @@ def generate_acme_dns_tiny_config():
     return {
         # configs
         "goodCName": goodCName,
-        "goodCNameWithoutCSR": goodCNameWithoutCSR,
+        "goodCNameWithoutCSR": goodCNameWithoutCSR.name,
         "wildCName": wildCName.name,
         "dnsHostIP": dnsHostIP.name,
         "goodSAN": goodSAN.name,
-- 
GitLab


From a601143b43a43bbcad380a904de860694cb384be Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sun, 18 Mar 2018 15:25:45 +0100
Subject: [PATCH 71/93] test-acme-dns-tiny: fix subprocess open

---
 tests/test_acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index c415689..90054bb 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -131,7 +131,7 @@ class TestACMEDNSTiny(unittest.TestCase):
     def test_success_cli_with_csr_option(self):
         """ Successfully issue a certificate via command line interface using CSR option"""
         certout, err = subprocess.Popen([
-            "python3", "acme_dns_tiny.py", ['--csr', self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']]
+            "python3", "acme_dns_tiny.py", "--csr {0} {1}".format(self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR'])
         ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
 
         certchain = certout.decode("utf8")
-- 
GitLab


From 007a2ec4f6e90bd5aa599b24022c4a187d83c2dd Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 19 Mar 2018 15:22:25 +0100
Subject: [PATCH 72/93] acme-dns-tiny: fix the way to override ConfigParser
 values and add test in main process for the csr option

---
 acme_dns_tiny.py            |  2 +-
 tests/test_acme_dns_tiny.py | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index c37a0de..aed0c30 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -290,7 +290,7 @@ See example.ini file to configure correctly this script."""
     config.read(args.configfile)
 
     if args.csr :
-        config["acmednstiny"]["csrfile"] = args.csrfile
+        config.set("acmednstiny", "csrfile", args.csr)
 
     if (set(["accountkeyfile", "csrfile", "acmedirectory", "checkchallengedelay"]) - set(config.options("acmednstiny"))
         or set(["keyname", "keyvalue", "algorithm"]) - set(config.options("TSIGKeyring"))
diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 90054bb..036d7b2 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -63,6 +63,19 @@ class TestACMEDNSTiny(unittest.TestCase):
         
         self.assertCertificateChain(certchain)
 
+    def test_success_cn_with_csr_option(self):
+        """ Successfully issue a certificate using CSR option outside from the config file"""
+        old_stdout = sys.stdout
+        sys.stdout = StringIO()
+
+        acme_dns_tiny.main(["--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']])
+        certchain = sys.stdout.getvalue()
+
+        sys.stdout.close()
+        sys.stdout = old_stdout
+
+        self.assertCertificateChain(certchain)
+
     def test_success_wild_cn(self):
         """ Successfully issue a certificate via a wildcard common name """
         old_stdout = sys.stdout
-- 
GitLab


From e1f23bea8cd74f8b5ae00678b2a10ac23ab84f0a Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 19 Mar 2018 15:23:40 +0100
Subject: [PATCH 73/93] test-acme-dns-tiny: fix subprocess.Popen argument list

---
 tests/test_acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 036d7b2..4d26a96 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -144,7 +144,7 @@ class TestACMEDNSTiny(unittest.TestCase):
     def test_success_cli_with_csr_option(self):
         """ Successfully issue a certificate via command line interface using CSR option"""
         certout, err = subprocess.Popen([
-            "python3", "acme_dns_tiny.py", "--csr {0} {1}".format(self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR'])
+            "python3", "acme_dns_tiny.py", "--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']
         ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
 
         certchain = certout.decode("utf8")
-- 
GitLab


From 3fc26f61d8cbdae7ccb39418c97f31b7894c988d Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 19 Mar 2018 19:18:32 +0100
Subject: [PATCH 74/93] v2: remove CheckChallengeDelay option and replace it by
 a TTL option in DNS section

The new option TTL is used to configure the TXT resource records installed
by acme-dns-tiny: this allows us to correctly define records according
to the DNS server policy.

It allows too to fix issues about invalid TXT record errors, caused by
the ACME server cache which hidden new TXT records to the ACME server.
---
 acme_dns_tiny.py        | 17 +++++++++--------
 example.ini             | 17 ++++++++++++++---
 gitlab-ci/gitlab-ci.yml |  3 ---
 3 files changed, 23 insertions(+), 14 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index aed0c30..6525e16 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -182,14 +182,14 @@ def get_crt(config, log=LOGGER):
         keyauthorization = "{0}.{1}".format(token, thumbprint)
         keydigest64 = _b64(hashlib.sha256(keyauthorization.encode("utf8")).digest())
         dnsrr_domain = "_acme-challenge.{0}.".format(domain)
-        dnsrr_set = dns.rrset.from_text(dnsrr_domain, 300, "IN", "TXT",  '"{0}"'.format(keydigest64))
+        dnsrr_set = dns.rrset.from_text(dnsrr_domain, config["DNS"]["TTL"], "IN", "TXT",  '"{0}"'.format(keydigest64))
         try:
             _update_dns(dnsrr_set, "add")
         except dns.exception.DNSException as dnsexception:
             raise ValueError("Error updating DNS records: {0} : {1}".format(type(dnsexception).__name__, str(dnsexception)))
 
-        log.info("Waiting for {0} seconds before starting self challenge check.".format(config["acmednstiny"].getint("CheckChallengeDelay")))
-        time.sleep(config["acmednstiny"].getint("CheckChallengeDelay"))
+        log.info("Waiting for 1 TTL ({0} seconds) before starting self challenge check.".format(config["DNS"].getint("TTL")))
+        time.sleep(config["DNS"].getint("TTL"))
         challenge_verified = False
         number_check_fail = 1
         while challenge_verified is False:
@@ -206,10 +206,10 @@ def get_crt(config, log=LOGGER):
                     if number_check_fail >= 10:
                         raise ValueError("Error checking challenge, value not found: {0}".format(keydigest64))
                     number_check_fail = number_check_fail + 1
-                    time.sleep(2)
+                    time.sleep(config["DNS"].getint("TTL"))
 
-        log.info("Waiting for {0} seconds before asking ACME server to validate challenge.".format(max(10, config["acmednstiny"].getint("CheckChallengeDelay"))))
-        time.sleep(max(10, config["acmednstiny"].getint("CheckChallengeDelay")))
+        log.info("Waiting for one TTL ({0} seconds) before asking ACME server to validate challenge.".format(config["DNS"].getint("TTL")))
+        time.sleep(config["DNS"].getint("TTL"))
         code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
         if code != 200:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
@@ -286,7 +286,8 @@ See example.ini file to configure correctly this script."""
     config = ConfigParser()
     config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v02.api.letsencrypt.org/directory",
                                       "CheckChallengeDelay": 3},
-                      "DNS": {"Port": "53"}})
+                      "DNS": {"Port": "53",
+                              "TTL": "10"}})
     config.read(args.configfile)
 
     if args.csr :
@@ -294,7 +295,7 @@ See example.ini file to configure correctly this script."""
 
     if (set(["accountkeyfile", "csrfile", "acmedirectory", "checkchallengedelay"]) - set(config.options("acmednstiny"))
         or set(["keyname", "keyvalue", "algorithm"]) - set(config.options("TSIGKeyring"))
-        or set(["zone", "host", "port"]) - set(config.options("DNS"))):
+        or set(["zone", "host", "port", "ttl"]) - set(config.options("DNS"))):
         raise ValueError("Some required settings are missing.")
 
     LOGGER.setLevel(args.quiet or LOGGER.level)
diff --git a/example.ini b/example.ini
index c9d37f1..264303c 100644
--- a/example.ini
+++ b/example.ini
@@ -1,15 +1,15 @@
 [acmednstiny]
 # Required readable ACME account key
 AccountKeyFile = account.key
+
 # Required readable CSR file
 # Note: if you use the "--csr" optional argument, this setting is not read and can be omitted
 CSRFile = domain.csr
+
 # Optional ACME directory url
 # Default: https://acme-staging-v02.api.letsencrypt.org/directory
 ACMEDirectory = https://acme-staging-v02.api.letsencrypt.org/directory
-# Optional time in seconds to wait between DNS update and challenge check
-# Default: 3
-CheckChallengeDelay = 3
+
 # Optional To be able to be reached by ACME provider (e.g. to warn about
 # certificate expicration), you can provide some contact informations.
 # Contacts setting is a list of contact URI separated by semicolon (;).
@@ -19,6 +19,7 @@ CheckChallengeDelay = 3
 # without header fields (see [RFC6068]).
 # Default: none
 Contacts = mailto:mail@example.com;mailto:mail2@example.org
+
 # Optional to give hint to the ACME server about your prefered language for errors given by their server
 # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language for more informations
 # Default: en
@@ -27,15 +28,25 @@ Language = en
 [TSIGKeyring]
 # Required TSIG key name
 KeyName = host-example
+
 # Required TSIG key value in base64
 KeyValue = XXXXXXXXXXX==
+
 # Required TSIG algorithm
 Algorithm = hmac-sha256
 
 [DNS]
 # Required name of zone to update
 Zone = dnszone
+
 # Required name or IP of DNS server
 Host = dnsserver
+
 # Optional port to connect on DNS server (default: 53)
 Port = 53
+
+# Optional time to live (TTL) value for the added DNS entries
+# If you set a value too high, ACME server could return error about invalid entries while checking the TXT resources
+# So, the default value is low to increase the probability of having a working setup without needing to update it
+# Default: 10 seconds
+TTL = 10
diff --git a/gitlab-ci/gitlab-ci.yml b/gitlab-ci/gitlab-ci.yml
index e51bb8b..bf3385f 100644
--- a/gitlab-ci/gitlab-ci.yml
+++ b/gitlab-ci/gitlab-ci.yml
@@ -1,6 +1,3 @@
-after_script:
-    - sleep 10
-
 jessie:
   image: adt-jessie_dnspython3_1.11
   before_script:
-- 
GitLab


From 230998d8f807a97414e8ebabdfd35b53bff8fc8b Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 19 Mar 2018 20:29:29 +0100
Subject: [PATCH 75/93] v2: finish to remove the CheckChallengeDelay and set to
 correct type default values

---
 acme_dns_tiny.py        | 9 ++++-----
 tests/config_factory.py | 4 ++--
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 6525e16..054ab91 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -284,16 +284,15 @@ See example.ini file to configure correctly this script."""
     args = parser.parse_args(argv)
 
     config = ConfigParser()
-    config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v02.api.letsencrypt.org/directory",
-                                      "CheckChallengeDelay": 3},
-                      "DNS": {"Port": "53",
-                              "TTL": "10"}})
+    config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v02.api.letsencrypt.org/directory"},
+                      "DNS": {"Port": 53,
+                              "TTL": 10}})
     config.read(args.configfile)
 
     if args.csr :
         config.set("acmednstiny", "csrfile", args.csr)
 
-    if (set(["accountkeyfile", "csrfile", "acmedirectory", "checkchallengedelay"]) - set(config.options("acmednstiny"))
+    if (set(["accountkeyfile", "csrfile", "acmedirectory"]) - set(config.options("acmednstiny"))
         or set(["keyname", "keyvalue", "algorithm"]) - set(config.options("TSIGKeyring"))
         or set(["zone", "host", "port", "ttl"]) - set(config.options("DNS"))):
         raise ValueError("Some required settings are missing.")
diff --git a/tests/config_factory.py b/tests/config_factory.py
index d6281af..b97363e 100644
--- a/tests/config_factory.py
+++ b/tests/config_factory.py
@@ -5,11 +5,11 @@ from subprocess import Popen
 # domain with server.py running on it for testing
 DOMAIN = os.getenv("GITLABCI_DOMAIN")
 ACMEDIRECTORY = os.getenv("GITLABCI_ACMEDIRECTORY_V2", "https://acme-staging-v02.api.letsencrypt.org/directory")
-CHALLENGEDELAY = os.getenv("GITLABCI_CHALLENGEDELAY", "3")
 DNSHOST = os.getenv("GITLABCI_DNSHOST")
 DNSHOSTIP = os.getenv("GITLABCI_DNSHOSTIP")
 DNSZONE = os.getenv("GITLABCI_DNSZONE")
 DNSPORT = os.getenv("GITLABCI_DNSPORT", "53")
+DNSTTL = os.getenv("GITLABCI_DNSTTL", "10")
 TSIGKEYNAME = os.getenv("GITLABCI_TSIGKEYNAME")
 TSIGKEYVALUE = os.getenv("GITLABCI_TSIGKEYVALUE")
 TSIGALGORITHM = os.getenv("GITLABCI_TSIGALGORITHM")
@@ -32,7 +32,6 @@ def generate_config():
     parser["acmednstiny"]["AccountKeyFile"] = account_key.name
     parser["acmednstiny"]["CSRFile"] = domain_csr.name
     parser["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY
-    parser["acmednstiny"]["CheckChallengeDelay"] = CHALLENGEDELAY
     parser["acmednstiny"]["Contacts"] = "mailto:mail@example.com"
     parser["TSIGKeyring"]["KeyName"] = TSIGKEYNAME
     parser["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE
@@ -40,6 +39,7 @@ def generate_config():
     parser["DNS"]["Host"] = DNSHOST
     parser["DNS"]["Port"] = DNSPORT
     parser["DNS"]["Zone"] = DNSZONE
+    parser["DNS"]["TTL"] = DNSTTL
 
     config = NamedTemporaryFile(delete=False)
     with open(config.name, 'w') as configfile:
-- 
GitLab


From 1c7a33d7b9dac5adcae598f59018335633af51bc Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 19 Mar 2018 21:15:04 +0100
Subject: [PATCH 76/93] v2: fix TTL type to create TXT resource

---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 054ab91..4060e50 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -182,7 +182,7 @@ def get_crt(config, log=LOGGER):
         keyauthorization = "{0}.{1}".format(token, thumbprint)
         keydigest64 = _b64(hashlib.sha256(keyauthorization.encode("utf8")).digest())
         dnsrr_domain = "_acme-challenge.{0}.".format(domain)
-        dnsrr_set = dns.rrset.from_text(dnsrr_domain, config["DNS"]["TTL"], "IN", "TXT",  '"{0}"'.format(keydigest64))
+        dnsrr_set = dns.rrset.from_text(dnsrr_domain, config["DNS"].getint("TTL"), "IN", "TXT",  '"{0}"'.format(keydigest64))
         try:
             _update_dns(dnsrr_set, "add")
         except dns.exception.DNSException as dnsexception:
-- 
GitLab


From 8803de19e409fdfcb7dc78bf78a51ddadf382ec2 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 19 Mar 2018 21:21:33 +0100
Subject: [PATCH 77/93] v2: fix text style

---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 4060e50..6f12232 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -208,7 +208,7 @@ def get_crt(config, log=LOGGER):
                     number_check_fail = number_check_fail + 1
                     time.sleep(config["DNS"].getint("TTL"))
 
-        log.info("Waiting for one TTL ({0} seconds) before asking ACME server to validate challenge.".format(config["DNS"].getint("TTL")))
+        log.info("Waiting for 1 TTL ({0} seconds) before asking ACME server to validate challenge.".format(config["DNS"].getint("TTL")))
         time.sleep(config["DNS"].getint("TTL"))
         code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
         if code != 200:
-- 
GitLab


From ebb348c23024d0343bde54d88649ae60d6fc462a Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 20 Mar 2018 20:39:55 +0100
Subject: [PATCH 78/93] acme-dns-tiny: only wait for 1 TTL by domain and be
 more accurate for the TTL configuration documentation

---
 acme_dns_tiny.py | 6 ++----
 example.ini      | 7 ++++---
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 6f12232..95decdc 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -195,8 +195,7 @@ def get_crt(config, log=LOGGER):
         while challenge_verified is False:
             try:
                 log.debug('Self test (try: {0}): Check resource with value "{1}" exits on nameservers: {2}'.format(number_check_fail, keydigest64, resolver.nameservers))
-                challenges = resolver.query(dnsrr_domain, rdtype="TXT")
-                for response in challenges.rrset:
+                for response in resolver.query(dnsrr_domain, rdtype="TXT").rrset:
                     log.debug("  - Found value {0}".format(response.to_text()))
                     challenge_verified = challenge_verified or response.to_text() == '"{0}"'.format(keydigest64)
             except dns.exception.DNSException as dnsexception:
@@ -208,8 +207,7 @@ def get_crt(config, log=LOGGER):
                     number_check_fail = number_check_fail + 1
                     time.sleep(config["DNS"].getint("TTL"))
 
-        log.info("Waiting for 1 TTL ({0} seconds) before asking ACME server to validate challenge.".format(config["DNS"].getint("TTL")))
-        time.sleep(config["DNS"].getint("TTL"))
+        log.info("Asking ACME server to validate challenge.")
         code, result, headers = _send_signed_request(challenge["url"], {"keyAuthorization": keyauthorization})
         if code != 200:
             raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
diff --git a/example.ini b/example.ini
index 264303c..02a5cd6 100644
--- a/example.ini
+++ b/example.ini
@@ -45,8 +45,9 @@ Host = dnsserver
 # Optional port to connect on DNS server (default: 53)
 Port = 53
 
-# Optional time to live (TTL) value for the added DNS entries
-# If you set a value too high, ACME server could return error about invalid entries while checking the TXT resources
-# So, the default value is low to increase the probability of having a working setup without needing to update it
+# Optional time to live (TTL) value used to add DNS entries
+# For each domain registered in the CSR, at least 1 TTL is waited before certificate creation.
+# If an error occurs while looking for TXT records, we wait up to 10 TTLs by domain.
+# That's why the default is only of 10 seconds, to avoid having too long time to wait to receive a new certificate.
 # Default: 10 seconds
 TTL = 10
-- 
GitLab


From c664b31e0aeb0dec947e9be4d87336ffab49e6bd Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 20 Mar 2018 20:40:49 +0100
Subject: [PATCH 79/93] acme-dns-tiny: add verbose option and show in standard
 output Account URL

---
 acme_dns_tiny.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 95decdc..0b0d9b7 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -7,7 +7,6 @@ from urllib.error import HTTPError
 
 LOGGER = logging.getLogger('acme_dns_tiny')
 LOGGER.addHandler(logging.StreamHandler())
-LOGGER.setLevel(logging.INFO)
 
 def get_crt(config, log=LOGGER):
     # helper function base64 encode as defined in acme spec
@@ -130,7 +129,7 @@ def get_crt(config, log=LOGGER):
     account_info = {}
     if code == 201:
         jws_header["kid"] = dict(headers).get("Location")
-        log.debug("  - Registered a new account: '{0}'".format(jws_header["kid"]))
+        log.info("  - Registered a new account: '{0}'".format(jws_header["kid"]))
         account_info = json.loads(result.decode("utf8"))
     elif code == 200:
         jws_header["kid"] = dict(headers).get("Location")
@@ -276,7 +275,8 @@ Example: requests certificate chain and store it in chain.crt
 
 See example.ini file to configure correctly this script."""
     )
-    parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors")
+    parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="show only errors on stderr")
+    parser.add_argument("--verbose", action="store_const", const=logging.DEBUG, help="show all debug informations on stderr")
     parser.add_argument("--csr", help="specifies CSR file path to use instead of the CSRFile option from the configuration file.")
     parser.add_argument("configfile", help="path to your configuration file")
     args = parser.parse_args(argv)
@@ -295,7 +295,7 @@ See example.ini file to configure correctly this script."""
         or set(["zone", "host", "port", "ttl"]) - set(config.options("DNS"))):
         raise ValueError("Some required settings are missing.")
 
-    LOGGER.setLevel(args.quiet or LOGGER.level)
+    LOGGER.setLevel(args.verbose or args.quiet or logging.INFO)
     signed_crt = get_crt(config, log=LOGGER)
     sys.stdout.write(signed_crt)
 
-- 
GitLab


From 6d9683958ebcfa72d8f2c2eb3f8fe5313c3d5604 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 20 Mar 2018 20:58:44 +0100
Subject: [PATCH 80/93] v2: acme-dns-tiny: if a CNAME is defined on
 _acme-challenge.example.org following it to decide where to install the TXT
 records

Limitation: it only follows one CNAME for the moment, maybe we should
go through CNAME chain, but it will requires to detect CNAME loop too.
---
 acme_dns_tiny.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 0b0d9b7..a93ce9e 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -181,6 +181,11 @@ def get_crt(config, log=LOGGER):
         keyauthorization = "{0}.{1}".format(token, thumbprint)
         keydigest64 = _b64(hashlib.sha256(keyauthorization.encode("utf8")).digest())
         dnsrr_domain = "_acme-challenge.{0}.".format(domain)
+        try: # a CNAME resource can be used for advanced TSIG configuration, trying to follow it
+            dnsrr_domain = (response.to_text() for response in resolver.query(dnsrr_domain, rdtype="CNAME"))
+            log.info("  - A CNAME resource has been found for this domain, will install TXT on {0}".format(dnsrr_domain))
+        except dns.resolver.NoAnswer as noAnswer:
+            log.debug("  - Not any CNAME resource has been found for this domain, will install TXT directly on {0}".format(dnsrr_domain))
         dnsrr_set = dns.rrset.from_text(dnsrr_domain, config["DNS"].getint("TTL"), "IN", "TXT",  '"{0}"'.format(keydigest64))
         try:
             _update_dns(dnsrr_set, "add")
-- 
GitLab


From c17a40492ffe144ab53d40dbf05f9c32b390a97e Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 20 Mar 2018 21:35:04 +0100
Subject: [PATCH 81/93] v2: test-acme-dns-tiny: enable verbose option

---
 tests/test_acme_dns_tiny.py | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/tests/test_acme_dns_tiny.py b/tests/test_acme_dns_tiny.py
index 4d26a96..6754da0 100644
--- a/tests/test_acme_dns_tiny.py
+++ b/tests/test_acme_dns_tiny.py
@@ -55,7 +55,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         old_stdout = sys.stdout
         sys.stdout = StringIO()
         
-        acme_dns_tiny.main([self.configs['goodCName']])
+        acme_dns_tiny.main([self.configs['goodCName'], "--verbose"])
         certchain = sys.stdout.getvalue()
         
         sys.stdout.close()
@@ -68,7 +68,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         old_stdout = sys.stdout
         sys.stdout = StringIO()
 
-        acme_dns_tiny.main(["--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']])
+        acme_dns_tiny.main(["--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR'], "--verbose"])
         certchain = sys.stdout.getvalue()
 
         sys.stdout.close()
@@ -81,7 +81,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         old_stdout = sys.stdout
         sys.stdout = StringIO()
 
-        acme_dns_tiny.main([self.configs['wildCName']])
+        acme_dns_tiny.main([self.configs['wildCName'], "--verbose"])
         certchain = sys.stdout.getvalue()
 
         sys.stdout.close()
@@ -95,7 +95,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         sys.stdout = StringIO()
         
         with self.assertLogs(level='INFO') as adnslog:
-            acme_dns_tiny.main([self.configs['dnsHostIP']])
+            acme_dns_tiny.main([self.configs['dnsHostIP'], "--verbose"])
         self.assertIn("INFO:acme_dns_tiny:A and/or AAAA DNS resources not found for configured dns host: we will use either resource found if one exists or directly the DNS Host configuration.",
             adnslog.output)
         certchain = sys.stdout.getvalue()
@@ -110,7 +110,7 @@ class TestACMEDNSTiny(unittest.TestCase):
         old_stdout = sys.stdout
         sys.stdout = StringIO()
         
-        acme_dns_tiny.main([self.configs['goodSAN']])
+        acme_dns_tiny.main([self.configs['goodSAN'], "--verbose"])
         certchain = sys.stdout.getvalue()
         
         sys.stdout.close()
@@ -134,7 +134,7 @@ class TestACMEDNSTiny(unittest.TestCase):
     def test_success_cli(self):
         """ Successfully issue a certificate via command line interface """
         certout, err = subprocess.Popen([
-            "python3", "acme_dns_tiny.py", self.configs['goodCName']
+            "python3", "acme_dns_tiny.py", self.configs['goodCName'], "--verbose"
         ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
         
         certchain = certout.decode("utf8")
@@ -144,7 +144,7 @@ class TestACMEDNSTiny(unittest.TestCase):
     def test_success_cli_with_csr_option(self):
         """ Successfully issue a certificate via command line interface using CSR option"""
         certout, err = subprocess.Popen([
-            "python3", "acme_dns_tiny.py", "--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']
+            "python3", "acme_dns_tiny.py", "--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR'], "--verbose"
         ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
 
         certchain = certout.decode("utf8")
@@ -155,25 +155,25 @@ class TestACMEDNSTiny(unittest.TestCase):
         """ Let's Encrypt rejects weak keys """
         self.assertRaisesRegex(ValueError,
                                "key too small",
-                               acme_dns_tiny.main, [self.configs['weakKey']])
+                               acme_dns_tiny.main, [self.configs['weakKey'], "--verbose"])
 
     def test_account_key_domain(self):
         """ Can't use the account key for the CSR """
         self.assertRaisesRegex(ValueError,
                                "certificate public key must be different than account key",
-                               acme_dns_tiny.main, [self.configs['accountAsDomain']])
+                               acme_dns_tiny.main, [self.configs['accountAsDomain'], "--verbose"])
 
     def test_failure_dns_update_tsigkeyname(self):
         """ Fail to update DNS records by invalid TSIG Key name """
         self.assertRaisesRegex(ValueError,
                                "Error updating DNS",
-                               acme_dns_tiny.main, [self.configs['invalidTSIGName']])
+                               acme_dns_tiny.main, [self.configs['invalidTSIGName'], "--verbose"])
 
     def test_failure_notcompleted_configuration(self):
         """ Configuration file have to be completed """
         self.assertRaisesRegex(ValueError,
                                "Some required settings are missing\.",
-                               acme_dns_tiny.main, [self.configs['missingDNS']])
+                               acme_dns_tiny.main, [self.configs['missingDNS'], "--verbose"])
 
 if __name__ == "__main__":
     unittest.main()
-- 
GitLab


From 10e2a34f4c4ea80b47f5b4f8fd6f0b7318d41581 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 20 Mar 2018 21:35:25 +0100
Subject: [PATCH 82/93] v2: acme-dns-tiny: enlarge exception except to
 DNSException as we can receive either NoAnswer or NXDOMAIN

---
 acme_dns_tiny.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index a93ce9e..330b1f1 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -184,8 +184,8 @@ def get_crt(config, log=LOGGER):
         try: # a CNAME resource can be used for advanced TSIG configuration, trying to follow it
             dnsrr_domain = (response.to_text() for response in resolver.query(dnsrr_domain, rdtype="CNAME"))
             log.info("  - A CNAME resource has been found for this domain, will install TXT on {0}".format(dnsrr_domain))
-        except dns.resolver.NoAnswer as noAnswer:
-            log.debug("  - Not any CNAME resource has been found for this domain, will install TXT directly on {0}".format(dnsrr_domain))
+        except dns.exception.DNSException as dnsexception:
+            log.debug("  - Not any CNAME resource has been found for this domain ({1}), will install TXT directly on {0}".format(dnsrr_domain, dnsexception.msg))
         dnsrr_set = dns.rrset.from_text(dnsrr_domain, config["DNS"].getint("TTL"), "IN", "TXT",  '"{0}"'.format(keydigest64))
         try:
             _update_dns(dnsrr_set, "add")
-- 
GitLab


From 43a73a9cadb3fef4a89c6be055db1fa826b1d16e Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 20 Mar 2018 21:40:52 +0100
Subject: [PATCH 83/93] gitlab-ci: add artifacts for stretch

---
 gitlab-ci/gitlab-ci.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/gitlab-ci/gitlab-ci.yml b/gitlab-ci/gitlab-ci.yml
index bf3385f..b5b6cd9 100644
--- a/gitlab-ci/gitlab-ci.yml
+++ b/gitlab-ci/gitlab-ci.yml
@@ -22,3 +22,6 @@ stretch:
     - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_deactivate
     - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_deactivate.py
     - coverage html
+  artifacts:
+    paths:
+     - htmlcov
-- 
GitLab


From f056769b6c7ace2878a310028d6f6e8f39c524ea Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Tue, 20 Mar 2018 21:52:09 +0100
Subject: [PATCH 84/93] v2: show just name of DNSException in case of error)

---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 330b1f1..5ce1e49 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -185,7 +185,7 @@ def get_crt(config, log=LOGGER):
             dnsrr_domain = (response.to_text() for response in resolver.query(dnsrr_domain, rdtype="CNAME"))
             log.info("  - A CNAME resource has been found for this domain, will install TXT on {0}".format(dnsrr_domain))
         except dns.exception.DNSException as dnsexception:
-            log.debug("  - Not any CNAME resource has been found for this domain ({1}), will install TXT directly on {0}".format(dnsrr_domain, dnsexception.msg))
+            log.debug("  - Not any CNAME resource has been found for this domain ({1}), will install TXT directly on {0}".format(dnsrr_domain, type(dnsexception).__name__))
         dnsrr_set = dns.rrset.from_text(dnsrr_domain, config["DNS"].getint("TTL"), "IN", "TXT",  '"{0}"'.format(keydigest64))
         try:
             _update_dns(dnsrr_set, "add")
-- 
GitLab


From bb2e8cef78a48234d22eb4bc1774cce7785903cd Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sat, 24 Mar 2018 15:59:21 +0100
Subject: [PATCH 85/93] v2: fix cname response read

---
 acme_dns_tiny.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 5ce1e49..2ce0eb9 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -181,8 +181,9 @@ def get_crt(config, log=LOGGER):
         keyauthorization = "{0}.{1}".format(token, thumbprint)
         keydigest64 = _b64(hashlib.sha256(keyauthorization.encode("utf8")).digest())
         dnsrr_domain = "_acme-challenge.{0}.".format(domain)
-        try: # a CNAME resource can be used for advanced TSIG configuration, trying to follow it
-            dnsrr_domain = (response.to_text() for response in resolver.query(dnsrr_domain, rdtype="CNAME"))
+        try: # a CNAME resource can be used for advanced TSIG configuration
+            # Note: the CNAME target has to be of "non-CNAME" type to be able to add TXT records aside it
+            dnsrr_domain = [response.to_text() for response in resolver.query(dnsrr_domain, rdtype="CNAME")][0]
             log.info("  - A CNAME resource has been found for this domain, will install TXT on {0}".format(dnsrr_domain))
         except dns.exception.DNSException as dnsexception:
             log.debug("  - Not any CNAME resource has been found for this domain ({1}), will install TXT directly on {0}".format(dnsrr_domain, type(dnsexception).__name__))
-- 
GitLab


From ada9f449f787ab3e330bef1f4b8ca21d63e98386 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Sat, 24 Mar 2018 15:59:41 +0100
Subject: [PATCH 86/93] v2: the self check challenge needs to check from the
 _acme-challenge record

---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 2ce0eb9..8f2bbfe 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -200,7 +200,7 @@ def get_crt(config, log=LOGGER):
         while challenge_verified is False:
             try:
                 log.debug('Self test (try: {0}): Check resource with value "{1}" exits on nameservers: {2}'.format(number_check_fail, keydigest64, resolver.nameservers))
-                for response in resolver.query(dnsrr_domain, rdtype="TXT").rrset:
+                for response in resolver.query("_acme-challenge.{0}.".format(domain), rdtype="TXT").rrset:
                     log.debug("  - Found value {0}".format(response.to_text()))
                     challenge_verified = challenge_verified or response.to_text() == '"{0}"'.format(keydigest64)
             except dns.exception.DNSException as dnsexception:
-- 
GitLab


From 6a3968589a660343ef100e5fa78f3d9f1590ad11 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 2 Apr 2018 18:06:42 +0200
Subject: [PATCH 87/93] v2: Content-Type header is required to be
 application/jose+json, so we use Requests python library to mange all HTTP
 requests simply

---
 acme_dns_tiny.py | 90 +++++++++++++++++++++++++-----------------------
 1 file changed, 47 insertions(+), 43 deletions(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index 8f2bbfe..d9f003c 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -1,9 +1,6 @@
 #!/usr/bin/env python3
-import os, argparse, subprocess, json, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging
+import argparse, subprocess, requests, json, sys, base64, binascii, time, hashlib, re, copy, logging, configparser
 import dns.resolver, dns.tsigkeyring, dns.update
-from configparser import ConfigParser
-import urllib.request
-from urllib.error import HTTPError
 
 LOGGER = logging.getLogger('acme_dns_tiny')
 LOGGER.addHandler(logging.StreamHandler())
@@ -39,7 +36,7 @@ def get_crt(config, log=LOGGER):
         nonlocal jws_nonce
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         protected = copy.deepcopy(jws_header)
-        protected["nonce"] = jws_nonce or webclient.open(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+        protected["nonce"] = jws_nonce or requests.get(acme_config["newNonce"]).headers['Replay-Nonce']
         protected["url"] = url
         if url == acme_config["newAccount"]:
             del protected["kid"]
@@ -48,24 +45,27 @@ def get_crt(config, log=LOGGER):
         protected64 = _b64(json.dumps(protected).encode("utf8"))
         signature = _openssl("dgst", ["-sha256", "-sign", config["acmednstiny"]["AccountKeyFile"]],
                              "{0}.{1}".format(protected64, payload64).encode("utf8"))
-        data = json.dumps({
+        jose = {
             "protected": protected64, "payload": payload64,"signature": _b64(signature)
-        })
+        }
         try:
-            resp = webclient.open(url, data.encode("utf8"))
-        except HTTPError as httperror:
-            resp = httperror
+            resp = requests.post(url, json=jose, headers=joseheaders)
+        except requests.exceptions.RequestException as error:
+            resp = error.response
         finally:
-            jws_nonce = resp.getheader("Replay-Nonce", None)
-            return resp.getcode(), resp.read(), resp.getheaders()
+            jws_nonce = resp.headers['Replay-Nonce']
+            return resp.status_code, resp.json(), resp.headers
 
     # main code
-    webclient = urllib.request.build_opener()
-    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0'), ('Accept-Language', config["acmednstiny"].get("Language", "en"))]
+    adtheaders =  {'User-Agent': 'acme-dns-tiny/2.0',
+        'Accept-Language': config["acmednstiny"].get("Language", "en")
+    }
+    joseheaders=copy.deepcopy(adtheaders)
+    joseheaders['Content-Type']='application/jose+json'
 
     log.info("Fetch informations from the ACME directory.")
-    directory = webclient.open(config["acmednstiny"]["ACMEDirectory"])
-    acme_config = json.loads(directory.read().decode("utf8"))
+    directory = requests.get(config["acmednstiny"]["ACMEDirectory"], headers=adtheaders)
+    acme_config = directory.json()
     terms_service = acme_config.get("meta", {}).get("termsOfService", "")
 
     log.info("Prepare DNS keyring and resolver.")
@@ -128,15 +128,15 @@ def get_crt(config, log=LOGGER):
     code, result, headers = _send_signed_request(acme_config["newAccount"], account_request)
     account_info = {}
     if code == 201:
-        jws_header["kid"] = dict(headers).get("Location")
+        jws_header["kid"] = headers['Location']
         log.info("  - Registered a new account: '{0}'".format(jws_header["kid"]))
-        account_info = json.loads(result.decode("utf8"))
+        account_info = result
     elif code == 200:
-        jws_header["kid"] = dict(headers).get("Location")
+        jws_header["kid"] = headers['Location']
         log.debug("  - Account is already registered: '{0}'".format(jws_header["kid"]))
 
         code, result, headers = _send_signed_request(jws_header["kid"], {})
-        account_info = json.loads(result.decode("utf8"))
+        account_info = result
     else:
         raise ValueError("Error registering account: {0} {1}".format(code, result))
 
@@ -152,15 +152,15 @@ def get_crt(config, log=LOGGER):
     log.info("Request to the ACME server an order to validate domains.")
     new_order = { "identifiers": [{"type": "dns", "value": domain} for domain in domains]}
     code, result, headers = _send_signed_request(acme_config["newOrder"], new_order)
-    order = json.loads(result.decode("utf8"))
+    order = result
     if code == 201:
-        order_location = dict(headers).get("Location")
+        order_location = headers['Location']
         log.debug("  - Order received: {0}".format(order_location))
         if order["status"] != "pending":
             raise ValueError("Order status is not pending, we can't use it: {0}".format(order))
     elif (code == 403
         and order["type"] == "urn:ietf:params:acme:error:userActionRequired"):
-        raise ValueError("Order creation failed ({0}). Read Terms of Service ({1}), then follow your CA instructions: {2}".format(order["detail"], dict(headers)["Link"], order["instance"]))
+        raise ValueError("Order creation failed ({0}). Read Terms of Service ({1}), then follow your CA instructions: {2}".format(order["detail"], headers['Link'], order["instance"]))
     else:
         raise ValueError("Error getting new Order: {0} {1}".format(code, result))
 
@@ -169,10 +169,10 @@ def get_crt(config, log=LOGGER):
         log.info("Process challenge for authorization: {0}".format(authz))
 
         # get new challenge
-        resp = webclient.open(authz)
-        authorization = json.loads(resp.read().decode("utf8"))
-        if resp.getcode() != 200:
-            raise ValueError("Error fetching challenges: {0} {1}".format(resp.getcode(), authorization))
+        resp = requests.get(authz, headers=adtheaders)
+        authorization = resp.json()
+        if resp.status_code != 200:
+            raise ValueError("Error fetching challenges: {0} {1}".format(resp.status_code, authorization))
         domain = authorization["identifier"]["value"]
 
         log.info("Install DNS TXT resource for domain: {0}".format(domain))
@@ -219,11 +219,11 @@ def get_crt(config, log=LOGGER):
         try:
             while True:
                 try:
-                    resp = webclient.open(challenge["url"])
-                    challenge_status = json.loads(resp.read().decode("utf8"))
-                except IOError as e:
+                    resp = requests.get(challenge["url"], headers=adtheaders)
+                    challenge_status = resp.json()
+                except requests.exceptions.RequestException as error:
                     raise ValueError("Error during challenge validation: {0} {1}".format(
-                        e.code, json.loads(e.read().decode("utf8"))))
+                        error.response.status_code, error.response.text()))
                 if challenge_status["status"] == "pending":
                     time.sleep(2)
                 elif challenge_status["status"] == "valid":
@@ -236,8 +236,8 @@ def get_crt(config, log=LOGGER):
             _update_dns(dnsrr_set, "delete")
 
     log.info("Request to finalize the order (all chalenge have been completed)")
-    resp = webclient.open(order_location)
-    finalize = json.loads(resp.read().decode("utf8"))
+    resp = requests.get(order_location, headers=adtheaders)
+    finalize = resp.json()
     csr_der = _b64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"]))
     code, result, headers = _send_signed_request(order["finalize"], {"csr": csr_der})
     if code != 200:
@@ -245,14 +245,18 @@ def get_crt(config, log=LOGGER):
 
     while True:
         try:
-            resp = webclient.open(order_location)
-            finalize = json.loads(resp.read().decode("utf8"))
-        except IOError as e:
+            resp = requests.get(order_location, headers=adtheaders)
+            resp.raise_for_status()
+            finalize = resp.json()
+        except requests.exceptions.RequestException as error:
             raise ValueError("Error finalizing order: {0} {1}".format(
-                e.code, json.loads(e.read().decode("utf8"))))
+                error.response.status_code, error.response.text()))
 
         if finalize["status"] == "processing":
-            time.sleep(resp.getheader("Retry-After", 2))
+            if resp.headers["Retry-After"]:
+                time.sleep(resp.headers["Retry-After"])
+            else:
+                time.sleep(2)
         elif finalize["status"] == "valid":
             log.info("Order finalized!")
             break
@@ -260,11 +264,11 @@ def get_crt(config, log=LOGGER):
             raise ValueError("Finalizing order {0} got errors: {1}".format(
                 domain, finalize))
     
-    resp = webclient.open(finalize["certificate"])
-    if resp.getcode() != 200:
+    resp = requests.get(finalize["certificate"], headers=adtheaders)
+    if resp.status_code != 200:
         raise ValueError("Finalizing order {0} got errors: {1}".format(
-            resp.getcode(), resp.read.decode("utf8")))
-    certchain = resp.read().decode("utf8")
+            resp.status_code, resp.json()))
+    certchain = resp.text
     
     log.info("Certificate signed and chain received: {0}".format(finalize["certificate"]))
     return certchain
@@ -287,7 +291,7 @@ See example.ini file to configure correctly this script."""
     parser.add_argument("configfile", help="path to your configuration file")
     args = parser.parse_args(argv)
 
-    config = ConfigParser()
+    config = configparser.ConfigParser()
     config.read_dict({"acmednstiny": {"ACMEDirectory": "https://acme-staging-v02.api.letsencrypt.org/directory"},
                       "DNS": {"Port": 53,
                               "TTL": 10}})
-- 
GitLab


From 782485dbad23d62adfc00db057157cc2864c9d77 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 2 Apr 2018 18:07:45 +0200
Subject: [PATCH 88/93] v2: acme_account_deactivate updates to Requests library

---
 tools/acme_account_deactivate.py | 65 ++++++++++++++++----------------
 1 file changed, 33 insertions(+), 32 deletions(-)

diff --git a/tools/acme_account_deactivate.py b/tools/acme_account_deactivate.py
index 181051e..9ae665b 100644
--- a/tools/acme_account_deactivate.py
+++ b/tools/acme_account_deactivate.py
@@ -1,11 +1,8 @@
 #!/usr/bin/env python3
-import sys, os, argparse, subprocess, json, base64, binascii, re, copy, logging
-import urllib.request
-from urllib.error import HTTPError
+import sys, argparse, subprocess, json, base64, binascii, re, copy, logging, requests
 
 LOGGER = logging.getLogger("acme_account_deactivate")
 LOGGER.addHandler(logging.StreamHandler())
-LOGGER.setLevel(logging.INFO)
 
 def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
     # helper function base64 encode as defined in acme spec
@@ -21,12 +18,12 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
             raise IOError("OpenSSL Error: {0}".format(err))
         return out
 
-    # helper function make signed requests
+    # helper function to send signed requests
     def _send_signed_request(url, payload):
         nonlocal jws_nonce
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         protected = copy.deepcopy(jws_header)
-        protected["nonce"] = jws_nonce or webclient.open(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+        protected["nonce"] = jws_nonce or requests.get(acme_config["newNonce"]).headers['Replay-Nonce']
         protected["url"] = url
         if url == acme_config["newAccount"]:
             del protected["kid"]
@@ -35,22 +32,28 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
         protected64 = _b64(json.dumps(protected).encode("utf8"))
         signature = _openssl("dgst", ["-sha256", "-sign", accountkeypath],
                              "{0}.{1}".format(protected64, payload64).encode("utf8"))
-        data = json.dumps({
-            "protected": protected64, "payload": payload64,"signature": _b64(signature)
-        })
+        jws = {
+            "protected": protected64, "payload": payload64, "signature": _b64(signature)
+        }
         try:
-            resp = webclient.open(url, data.encode("utf8"))
-        except HTTPError as httperror:
-            resp = httperror
+            resp = requests.post(url, json=jws, headers=joseheaders)
+        except requests.exceptions.RequestException as error:
+            resp = error.response
         finally:
-            jws_nonce = resp.getheader("Replay-Nonce", None)
-            return resp.getcode(), resp.read(), resp.getheaders()
+            jws_nonce = resp.headers['Replay-Nonce']
+            if resp.text != '':
+                return resp.status_code, resp.json(), resp.headers
+            else:
+                return resp.status_code, json.dumps({}), resp.headers
 
-    webclient = urllib.request.build_opener();
-    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0/account_deactivate')]
-    log.info("Reading ACME directory.")
-    directory = webclient.open(acme_directory)
-    acme_config = json.loads(directory.read().decode("utf8"))
+    # main code
+    adtheaders = {'User-Agent': 'acme-dns-tiny/2.0'}
+    joseheaders = copy.deepcopy(adtheaders)
+    joseheaders['Content-Type'] = 'application/jose+json'
+
+    log.info("Fetch informations from the ACME directory.")
+    directory = requests.get(acme_directory, headers=adtheaders)
+    acme_config = directory.json()
 
     log.info("Parsing account key.")
     accountkey = _openssl("rsa", ["-in", accountkeypath, "-noout", "-text"])
@@ -76,7 +79,7 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
 
     code, result, headers = _send_signed_request(acme_config["newAccount"], account_request)
     if code == 200:
-        jws_header["kid"] = dict(headers).get("Location")
+        jws_header["kid"] = headers['Location']
     else:
         raise ValueError("Error looking or account URL: {0} {1}".format(code, result))
 
@@ -91,29 +94,27 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
 def main(argv):
     parser = argparse.ArgumentParser(
         formatter_class=argparse.RawDescriptionHelpFormatter,
-        description="""
-This script permanently *deactivate* your account from an ACME server.
+        description="Tiny ACME client to deactivate ACME account",
+        epilog="""This script permanently *deactivates* an ACME account.
+
 You should revoke your certificates *before* using this script,
 as the server won't accept any further request with this account.
 
-It will need to have access to your private account key, so
-PLEASE READ THROUGH IT!
+It will need to access the ACME private account key, so PLEASE READ THROUGH IT!
 It's around 150 lines, so it won't take long.
 
-=== Example Usage ===
-Remove account.key from staging Let's Encrypt:
-python3 acme_account_deactivate.py --account-key account.key --acme-directory https://acme-staging-v02.api.letsencrypt.org/directory
-"""
+Example: deactivate account.key from staging Let's Encrypt:
+  python3 acme_account_deactivate.py --account-key account.key --acme-directory https://acme-staging-v02.api.letsencrypt.org/directory"""
     )
-    parser.add_argument("--account-key", required = True, help="path to the private account key to deactivate")
-    parser.add_argument("--acme-directory", required = True, help="ACME directory URL of the ACME server where to remove the key")
+    parser.add_argument("--account-key", required=True, help="path to the private account key to deactivate")
+    parser.add_argument("--acme-directory", required=True, help="ACME directory URL of the ACME server where to remove the key")
     parser.add_argument("--quiet", action="store_const",
                         const=logging.ERROR,
                         help="suppress output except for errors")
     args = parser.parse_args(argv)
 
-    LOGGER.setLevel(args.quiet or LOGGER.level)
-    account_deactivate(args.account_key, args.acme_directory)
+    LOGGER.setLevel(args.quiet or logging.INFO)
+    account_deactivate(args.account_key, args.acme_directory, log=LOGGER)
 
 if __name__ == "__main__":  # pragma: no cover
     main(sys.argv[1:])
-- 
GitLab


From 7084e7c2de866eaf27a3358ac0fea692c50adf9f Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 2 Apr 2018 18:08:06 +0200
Subject: [PATCH 89/93] v2: acme_account_deactivate updates to Requests library

---
 tools/acme_account_rollover.py | 54 +++++++++++++++++-----------------
 1 file changed, 27 insertions(+), 27 deletions(-)

diff --git a/tools/acme_account_rollover.py b/tools/acme_account_rollover.py
index b50910a..9af1362 100644
--- a/tools/acme_account_rollover.py
+++ b/tools/acme_account_rollover.py
@@ -1,11 +1,8 @@
 #!/usr/bin/env python3
-import sys, os, argparse, subprocess, json, base64, binascii, hashlib, re, copy, logging
-import urllib.request
-from urllib.error import HTTPError
+import sys, argparse, subprocess, json, base64, binascii, re, copy, logging, requests
 
 LOGGER = logging.getLogger("acme_account_rollover")
 LOGGER.addHandler(logging.StreamHandler())
-LOGGER.setLevel(logging.INFO)
 
 def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOGGER):
     # helper function base64 encode as defined in acme spec
@@ -46,7 +43,7 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
         payload64 = _b64(json.dumps(payload).encode("utf8"))
         if keypath == accountkeypath:
             protected = copy.deepcopy(jws_header)
-            protected["nonce"] = jws_nonce or webclient.open(acme_config["newNonce"]).getheader("Replay-Nonce", None)
+            protected["nonce"] = jws_nonce or requests.get(acme_config["newNonce"]).headers['Replay-Nonce']
         elif keypath == new_accountkeypath:
             protected = copy.deepcopy(new_jws_header)
         if (keypath == new_accountkeypath
@@ -66,20 +63,26 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
     # helper function make signed requests
     def _send_signed_request(url, keypath, payload):
         nonlocal jws_nonce
-        data = json.dumps(_sign_request(url, keypath, payload))
+        jws = _sign_request(url, keypath, payload)
         try:
-            resp = webclient.open(url, data.encode("utf8"))
-        except HTTPError as httperror:
-            resp = httperror
+            resp = requests.post(url, json=jws, headers=joseheaders)
+        except requests.exceptions.RequestException as error:
+            resp = error.response
         finally:
-            jws_nonce = resp.getheader("Replay-Nonce", None)
-            return resp.getcode(), resp.read(), resp.getheaders()
+            jws_nonce = resp.headers['Replay-Nonce']
+            if resp.text != '':
+                return resp.status_code, resp.json(), resp.headers
+            else:
+                return resp.status_code, json.dumps({}), resp.headers
 
-    webclient = urllib.request.build_opener();
-    webclient.addheaders = [('User-Agent', 'acme-dns-tiny/2.0/account_rollover')]
-    log.info("Reading ACME directory.")
-    directory = webclient.open(acme_directory)
-    acme_config = json.loads(directory.read().decode("utf8"))
+    # main code
+    adtheaders =  {'User-Agent': 'acme-dns-tiny/2.0'}
+    joseheaders=copy.deepcopy(adtheaders)
+    joseheaders['Content-Type']='application/jose+json'
+
+    log.info("Fetch informations from the ACME directory.")
+    directory = requests.get(acme_directory, headers=adtheaders)
+    acme_config = directory.json()
 
     log.info("Parsing current account key...")
     jws_header = _jws_header(accountkeypath)
@@ -93,7 +96,7 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
     code, result, headers = _send_signed_request(acme_config["newAccount"], accountkeypath, {
         "onlyReturnExisting": True })
     if code == 200:
-        jws_header["kid"] = dict(headers).get("Location")
+        jws_header["kid"] = headers["Location"]
     else:
         raise ValueError("Error looking or account URL: {0} {1}".format(code, result))
 
@@ -110,25 +113,22 @@ def account_rollover(accountkeypath, new_accountkeypath, acme_directory, log=LOG
 def main(argv):
     parser = argparse.ArgumentParser(
         formatter_class=argparse.RawDescriptionHelpFormatter,
-        description="""
-This script *rolls over* your account key on an ACME server.
+        description="Tiny ACME client to roll over an ACME account key with another one.",
+        epilog="""This script *rolls over* ACME account keys.
 
-It will need to have access to your private account key, so
-PLEASE READ THROUGH IT!
+It will need to have access to the ACME private account keys, so PLEASE READ THROUGH IT!
 It's around 150 lines, so it won't take long.
 
-=== Example Usage ===
-Rollover account.keys from account.key to newaccount.key:
-python3 acme_account_rollover.py --current account.key --new newaccount.key --acme-directory https://acme-staging.api.letsencrypt.org/directory"""
-    )
+Example: roll over account key from account.key to newaccount.key:
+  python3 acme_account_rollover.py --current account.key --new newaccount.key --acme-directory https://acme-staging-v02.api.letsencrypt.org/directory""")
     parser.add_argument("--current", required = True, help="path to the current private account key")
     parser.add_argument("--new", required = True, help="path to the newer private account key to register")
     parser.add_argument("--acme-directory", required = True, help="ACME directory URL of the ACME server where to remove the key")
     parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors")
     args = parser.parse_args(argv)
 
-    LOGGER.setLevel(args.quiet or LOGGER.level)
-    account_rollover(args.current, args.new, args.acme_directory)
+    LOGGER.setLevel(args.quiet or logging.INFO)
+    account_rollover(args.current, args.new, args.acme_directory, log=LOGGER)
 
 if __name__ == "__main__":  # pragma: no cover
     main(sys.argv[1:])
-- 
GitLab


From 7261d312733f2d17777b0382bb1f691db164f560 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 2 Apr 2018 18:25:02 +0200
Subject: [PATCH 90/93] v2: fix empty response parsing

---
 acme_dns_tiny.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index d9f003c..e125eaa 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -54,7 +54,10 @@ def get_crt(config, log=LOGGER):
             resp = error.response
         finally:
             jws_nonce = resp.headers['Replay-Nonce']
-            return resp.status_code, resp.json(), resp.headers
+            if resp.text != '':
+                return resp.status_code, resp.json(), resp.headers
+            else:
+                return resp.status_code, json.dumps({}), resp.headers
 
     # main code
     adtheaders =  {'User-Agent': 'acme-dns-tiny/2.0',
-- 
GitLab


From a4757d8cca2ab896ae40d4a66a9a8a9b8eb1930c Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 2 Apr 2018 18:25:28 +0200
Subject: [PATCH 91/93] requirements: add Requests library

---
 tests/requirements.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tests/requirements.txt b/tests/requirements.txt
index d37faed..59bc5db 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,3 +1,4 @@
 coverage
 argparse
-configparser
\ No newline at end of file
+configparser
+requests
-- 
GitLab


From a819f3c71fefd013ffffe25ad420433ac8a1d8db Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 2 Apr 2018 18:26:33 +0200
Subject: [PATCH 92/93] v2: gitlab-ci add pipe to stretch tests

---
 gitlab-ci/gitlab-ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/gitlab-ci/gitlab-ci.yml b/gitlab-ci/gitlab-ci.yml
index b5b6cd9..ce12e53 100644
--- a/gitlab-ci/gitlab-ci.yml
+++ b/gitlab-ci/gitlab-ci.yml
@@ -18,6 +18,8 @@ jessie_backport:
 
 stretch:
   image: adt-stretch_dnspython3_1.15
+  before_script:
+    - pip3 install --upgrade -r tests/requirements.txt
   script:
     - coverage run --source ./ -m unittest -v tests.test_acme_dns_tiny tests.test_acme_account_rollover tests.test_acme_account_deactivate
     - coverage report --include=acme_dns_tiny.py,tools/acme_account_rollover.py,tools/acme_account_deactivate.py
-- 
GitLab


From d952255e9b45d3624a95638f9e76efc569b2aa15 Mon Sep 17 00:00:00 2001
From: Adrien Dorsaz <adrien@adorsaz.ch>
Date: Mon, 9 Apr 2018 22:58:33 +0200
Subject: [PATCH 93/93] v2: dnspython is a stub resolver, so self-check TXT
 record query needs to happen on a non-CNAME record

This reverts ada9f449f787ab3e330bef1f4b8ca21d63e98386 as suggested and
explained on Github PR #1
---
 acme_dns_tiny.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py
index e125eaa..89b6682 100644
--- a/acme_dns_tiny.py
+++ b/acme_dns_tiny.py
@@ -203,7 +203,7 @@ def get_crt(config, log=LOGGER):
         while challenge_verified is False:
             try:
                 log.debug('Self test (try: {0}): Check resource with value "{1}" exits on nameservers: {2}'.format(number_check_fail, keydigest64, resolver.nameservers))
-                for response in resolver.query("_acme-challenge.{0}.".format(domain), rdtype="TXT").rrset:
+                for response in resolver.query(dnsrr_domain, rdtype="TXT").rrset:
                     log.debug("  - Found value {0}".format(response.to_text()))
                     challenge_verified = challenge_verified or response.to_text() == '"{0}"'.format(keydigest64)
             except dns.exception.DNSException as dnsexception:
-- 
GitLab