test_acme_dns_tiny.py 7.05 KB
Newer Older
1
import unittest, sys, os, subprocess, time
2
from io import StringIO
3
import dns.version
4
import acme_dns_tiny
Adrien Dorsaz's avatar
Adrien Dorsaz committed
5
from tests.config_factory import generate_acme_dns_tiny_config
6
from tools.acme_account_deactivate import account_deactivate
7

8
ACMEDirectory = os.getenv("GITLABCI_ACMEDIRECTORY_V2", "https://acme-staging-v02.api.letsencrypt.org/directory")
9

Adrien Dorsaz's avatar
Adrien Dorsaz committed
10
class TestACMEDNSTiny(unittest.TestCase):
11
    "Tests for acme_dns_tiny.get_crt()"
Adrien Dorsaz's avatar
Adrien Dorsaz committed
12

13 14
    @classmethod
    def setUpClass(self):
15 16 17
        print("Init acme_dns_tiny with python modules:")
        print("  - python: {0}".format(sys.version))
        print("  - dns python: {0}".format(dns.version.version))
Adrien Dorsaz's avatar
Adrien Dorsaz committed
18
        self.configs = generate_acme_dns_tiny_config()
19
        sys.stdout.flush()
20
        super(TestACMEDNSTiny, self).setUpClass()
21 22 23 24

    # To clean ACME staging server and close correctly temporary files
    @classmethod
    def tearDownClass(self):
25
        # deactivate account key registration at end of tests
26
        account_deactivate(self.configs["accountkey"], ACMEDirectory)
27 28
        # close temp files correctly
        for tmpfile in self.configs:
29
            os.remove(self.configs[tmpfile])
30
        super(TestACMEDNSTiny, self).tearDownClass()
31

32
    # helper function to run openssl command
33
    def openssl(self, command, options, communicate=None):
34 35 36 37 38
        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))
39
        return out.decode("utf8")
40

41 42 43 44
    # helper function to valid success by making assertion on returned certificate chain
    def assertCertificateChain(self, certificateChain):
        # Output have to contains two certiicates
        certlist = certificateChain.split("-----BEGIN CERTIFICATE-----")
45 46
        self.assertEqual(3, len(certlist))
        self.assertEqual('', certlist[0])
47
        self.assertIn("-----END CERTIFICATE-----{0}".format(os.linesep), certlist[1])
48
        self.assertIn("-----END CERTIFICATE-----{0}".format(os.linesep), certlist[2])
49 50 51 52
        # Use openssl to check validity of chain and simple test of readability
        readablecertchain = self.openssl("x509", ["-text", "-noout"], certificateChain.encode("utf8"))
        self.assertIn("Issuer", readablecertchain)

Daniel Roesler's avatar
Daniel Roesler committed
53
    def test_success_cn(self):
Jakub Wilk's avatar
Jakub Wilk committed
54
        """ Successfully issue a certificate via common name """
Daniel Roesler's avatar
Daniel Roesler committed
55 56
        old_stdout = sys.stdout
        sys.stdout = StringIO()
57
        
58
        acme_dns_tiny.main([self.configs['goodCName']])
59
        certchain = sys.stdout.getvalue()
60
        
61
        sys.stdout.close()
Daniel Roesler's avatar
Daniel Roesler committed
62
        sys.stdout = old_stdout
63
        
64
        self.assertCertificateChain(certchain)
Adrien Dorsaz's avatar
Adrien Dorsaz committed
65

