Commit 474f4b63 authored by Adrien Dorsaz's avatar Adrien Dorsaz

Merge branch 'fix-regexp' into 'master'

Accept critical SAN extension and improve a bit regexp

Closes #9 et #10

See merge request !22
parents e84912fd 5df02405
Pipeline #274 passed with stages
in 23 minutes and 5 seconds
...@@ -64,20 +64,20 @@ compile: ...@@ -64,20 +64,20 @@ compile:
lint: lint:
extends: .check extends: .check
script: script:
- pylint3 acme_dns_tiny.py - pylint3 --max-line-length=99 acme_dns_tiny.py
- pylint3 tools/acme_account_deactivate.py - pylint3 --max-line-length=99 tools/acme_account_deactivate.py
- pylint3 tools/acme_account_rollover.py - pylint3 --max-line-length=99 tools/acme_account_rollover.py
- pylint3 tests/config_factory.py - pylint3 --max-line-length=99 tests/config_factory.py
- pylint3 tests/staging_test_acme_dns_tiny.py - pylint3 --max-line-length=99 tests/staging_test_acme_dns_tiny.py
- pylint3 tests/unit_test_acme_dns_tiny.py - pylint3 --max-line-length=99 tests/unit_test_acme_dns_tiny.py
- pylint3 tests/staging_test_acme_account_deactivate.py - pylint3 --max-line-length=99 tests/staging_test_acme_account_deactivate.py
- pylint3 tests/staging_test_acme_account_rollover.py - pylint3 --max-line-length=99 tests/staging_test_acme_account_rollover.py
pep8: pep8:
extends: .check extends: .check
script: script:
- pycodestyle --max-line-length=100 --ignore=E401,W503 --exclude=tests . - pycodestyle --max-line-length=99 --ignore=E401,W503 --exclude=tests .
- pycodestyle --max-line-length=100 --ignore=E722 tests - pycodestyle --max-line-length=99 --ignore=E722 tests
jessie-ut: jessie-ut:
extends: .unit_test extends: .unit_test
......
...@@ -9,7 +9,7 @@ validation. ...@@ -9,7 +9,7 @@ validation.
Since it has to have access to your private ACME account key and the 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 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. 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 ...@@ -20,7 +20,7 @@ code) or any release of dnspython module (pyhton2 and python3 merged code) since
1.15.0. 1.15.0.
**PLEASE READ THE SOURCE CODE! YOU MUST TRUST IT! **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) 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. which uses ACME HTTP verification to create signed certificates.
......
...@@ -90,8 +90,9 @@ def get_crt(config, log=LOGGER): ...@@ -90,8 +90,9 @@ def get_crt(config, log=LOGGER):
common_name = re.search(r"Subject:.*?\s+?CN\s*?=\s*?([^\s,;/]+)", csr) common_name = re.search(r"Subject:.*?\s+?CN\s*?=\s*?([^\s,;/]+)", csr)
if common_name is not None: if common_name is not None:
domains.add(common_name.group(1)) domains.add(common_name.group(1))
subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \r?\n +([^\r\n]+)\r?\n", csr, subject_alt_names = re.search(
re.MULTILINE | re.DOTALL) r"X509v3 Subject Alternative Name: (?:critical)?\s+([^\r\n]+)\r?\n",
csr, re.MULTILINE)
if subject_alt_names is not None: if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "): for san in subject_alt_names.group(1).split(", "):
if san.startswith("DNS:"): if san.startswith("DNS:"):
...@@ -122,8 +123,8 @@ def get_crt(config, log=LOGGER): ...@@ -122,8 +123,8 @@ def get_crt(config, log=LOGGER):
accountkey = _openssl("rsa", ["-in", config["acmednstiny"]["AccountKeyFile"], accountkey = _openssl("rsa", ["-in", config["acmednstiny"]["AccountKeyFile"],
"-noout", "-text"]) "-noout", "-text"])
pub_hex, pub_exp = re.search( pub_hex, pub_exp = re.search(
r"modulus:\r?\n\s+00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)", r"modulus:\s+?00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)",
accountkey.decode("utf8"), re.MULTILINE | re.DOTALL).groups() accountkey.decode("utf8"), re.MULTILINE).groups()
pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else 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 # 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): ...@@ -168,7 +169,8 @@ def get_crt(config, log=LOGGER):
log.info("Update contact information if needed.") log.info("Update contact information if needed.")
if ("contact" in account_request if ("contact" in account_request
and set(account_request["contact"]) != set(account_info["contact"])): 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: if http_response.status_code == 200:
log.debug(" - Account updated with latest contact informations.") log.debug(" - Account updated with latest contact informations.")
else: else:
...@@ -189,7 +191,8 @@ def get_crt(config, log=LOGGER): ...@@ -189,7 +191,8 @@ def get_crt(config, log=LOGGER):
and order["type"] == "urn:ietf:params:acme:error:userActionRequired"): and order["type"] == "urn:ietf:params:acme:error:userActionRequired"):
raise ValueError(("Order creation failed ({0}). Read Terms of Service ({1}), then follow " raise ValueError(("Order creation failed ({0}). Read Terms of Service ({1}), then follow "
"your CA instructions: {2}") "your CA instructions: {2}")
.format(order["detail"], http_response.headers['Link'], order["instance"])) .format(order["detail"],
http_response.headers['Link'], order["instance"]))
else: else:
raise ValueError("Error getting new Order: {0} {1}" raise ValueError("Error getting new Order: {0} {1}"
.format(http_response.status_code, order)) .format(http_response.status_code, order))
...@@ -239,7 +242,8 @@ def get_crt(config, log=LOGGER): ...@@ -239,7 +242,8 @@ def get_crt(config, log=LOGGER):
while challenge_verified is False: while challenge_verified is False:
try: try:
log.debug(('Self test (try: %s): Check resource with value "%s" exits on ' 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: for response in resolver.query(dnsrr_domain, rdtype="TXT").rrset:
log.debug(" - Found value %s", response.to_text()) log.debug(" - Found value %s", response.to_text())
challenge_verified = (challenge_verified challenge_verified = (challenge_verified
...@@ -280,7 +284,8 @@ def get_crt(config, log=LOGGER): ...@@ -280,7 +284,8 @@ def get_crt(config, log=LOGGER):
_update_dns(dnsrr_set, "delete") _update_dns(dnsrr_set, "delete")
log.info("Request to finalize the order (all chalenge have been completed)") 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}) http_response, result = _send_signed_request(order["finalize"], {"csr": csr_der})
if http_response.status_code != 200: if http_response.status_code != 200:
raise ValueError("Error while sending the CSR: {0} {1}" raise ValueError("Error while sending the CSR: {0} {1}"
......
...@@ -141,7 +141,8 @@ def generate_acme_dns_tiny_config(): # pylint: disable=too-many-locals,too-many ...@@ -141,7 +141,8 @@ def generate_acme_dns_tiny_config(): # pylint: disable=too-many-locals,too-many
wild_san_conf = NamedTemporaryFile(delete=False) wild_san_conf = NamedTemporaryFile(delete=False)
with open("/etc/ssl/openssl.cnf", 'r') as opensslcnf: with open("/etc/ssl/openssl.cnf", 'r') as opensslcnf:
wild_san_conf.write(opensslcnf.read().encode("utf8")) 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) wild_san_conf.seek(0)
Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key, Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key,
"-subj", "/", "-reqexts", "SAN", "-config", wild_san_conf.name, "-subj", "/", "-reqexts", "SAN", "-config", wild_san_conf.name,
......
...@@ -206,13 +206,15 @@ class TestACMEDNSTiny(unittest.TestCase): ...@@ -206,13 +206,15 @@ class TestACMEDNSTiny(unittest.TestCase):
"""Can't use the account key for the CSR.""" """Can't use the account key for the CSR."""
self.assertRaisesRegex(ValueError, self.assertRaisesRegex(ValueError,
"certificate public key must be different than account key", "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): def test_failure_dns_update_tsigkeyname(self):
"""Fail to update DNS records by invalid TSIG Key name.""" """Fail to update DNS records by invalid TSIG Key name."""
self.assertRaisesRegex(ValueError, self.assertRaisesRegex(ValueError,
"Error updating DNS", "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 if __name__ == "__main__": # pragma: no cover
......
...@@ -79,8 +79,8 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER): ...@@ -79,8 +79,8 @@ def account_deactivate(accountkeypath, acme_directory, log=LOGGER):
log.info("Get private signature from account key.") log.info("Get private signature from account key.")
accountkey = _openssl("rsa", ["-in", accountkeypath, "-noout", "-text"]) accountkey = _openssl("rsa", ["-in", accountkeypath, "-noout", "-text"])
pub_hex, pub_exp = re.search( pub_hex, pub_exp = re.search(
r"modulus:\r?\n\s+00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)", r"modulus:\s+?00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)",
accountkey.decode("utf8"), re.MULTILINE | re.DOTALL).groups() accountkey.decode("utf8"), re.MULTILINE).groups()
pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else 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 # That signature is used to authenticate with the ACME server, it needs to be safely kept
......
...@@ -37,8 +37,8 @@ def account_rollover(old_accountkeypath, new_accountkeypath, acme_directory, log ...@@ -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.""" """Read the account key to get the signature to authenticate with the ACME server."""
accountkey = _openssl("rsa", ["-in", accountkeypath, "-noout", "-text"]) accountkey = _openssl("rsa", ["-in", accountkeypath, "-noout", "-text"])
pub_hex, pub_exp = re.search( pub_hex, pub_exp = re.search(
r"modulus:\r?\n\s+00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)", r"modulus:\s+?00:([a-f0-9\:\s]+?)\r?\npublicExponent: ([0-9]+)",
accountkey.decode("utf8"), re.MULTILINE | re.DOTALL).groups() accountkey.decode("utf8"), re.MULTILINE).groups()
pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
return { return {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment