diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 683866d2d09ba0d1a6c2beb236e8705e3b533c54..e305244bcb7b6ee8b40ee642be4365fd4bfd1958 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,20 +64,20 @@ compile: lint: extends: .check script: - - pylint3 acme_dns_tiny.py - - pylint3 tools/acme_account_deactivate.py - - pylint3 tools/acme_account_rollover.py - - pylint3 tests/config_factory.py - - pylint3 tests/staging_test_acme_dns_tiny.py - - pylint3 tests/unit_test_acme_dns_tiny.py - - pylint3 tests/staging_test_acme_account_deactivate.py - - pylint3 tests/staging_test_acme_account_rollover.py + - pylint3 --max-line-length=99 acme_dns_tiny.py + - pylint3 --max-line-length=99 tools/acme_account_deactivate.py + - pylint3 --max-line-length=99 tools/acme_account_rollover.py + - pylint3 --max-line-length=99 tests/config_factory.py + - pylint3 --max-line-length=99 tests/staging_test_acme_dns_tiny.py + - pylint3 --max-line-length=99 tests/unit_test_acme_dns_tiny.py + - pylint3 --max-line-length=99 tests/staging_test_acme_account_deactivate.py + - pylint3 --max-line-length=99 tests/staging_test_acme_account_rollover.py pep8: extends: .check script: - - pycodestyle --max-line-length=100 --ignore=E401,W503 --exclude=tests . - - pycodestyle --max-line-length=100 --ignore=E722 tests + - pycodestyle --max-line-length=99 --ignore=E401,W503 --exclude=tests . + - pycodestyle --max-line-length=99 --ignore=E722 tests jessie-ut: extends: .unit_test diff --git a/README.md b/README.md index f23a1afba1ccd51e91ec77d3d2bab8c9439d825a..a135d38ba700a2ab01b678c4dd7aaf2ec155fd27 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ validation. Since it has to have access to your private ACME account key and the rights to update the DNS records of your DNS server, this code has been designed -to be as tiny as possible (currently less than 300 lines). +to be as tiny as possible (currently less than 400 lines). The only prerequisites are Python 3, OpenSSL and the dnspython module. @@ -20,7 +20,7 @@ code) or any release of dnspython module (pyhton2 and python3 merged code) since 1.15.0. **PLEASE READ THE SOURCE CODE! YOU MUST TRUST IT! -IT HANDLES YOUR ACCOUNT PRIVATE KEYS!** +IT HANDLES YOUR ACCOUNT PRIVATE KEY AND UPDATE SOME OF YOUR DNS RESOURCES !** Note: this script is a fork of the [acme-tiny project](https://github.com/diafygi/acme-tiny) which uses ACME HTTP verification to create signed certificates. diff --git a/acme_dns_tiny.py b/acme_dns_tiny.py index f36e378baac4b1c4656a091d3557fa67a568a587..094dc8d24d7bde7a53d131a6d12b7c17f407efd5 100644 --- a/acme_dns_tiny.py +++ b/acme_dns_tiny.py @@ -90,8 +90,9 @@ def get_crt(config, log=LOGGER): 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) + subject_alt_names = re.search( + r"X509v3 Subject Alternative Name: (?:critical)?\s+([^\r\n]+)\r?\n", + csr, re.MULTILINE) if subject_alt_names is not None: for san in subject_alt_names.group(1).split(", "): if san.startswith("DNS:"): @@ -122,8 +123,8 @@ def get_crt(config, log=LOGGER): 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]+)", - accountkey.decode("utf8"), re.MULTILINE | re.DOTALL).groups() + r"modulus:\s+?00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)", + accountkey.decode("utf8"), re.MULTILINE).groups() pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp # That signature is used to authenticate with the ACME server, it needs to be safely kept @@ -168,7 +169,8 @@ def get_crt(config, log=LOGGER): log.info("Update contact information if needed.") if ("contact" in account_request and set(account_request["contact"]) != set(account_info["contact"])): - http_response, result = _send_signed_request(private_acme_signature["kid"], account_request) + http_response, result = _send_signed_request(private_acme_signature["kid"], + account_request) if http_response.status_code == 200: log.debug(" - Account updated with latest contact informations.") else: @@ -189,7 +191,8 @@ def get_crt(config, log=LOGGER): 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"], http_response.headers['Link'], order["instance"])) + .format(order["detail"], + http_response.headers['Link'], order["instance"])) else: raise ValueError("Error getting new Order: {0} {1}" .format(http_response.status_code, order)) @@ -239,7 +242,8 @@ def get_crt(config, log=LOGGER): while challenge_verified is False: try: log.debug(('Self test (try: %s): Check resource with value "%s" exits on ' - 'nameservers: %s'), number_check_fail, keydigest64, resolver.nameservers) + 'nameservers: %s'), number_check_fail, keydigest64, + resolver.nameservers) for response in resolver.query(dnsrr_domain, rdtype="TXT").rrset: log.debug(" - Found value %s", response.to_text()) challenge_verified = (challenge_verified @@ -280,7 +284,8 @@ def get_crt(config, log=LOGGER): _update_dns(dnsrr_set, "delete") log.info("Request to finalize the order (all chalenge have been completed)") - csr_der = _base64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], "-outform", "DER"])) + csr_der = _base64(_openssl("req", ["-in", config["acmednstiny"]["CSRFile"], + "-outform", "DER"])) http_response, result = _send_signed_request(order["finalize"], {"csr": csr_der}) if http_response.status_code != 200: raise ValueError("Error while sending the CSR: {0} {1}" diff --git a/tests/config_factory.py b/tests/config_factory.py index 160e6db112b68615a362e1ac266f7fa79d6894da..82a7ce46552c241f70541f12401988bdcf16f605 100644 --- a/tests/config_factory.py +++ b/tests/config_factory.py @@ -141,7 +141,8 @@ def generate_acme_dns_tiny_config(): # pylint: disable=too-many-locals,too-many wild_san_conf = NamedTemporaryFile(delete=False) with open("/etc/ssl/openssl.cnf", 'r') as opensslcnf: wild_san_conf.write(opensslcnf.read().encode("utf8")) - wild_san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0},DNS:*.{0}\n".format(DOMAIN).encode("utf8")) + wild_san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0},DNS:*.{0}\n" + .format(DOMAIN).encode("utf8")) wild_san_conf.seek(0) Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key, "-subj", "/", "-reqexts", "SAN", "-config", wild_san_conf.name, diff --git a/tests/staging_test_acme_dns_tiny.py b/tests/staging_test_acme_dns_tiny.py index 346619c14b44a470165a44b51ba83b388d779c65..c2f1e006653a089c3e2c45f34d9e865f52a46ec4 100644 --- a/tests/staging_test_acme_dns_tiny.py +++ b/tests/staging_test_acme_dns_tiny.py @@ -206,13 +206,15 @@ class TestACMEDNSTiny(unittest.TestCase): """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['account_as_domain'], "--verbose"]) + acme_dns_tiny.main, [self.configs['account_as_domain'], + "--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['invalid_tsig_name'], "--verbose"]) + acme_dns_tiny.main, [self.configs['invalid_tsig_name'], + "--verbose"]) if __name__ == "__main__": # pragma: no cover diff --git a/tools/acme_account_deactivate.py b/tools/acme_account_deactivate.py index eb687fecb6b84aa49ee2de7aec19d8e01f64ad25..9acd632735d6e56c2d60dfeb17cf996fd4090ef8 100644 --- a/tools/acme_account_deactivate.py +++ b/tools/acme_account_deactivate.py @@ -79,8 +79,8 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER): log.info("Get private signature from 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]+)", - accountkey.decode("utf8"), re.MULTILINE | re.DOTALL).groups() + r"modulus:\s+?00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)", + accountkey.decode("utf8"), re.MULTILINE).groups() pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp # That signature is used to authenticate with the ACME server, it needs to be safely kept diff --git a/tools/acme_account_rollover.py b/tools/acme_account_rollover.py index 9c1cd9d8b3735ccfece9bbf6a3450d5bb032620b..022be29fdf753262d5de6afa92098d2bca5ad824 100644 --- a/tools/acme_account_rollover.py +++ b/tools/acme_account_rollover.py @@ -37,8 +37,8 @@ def account_rollover(old_accountkeypath, new_accountkeypath, acme_directory, log """Read the account key to get the signature to authenticate with the ACME server.""" 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]+)", - accountkey.decode("utf8"), re.MULTILINE | re.DOTALL).groups() + r"modulus:\s+?00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)", + accountkey.decode("utf8"), re.MULTILINE).groups() pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp return {