66 67 68 69 70 71 72 73 74 75 76 77 78
    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()

        acme_dns_tiny.main(["--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']])
        certchain = sys.stdout.getvalue()

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

        self.assertCertificateChain(certchain)

79 80 81 82 83 84 85 86 87 88 89 90 91
    def test_success_wild_cn(self):
        """ Successfully issue a certificate via a wildcard common name """
        old_stdout = sys.stdout
        sys.stdout = StringIO()

        acme_dns_tiny.main([self.configs['wildCName']])
        certchain = sys.stdout.getvalue()

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

        self.assertCertificateChain(certchain)

92 93 94 95
    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()
96 97
        
        with self.assertLogs(level='INFO') as adnslog:
98
            acme_dns_tiny.main([self.configs['dnsHostIP']])
Adrien Dorsaz's avatar
Adrien Dorsaz committed
99 100
        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)
101
        certchain = sys.stdout.getvalue()
102
        
103
        sys.stdout.close()
104
        sys.stdout = old_stdout
105
        
106
        self.assertCertificateChain(certchain)
Daniel Roesler's avatar
Daniel Roesler committed
107 108

    def test_success_san(self):
Jakub Wilk's avatar
Jakub Wilk committed
109
        """ Successfully issue a certificate via subject alt name """
Daniel Roesler's avatar
Daniel Roesler committed
110 111
        old_stdout = sys.stdout
        sys.stdout = StringIO()
112
        
113
        acme_dns_tiny.main([self.configs['goodSAN']])
114
        certchain = sys.stdout.getvalue()
115
        
116
        sys.stdout.close()
Daniel Roesler's avatar
Daniel Roesler committed
117
        sys.stdout = old_stdout
118
        
119
        self.assertCertificateChain(certchain)
Daniel Roesler's avatar
Daniel Roesler committed
120

121 122 123 124 125 126 127 128 129 130 131 132 133
    def test_success_wildsan(self):
        """ Successfully issue a certificate via wildcard in subject alt name """
        old_stdout = sys.stdout
        sys.stdout = StringIO()

        acme_dns_tiny.main([self.configs['wildSAN']])
        certchain = sys.stdout.getvalue()

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

        self.assertCertificateChain(certchain)

Daniel Roesler's avatar
Daniel Roesler committed
134
    def test_success_cli(self):
Jakub Wilk's avatar
Jakub Wilk committed
135
        """ Successfully issue a certificate via command line interface """
136
        certout, err = subprocess.Popen([
137
            "python3", "acme_dns_tiny.py", self.configs['goodCName']
138
        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
139
        
140
        certchain = certout.decode("utf8")
141 142
        
        self.assertCertificateChain(certchain)
Daniel Roesler's avatar
Daniel Roesler committed
143

144 145 146
    def test_success_cli_with_csr_option(self):
        """ Successfully issue a certificate via command line interface using CSR option"""
        certout, err = subprocess.Popen([
147
            "python3", "acme_dns_tiny.py", "--csr", self.configs['cnameCSR'], self.configs['goodCNameWithoutCSR']
148 149 150 151 152 153
        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()

        certchain = certout.decode("utf8")

        self.assertCertificateChain(certchain)

Daniel Roesler's avatar
Daniel Roesler committed
154 155
    def test_weak_key(self):
        """ Let's Encrypt rejects weak keys """
156
        self.assertRaisesRegex(ValueError,
Adrien Dorsaz's avatar
Adrien Dorsaz committed
157
                               "key too small",
158
                               acme_dns_tiny.main, [self.configs['weakKey']])
Daniel Roesler's avatar
Daniel Roesler committed
159 160 161

    def test_account_key_domain(self):
        """ Can't use the account key for the CSR """
162
        self.assertRaisesRegex(ValueError,
Adrien Dorsaz's avatar
Adrien Dorsaz committed
163
                               "certificate public key must be different than account key",
164
                               acme_dns_tiny.main, [self.configs['accountAsDomain']])
Daniel Roesler's avatar
Daniel Roesler committed
165

166 167
    def test_failure_dns_update_tsigkeyname(self):
        """ Fail to update DNS records by invalid TSIG Key name """
168 169
        self.assertRaisesRegex(ValueError,
                               "Error updating DNS",
170
                               acme_dns_tiny.main, [self.configs['invalidTSIGName']])
171

172 173
    def test_failure_notcompleted_configuration(self):
        """ Configuration file have to be completed """
174 175
        self.assertRaisesRegex(ValueError,
                               "Some required settings are missing\.",
176
                               acme_dns_tiny.main, [self.configs['missingDNS']])
177 178

if __name__ == "__main__":
179
    unittest.main()