staging_test_acme_dns_tiny.py 7.65 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
ACME_DIRECTORY = os.getenv("GITLABCI_ACMEDIRECTORY_V2",
                           "https://acme-staging-v02.api.letsencrypt.org/directory")

16

17
def _openssl(command, options, communicate=None):
18
    """Helper function to run openssl command."""
19 20 21 22 23 24 25
    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")
26

27

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

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

    # To clean ACME staging server and close correctly temporary files
41
    # pylint: disable=bare-except
42
    @classmethod
43
    def tearDownClass(cls):
44
        # close temp files correctly
45
        for conffile in cls.configs:
46 47 48 49 50
            # 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:
51
                    account_deactivate(parser["acmednstiny"]["AccountKeyFile"], ACME_DIRECTORY)
52 53 54 55 56 57 58 59 60 61
                except:
                    pass
                try:
                    os.remove(parser["acmednstiny"]["AccountKeyFile"])
                except:
                    pass
                try:
                    os.remove(parser["acmednstiny"]["CSRFile"])
                except:
                    pass
62
            try:
63
                os.remove(cls.configs[conffile])
64 65
            except:
                pass
66
        super(TestACMEDNSTiny, cls).tearDownClass()
67

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

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

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

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

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

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

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

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

105
        self._assert_certificate_chain(certchain)
106

107
    def test_success_cn_with_csr_option(self):
108
        """Successfully issue a certificate using CSR option outside from the config file."""
109 110 111
        old_stdout = sys.stdout
        sys.stdout = StringIO()

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

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

119
        self._assert_certificate_chain(certchain)
120

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

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

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

132
        self._assert_certificate_chain(certchain)
133

134
    def test_success_dnshost_ip(self):
135
        """When DNS Host is an IP, DNS resolution have to fail without error."""
136 137
        old_stdout = sys.stdout
        sys.stdout = StringIO()
138

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

147
        sys.stdout.close()
148
        sys.stdout = old_stdout
149 150

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

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

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

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

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

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

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

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

176
        self._assert_certificate_chain(certchain)
177

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

184
        certchain = certout.decode("utf8")
185 186

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

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

        certchain = certout.decode("utf8")

197
        self._assert_certificate_chain(certchain)
198

199
    def test_failure_dns_update_tsigkeyname(self):
200
        """Fail to update DNS records by invalid TSIG Key name."""
201
        self.assertRaisesRegex(ValueError,
202
                               "Error updating DNS records",
203 204
                               acme_dns_tiny.main, [self.configs['invalid_tsig_name'],
                                                    "--verbose"])
205

206

207
if __name__ == "__main__":  # pragma: no cover
208
    unittest.main()