Commit 4f2dfcb7 authored by Adrien Dorsaz's avatar Adrien Dorsaz

Merge branch 'wip/coverage_90' into 'master'

Clean tests and cover more code

Clean up tests code
Add some tests to cover more code

See merge request !4
parents b0e9585f 2706dea7
Pipeline #51 failed with stage
in 10 minutes and 23 seconds
......@@ -4,7 +4,7 @@ import dns.resolver, dns.tsigkeyring, dns.update
from configparser import ConfigParser
from urllib.request import urlopen
LOGGER = logging.getLogger(__name__)
LOGGER = logging.getLogger('acme_dns_tiny_logger')
LOGGER.addHandler(logging.StreamHandler())
LOGGER.setLevel(logging.INFO)
......@@ -53,6 +53,7 @@ def get_crt(config, log=LOGGER):
return getattr(e, "code", None), getattr(e, "read", e.__str__)(), None
# create DNS keyring and resolver
log.info("Prepare DNS tools...")
keyring = dns.tsigkeyring.from_text({config["TSIGKeyring"]["KeyName"]: config["TSIGKeyring"]["KeyValue"]})
nameserver = []
try:
......@@ -70,7 +71,6 @@ def get_crt(config, log=LOGGER):
resolver = dns.resolver.Resolver(configure=False)
resolver.nameservers = nameserver
resolver.retry_servfail = True
log.info("DNS checks will use servers: {0}".format(resolver.nameservers))
# parse account key to get public key
log.info("Parsing account key...")
......@@ -130,7 +130,7 @@ def get_crt(config, log=LOGGER):
raise ValueError("Error requesting challenges: {0} {1}".format(code, result))
# make and install DNS resource record
log.info("Create DNS RR")
log.info("Create DNS RR...")
challenge = [c for c in json.loads(result.decode("utf8"))["challenges"] if c["type"] == "dns-01"][0]
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge["token"])
keyauthorization = "{0}.{1}".format(token, thumbprint)
......@@ -144,15 +144,15 @@ def get_crt(config, log=LOGGER):
# notify challenge are met
time.sleep(config["acmednstiny"].getint("CheckChallengeDelay"))
log.info("Self challenge check")
log.info("Self challenge check...")
challenge_verified = False
number_check_fail = 0
while challenge_verified is False:
try:
log.info("check retry {0}, with nameservers: {1}".format(number_check_fail, resolver.nameservers))
log.info('Try {0}: Check ressource with value "{1}" exits on nameservers: {2}'.format(number_check_fail+1, keydigest64, resolver.nameservers))
challenges = resolver.query(dnsrr_domain, rdtype="TXT")
for response in challenges.rrset:
log.info("looking for {0}, found {1}, equals ? {2}".format(keydigest64, response.to_text(), response.to_text() == '"{0}"'.format(keydigest64)))
log.info(".. 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))
......@@ -163,7 +163,7 @@ def get_crt(config, log=LOGGER):
if challenge_verified is False:
number_check_fail = number_check_fail + 1
time.sleep(2)
log.info("Ask ACME server to perform check...")
log.info("Ask CA server to perform check...")
code, result, headers = _send_signed_request(challenge["uri"], {
"resource": "challenge",
"keyAuthorization": keyauthorization,
......@@ -224,13 +224,13 @@ def main(argv):
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent("""\
This script automates the process of getting a signed TLS certificate
chain from Let's Encrypt using the ACME protocol and its dns verification.
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
so PLEASE READ THROUGH IT!
It's only ~250 lines, so it won't take long.
===Example Usage===
python acme_dns_tiny.py ./example.ini > chain.crt
python3 acme_dns_tiny.py ./example.ini > chain.crt
See example.ini file to configure correctly this script.
===================
""")
......
......@@ -7,14 +7,27 @@ explains how to setup and test acme-tiny yourself.
## Setup instructions
1. Setup environment variables:
* Read top of monkey.py, all environnement variables used are defined there (top of file).
* Read top of config_maker.py, all environement variables used are defined there (top of file).
* These variables corresponds to the configuration file you have to do when using in production.
* If you don't own the gitlab project, you can set them on your build/test machine:
`export GITLABCI_DOMAIN=travis-ci.gethttpsforfree.com`
* Otherwise, you have to use your gitlab project to define environment variables for gitlab runners.
2. Install the test requirements on your build/test machine (automated by .gitlab-ci.yml for gitlab runners).
* `cd /path/to/acme-dns-tiny`
* `pip install --user -r tests/requirements.txt`
* `pip3 install --user -r tests/requirements.txt`
5. Run the test suit on your local.
* `cd /path/to/acme-dns-tiny`
* `coverage run --source ./ -m unittest tests`
## List of environment variables
* `GITLABCI_CAURL`: URL of a staging 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
* `GITLABCI_DNSPORT`: port to reach your DNS server
* `GITLABCI_DNSZONE`: zone name wich contains the domain name to test
* `GITLABCI_DOMAIN`: the main domain to test (one test uses also `www.GITLABCI_DOMAIN`)
* `GITLABCI_TSIGALGORITHM`: TSIG algorithm (e.g. `hmac-sha256`)
* `GITLABCI_TSIGKEYNAME`: TSIG key name
* `GITLABCI_TSIGKEYVALUE`: TSIG secret
import os, sys, configparser
import os, configparser
from tempfile import NamedTemporaryFile
from subprocess import Popen
from urllib.request import urlopen
# domain with server.py running on it for testing
DOMAIN = os.getenv("GITLABCI_DOMAIN")
CAURL = os.getenv("GITLABCI_CAURL", "https://acme-staging.api.letsencrypt.org")
CHALLENGEDELAY = os.getenv("GITLABCI_CHALLENGDELAY", "3")
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")
TSIGKEYNAME = os.getenv("GITLABCI_TSIGKEYNAME")
......@@ -15,7 +15,7 @@ TSIGKEYVALUE = os.getenv("GITLABCI_TSIGKEYVALUE")
TSIGALGORITHM = os.getenv("GITLABCI_TSIGALGORITHM")
# generate account and domain keys
def gen_configs():
def gen_config():
# good account key
account_key = NamedTemporaryFile()
Popen(["openssl", "genrsa", "-out", account_key.name, "2048"]).wait()
......@@ -40,16 +40,6 @@ def gen_configs():
"-subj", "/", "-reqexts", "SAN", "-config", san_conf.name,
"-out", san_csr.name]).wait()
# invalid domain csr
invalid_csr = NamedTemporaryFile()
# Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key.name,
# "-subj", "/CN=\xC3\xA0\xC2\xB2\xC2\xA0_\xC3\xA0\xC2\xB2\xC2\xA0.com", "-out", invalid_csr.name]).wait()
# nonexistent domain csr
nonexistent_csr = NamedTemporaryFile()
Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key.name,
"-subj", "/CN=404.{0}".format(DOMAIN), "-out", nonexistent_csr.name]).wait()
# account-signed domain csr
account_csr = NamedTemporaryFile()
Popen(["openssl", "req", "-new", "-sha256", "-key", account_key.name,
......@@ -73,6 +63,12 @@ def gen_configs():
with open(goodCName.name, 'w') as configfile:
config.write(configfile)
dnsHostIP = NamedTemporaryFile()
config["DNS"]["Host"] = DNSHOSTIP
with open(dnsHostIP.name, 'w') as configfile:
config.write(configfile)
config["DNS"]["Host"] = DNSHOST
goodSAN = NamedTemporaryFile()
config["acmednstiny"]["AccountKeyFile"] = account_key.name
config["acmednstiny"]["CSRFile"] = san_csr.name
......@@ -84,39 +80,36 @@ def gen_configs():
config["acmednstiny"]["CSRFile"] = domain_csr.name
with open(weakKey.name, 'w') as configfile:
config.write(configfile)
invalidCSR = NamedTemporaryFile()
config["acmednstiny"]["AccountKeyFile"] = account_key.name
config["acmednstiny"]["CSRFile"] = invalid_csr.name
with open(invalidCSR.name, 'w') as configfile:
config.write(configfile)
inexistantDomain = NamedTemporaryFile()
config["acmednstiny"]["AccountKeyFile"] = account_key.name
config["acmednstiny"]["CSRFile"] = nonexistent_csr.name
with open(inexistantDomain.name, 'w') as configfile:
config.write(configfile)
accountAsDomain = NamedTemporaryFile()
config["acmednstiny"]["AccountKeyFile"] = account_key.name
config["acmednstiny"]["CSRFile"] = account_csr.name
with open(accountAsDomain.name, 'w') as configfile:
config.write(configfile)
invalidTSIGName = NamedTemporaryFile()
config["TSIGKeyring"]["KeyName"] = "{0}.invalid".format(TSIGKEYNAME)
with open(invalidTSIGName.name, 'w') as configfile:
config.write(configfile)
missingDNS = NamedTemporaryFile()
config["DNS"] = {}
with open(missingDNS.name, 'w') as configfile:
config.write(configfile)
return {
"goodCName": goodCName,
"dnsHostIP": dnsHostIP,
"goodSAN": goodSAN,
"weakKey": weakKey,
"invalidCSR": invalidCSR,
"inexistantDomain": inexistantDomain,
"accountAsDomain": accountAsDomain,
"invalidTSIGName": invalidTSIGName,
"missingDNS": missingDNS,
"key": {"accountkey": account_key,
"weakkey": weak_key,
"domainkey": domain_key},
"csr" : {"domaincsr": domain_csr,
"sancsr": san_csr,
"invalidcsr": invalid_csr,
"nonexistantcsr": nonexistent_csr,
"accountcsr": account_csr}
}
coveralls
coverage
logassert
argparse
configparser
dnspython3
dnspython3
\ No newline at end of file
......@@ -2,13 +2,17 @@ import unittest, sys
from subprocess import Popen, PIPE
from io import StringIO
import acme_dns_tiny
from .monkey import gen_configs
from .config_maker import gen_config
from .acme_account_delete import delete_account
import logassert
CONFIGS = gen_configs()
CONFIGS = gen_config()
class TestModule(unittest.TestCase):
"Tests for acme_dns_tiny.get_crt()"
def setUp(self):
logassert.setup(self, 'acme_dns_tiny_logger')
def test_success_cn(self):
""" Successfully issue a certificate via common name """
......@@ -22,6 +26,21 @@ class TestModule(unittest.TestCase):
stdout=PIPE, stderr=PIPE).communicate(crt)
self.assertIn("BEGIN", crt.decode("utf8"))
self.assertIn("Issuer", out.decode("utf8"))
def test_success_dnshost_ip(self):
""" When DNS Host is an IP, DNS resolution have to fail without error """
old_stdout = sys.stdout
sys.stdout = StringIO()
result = acme_dns_tiny.main([CONFIGS['dnsHostIP'].name])
self.assertLoggedInfo("DNS IPv4 record not found for configured dns host.")
self.assertLoggedInfo("DNS IPv4 and IPv6 records not found for configured dns host.")
sys.stdout.seek(0)
crt = sys.stdout.read().encode("utf8")
sys.stdout = old_stdout
out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE,
stdout=PIPE, stderr=PIPE).communicate(crt)
self.assertIn("BEGIN", crt.decode("utf8"))
self.assertIn("Issuer", out.decode("utf8"))
def test_success_san(self):
""" Successfully issue a certificate via subject alt name """
......@@ -64,8 +83,41 @@ class TestModule(unittest.TestCase):
self.assertIsInstance(result, ValueError)
self.assertIn("Certificate public key must be different than account key", result.args[0])
def test_failure_dns_update_tsigkeyname(self):
""" Fail to update DNS records by invalid TSIG Key name """
try:
result = acme_dns_tiny.main([CONFIGS['invalidTSIGName'].name])
except Exception as e:
result = e
self.assertIsInstance(result, ValueError)
self.assertIn("Error updating DNS", result.args[0])
def test_failure_notcompleted_configuration(self):
""" Configuration file have to be completed """
try:
result = acme_dns_tiny.main([CONFIGS['missingDNS'].name])
except Exception as e:
result = e
self.assertIsInstance(result, ValueError)
self.assertIn("Some required settings are missing.", result.args[0])
if __name__ == "__main__":
unittest.main()
# delete account key registration at end of tests
delete_account(CONFIGS["key"]["accountkey"].name)
try:
unittest.main()
finally:
# delete account key registration at end of tests
delete_account(CONFIGS["key"]["accountkey"].name)
# close temp files correctly
CONFIGS["goodCName"].close()
CONFIGS["dnsHostIP"].close()
CONFIGS["goodSAN"].close()
CONFIGS["weakKey"].close()
CONFIGS["accountAsDomain"].close()
CONFIGS["invalidTSIGName"].close()
CONFIGS["missingDNS"].close()
CONFIGS["key"]["accountkey"].close()
CONFIGS["key"]["weakkey"].close()
CONFIGS["key"]["domainkey"].close()
CONFIGS["csr"]["domaincsr"].close()
CONFIGS["csr"]["sancsr"].close()
CONFIGS["csr"]["accountcsr"].close()
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