staging_test_acme_dns_tiny.py 8.13 KB
Newer Older
1 2 3 4 5 6
"""Tests for acme_dns_tiny script to be run with real ACME server"""
import unittest
import sys
import os
import subprocess
import configparser
7
from io import StringIO
8
import dns.version
9
import acme_dns_tiny
Adrien Dorsaz's avatar
Adrien Dorsaz committed
10
from tests.config_factory import generate_acme_dns_tiny_config
11
from tools.acme_account_deactivate import account_deactivate
12

13 14 15 16 17 18 19 20 21 22 23 24
ACME_DIRECTORY = os.getenv("GITLABCI_ACMEDIRECTORY_V2",
                           "https://acme-staging-v02.api.letsencrypt.org/directory")

def _openssl(command, options, communicate=None):
    """Helper function to run openssl command"""
    openssl = subprocess.Popen(["openssl", command] + options,
                               stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
    out, err = openssl.communicate(communicate)
    if openssl.returncode != 0:
        raise IOError("OpenSSL Error: {0}".format(err))
    return out.decode("utf8")
25

Adrien Dorsaz's avatar
Adrien Dorsaz committed
26
class TestACMEDNSTiny(unittest.TestCase):
27
    "Tests for acme_dns_tiny.get_crt()"
Adrien Dorsaz's avatar
Adrien Dorsaz committed
28

29
    @classmethod
30
    def setUpClass(cls):
31 32 33
        print("Init acme_dns_tiny with python modules:")
        print("  - python: {0}".format(sys.version))
        print("  - dns python: {0}".format(dns.version.version))
34
        cls.configs = generate_acme_dns_tiny_config()
35
        sys.stdout.flush()
36
        super(TestACMEDNSTiny, cls).setUpClass()
37 38

    # To clean ACME staging server and close correctly temporary files
39
    #pylint: disable=bare-except
40
    @classmethod
41
    def tearDownClass(cls):
42
        # close temp files correctly
43
        for conffile in cls.configs:
44 45 46 47 48
            # for each configuration file, deactivate the account and remove linked temporary files
            if conffile != "cname_csr":
                parser = configparser.ConfigParser()
                parser.read(cls.configs[conffile])
                try:
49
                    account_deactivate(parser["acmednstiny"]["AccountKeyFile"], ACME_DIRECTORY)
50 51 52 53 54 55 56 57 58 59
                except:
                    pass
                try:
                    os.remove(parser["acmednstiny"]["AccountKeyFile"])
                except:
                    pass
                try:
                    os.remove(parser["acmednstiny"]["CSRFile"])
                except:
                    pass
60
            try:
61
                os.remove(cls.configs[conffile])
62 63
            except:
                pass
64
        super(TestACMEDNSTiny, cls).tearDownClass()
65

66
    # helper function to valid success by making assertion on returned certificate chain
67
    def _assert_certificate_chain(self, cert_chain):
68
        # Output have to contains two certificates
69
        certlist = cert_chain.split("-----BEGIN CERTIFICATE-----")
70 71
        self.assertEqual(3, len(certlist))
        self.assertEqual('', certlist[0])
72
        self.assertIn("-----END CERTIFICATE-----{0}".format(os.linesep), certlist[1])
73
        self.assertIn("-----END CERTIFICATE-----{0}".format(os.linesep), certlist[2])
74
        # Use openssl to check validity of chain and simple test of readability
75 76
        readablecertchain = _openssl("x509", ["-text", "-noout"],
                                     cert_chain.encode("utf8"))
77 78
        self.assertIn("Issuer", readablecertchain)

Daniel Roesler's avatar
Daniel Roesler committed
79
    def test_success_cn(self):
Jakub Wilk's avatar
Jakub Wilk committed
80
        """ Successfully issue a certificate via common name """
Daniel Roesler's avatar
Daniel Roesler committed
81 82
        old_stdout = sys.stdout
        sys.stdout = StringIO()
83 84

        acme_dns_tiny.main([self.configs['good_cname'], "--verbose"])
85
        certchain = sys.stdout.getvalue()
86

87
        sys.stdout.close()
Daniel Roesler's avatar
Daniel Roesler committed
88
        sys.stdout = old_stdout
89 90

        self._assert_certificate_chain(certchain)
Adrien Dorsaz's avatar
Adrien Dorsaz committed
91

92 93 94 95 96
    def test_success_cn_without_contacts(self):
        """ Successfully issue a certificate via CN, but without Contacts field """
        old_stdout = sys.stdout
        sys.stdout = StringIO()

97
        acme_dns_tiny.main([self.configs['good_cname_without_contacts'], "--verbose"])
98 99 100 101 102
        certchain = sys.stdout.getvalue()

        sys.stdout.close()
        sys.stdout = old_stdout

103
        self._assert_certificate_chain(certchain)
104

105 106 107 108 109
    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()

110 111
        acme_dns_tiny.main(["--csr", self.configs['cname_csr'],
                            self.configs['good_cname_without_csr'], "--verbose"])
112 113 114 115 116
        certchain = sys.stdout.getvalue()

        sys.stdout.close()
        sys.stdout = old_stdout

117
        self._assert_certificate_chain(certchain)
118

119 120 121 122 123
    def test_success_wild_cn(self):
        """ Successfully issue a certificate via a wildcard common name """
        old_stdout = sys.stdout
        sys.stdout = StringIO()

124
        acme_dns_tiny.main([self.configs['wild_cname'], "--verbose"])
125 126 127 128 129
        certchain = sys.stdout.getvalue()

        sys.stdout.close()
        sys.stdout = old_stdout

130
        self._assert_certificate_chain(certchain)
131

132 133 134 135
    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()
136

137
        with self.assertLogs(level='INFO') as adnslog:
138 139 140 141 142
            acme_dns_tiny.main([self.configs['dns_host_ip'],
                                "--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)
143
        certchain = sys.stdout.getvalue()
144

145
        sys.stdout.close()
146
        sys.stdout = old_stdout
147 148

        self._assert_certificate_chain(certchain)
Daniel Roesler's avatar
Daniel Roesler committed
149 150

    def test_success_san(self):
Jakub Wilk's avatar
Jakub Wilk committed
151
        """ Successfully issue a certificate via subject alt name """
Daniel Roesler's avatar
Daniel Roesler committed
152 153
        old_stdout = sys.stdout
        sys.stdout = StringIO()
154 155

        acme_dns_tiny.main([self.configs['good_san'], "--verbose"])
156
        certchain = sys.stdout.getvalue()
157

158
        sys.stdout.close()
Daniel Roesler's avatar
Daniel Roesler committed
159
        sys.stdout = old_stdout
160 161

        self._assert_certificate_chain(certchain)
Daniel Roesler's avatar
Daniel Roesler committed
162

163 164 165 166 167
    def test_success_wildsan(self):
        """ Successfully issue a certificate via wildcard in subject alt name """
        old_stdout = sys.stdout
        sys.stdout = StringIO()

168
        acme_dns_tiny.main([self.configs['wild_san']])
169 170 171 172 173
        certchain = sys.stdout.getvalue()

        sys.stdout.close()
        sys.stdout = old_stdout

174
        self._assert_certificate_chain(certchain)
175

Daniel Roesler's avatar
Daniel Roesler committed
176
    def test_success_cli(self):
Jakub Wilk's avatar
Jakub Wilk committed
177
        """ Successfully issue a certificate via command line interface """
178 179
        certout, _ = subprocess.Popen([
            "python3", "acme_dns_tiny.py", self.configs['good_cname'], "--verbose"
180
        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
181

182
        certchain = certout.decode("utf8")
183 184

        self._assert_certificate_chain(certchain)
Daniel Roesler's avatar
Daniel Roesler committed
185

186 187
    def test_success_cli_with_csr_option(self):
        """ Successfully issue a certificate via command line interface using CSR option"""
188 189 190
        certout, _ = subprocess.Popen([
            "python3", "acme_dns_tiny.py", "--csr", self.configs['cname_csr'],
            self.configs['good_cname_without_csr'], "--verbose"
191 192 193 194
        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()

        certchain = certout.decode("utf8")

195
        self._assert_certificate_chain(certchain)
196

Daniel Roesler's avatar
Daniel Roesler committed
197 198
    def test_weak_key(self):
        """ Let's Encrypt rejects weak keys """
199
        self.assertRaisesRegex(ValueError,
Adrien Dorsaz's avatar
Adrien Dorsaz committed
200
                               "key too small",
201
                               acme_dns_tiny.main, [self.configs['weak_key'], "--verbose"])
Daniel Roesler's avatar
Daniel Roesler committed
202 203 204

    def test_account_key_domain(self):
        """ Can't use the account key for the CSR """
205
        self.assertRaisesRegex(ValueError,
Adrien Dorsaz's avatar
Adrien Dorsaz committed
206
                               "certificate public key must be different than account key",
207
                               acme_dns_tiny.main, [self.configs['account_as_domain'], "--verbose"])
Daniel Roesler's avatar
Daniel Roesler committed
208

209 210
    def test_failure_dns_update_tsigkeyname(self):
        """ Fail to update DNS records by invalid TSIG Key name """
211 212
        self.assertRaisesRegex(ValueError,
                               "Error updating DNS",
213
                               acme_dns_tiny.main, [self.configs['invalid_tsig_name'], "--verbose"])
214

215
if __name__ == "__main__":  # pragma: no cover
216
    unittest.main()