1import unittest, os, sys, tempfile, logging
2from subprocess import Popen, PIPE
3try:
4    from StringIO import StringIO # Python 2
5except ImportError:
6    from io import StringIO # Python 3
7
8import acme_tiny
9from .monkey import gen_keys
10
11KEYS = gen_keys()
12
13class TestModule(unittest.TestCase):
14    "Tests for acme_tiny.get_crt()"
15
16    def setUp(self):
17        self.DIR_URL = "https://acme-staging-v02.api.letsencrypt.org/directory"
18        self.tempdir = tempfile.mkdtemp()
19        self.fuse_proc = Popen(["python", "tests/monkey.py", self.tempdir])
20
21    def tearDown(self):
22        self.fuse_proc.terminate()
23        self.fuse_proc.wait()
24        os.rmdir(self.tempdir)
25
26    def test_success_cn(self):
27        """ Successfully issue a certificate via common name """
28        old_stdout = sys.stdout
29        sys.stdout = StringIO()
30        result = acme_tiny.main([
31            "--account-key", KEYS['account_key'].name,
32            "--csr", KEYS['domain_csr'].name,
33            "--acme-dir", self.tempdir,
34            "--directory-url", self.DIR_URL,
35        ])
36        sys.stdout.seek(0)
37        crt = sys.stdout.read().encode("utf8")
38        sys.stdout = old_stdout
39        out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE, stdout=PIPE, stderr=PIPE).communicate(crt)
40        self.assertIn("Issuer: CN=Fake LE Intermediate", out.decode("utf8"))
41
42    def test_success_san(self):
43        """ Successfully issue a certificate via subject alt name """
44        old_stdout = sys.stdout
45        sys.stdout = StringIO()
46        result = acme_tiny.main([
47            "--account-key", KEYS['account_key'].name,
48            "--csr", KEYS['san_csr'].name,
49            "--acme-dir", self.tempdir,
50            "--directory-url", self.DIR_URL,
51        ])
52        sys.stdout.seek(0)
53        crt = sys.stdout.read().encode("utf8")
54        sys.stdout = old_stdout
55        out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE, stdout=PIPE, stderr=PIPE).communicate(crt)
56        self.assertIn("Issuer: CN=Fake LE Intermediate", out.decode("utf8"))
57
58    def test_success_cli(self):
59        """ Successfully issue a certificate via command line interface """
60        crt, err = Popen([
61            "python", "acme_tiny.py",
62            "--account-key", KEYS['account_key'].name,
63            "--csr", KEYS['domain_csr'].name,
64            "--acme-dir", self.tempdir,
65            "--directory-url", self.DIR_URL,
66        ], stdout=PIPE, stderr=PIPE).communicate()
67        out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE, stdout=PIPE, stderr=PIPE).communicate(crt)
68        self.assertIn("Issuer: CN=Fake LE Intermediate", out.decode("utf8"))
69
70    def test_missing_account_key(self):
71        """ OpenSSL throws an error when the account key is missing """
72        try:
73            result = acme_tiny.main([
74                "--account-key", "/foo/bar",
75                "--csr", KEYS['domain_csr'].name,
76                "--acme-dir", self.tempdir,
77                "--directory-url", self.DIR_URL,
78            ])
79        except Exception as e:
80            result = e
81        self.assertIsInstance(result, IOError)
82        self.assertIn("Error opening Private Key", result.args[0])
83
84    def test_missing_csr(self):
85        """ OpenSSL throws an error when the CSR is missing """
86        try:
87            result = acme_tiny.main([
88                "--account-key", KEYS['account_key'].name,
89                "--csr", "/foo/bar",
90                "--acme-dir", self.tempdir,
91                "--directory-url", self.DIR_URL,
92            ])
93        except Exception as e:
94            result = e
95        self.assertIsInstance(result, IOError)
96        self.assertIn("Error loading /foo/bar", result.args[0])
97
98    def test_weak_key(self):
99        """ Let's Encrypt rejects weak keys """
100        try:
101            result = acme_tiny.main([
102                "--account-key", KEYS['weak_key'].name,
103                "--csr", KEYS['domain_csr'].name,
104                "--acme-dir", self.tempdir,
105                "--directory-url", self.DIR_URL,
106            ])
107        except Exception as e:
108            result = e
109        self.assertIsInstance(result, ValueError)
110        self.assertIn("key too small", result.args[0])
111
112    def test_invalid_domain(self):
113        """ Let's Encrypt rejects invalid domains """
114        try:
115            result = acme_tiny.main([
116                "--account-key", KEYS['account_key'].name,
117                "--csr", KEYS['invalid_csr'].name,
118                "--acme-dir", self.tempdir,
119                "--directory-url", self.DIR_URL,
120            ])
121        except Exception as e:
122            result = e
123        self.assertIsInstance(result, ValueError)
124        self.assertIn("Invalid character in DNS name", result.args[0])
125
126    def test_nonexistent_domain(self):
127        """ Should be unable verify a nonexistent domain """
128        try:
129            result = acme_tiny.main([
130                "--account-key", KEYS['account_key'].name,
131                "--csr", KEYS['nonexistent_csr'].name,
132                "--acme-dir", self.tempdir,
133                "--directory-url", self.DIR_URL,
134            ])
135        except Exception as e:
136            result = e
137        self.assertIsInstance(result, ValueError)
138        self.assertIn("but couldn't download", result.args[0])
139
140    def test_account_key_domain(self):
141        """ Can't use the account key for the CSR """
142        try:
143            result = acme_tiny.main([
144                "--account-key", KEYS['account_key'].name,
145                "--csr", KEYS['account_csr'].name,
146                "--acme-dir", self.tempdir,
147                "--directory-url", self.DIR_URL,
148            ])
149        except Exception as e:
150            result = e
151        self.assertIsInstance(result, ValueError)
152        self.assertIn("certificate public key must be different than account key", result.args[0])
153
154    def test_contact(self):
155        """ Make sure optional contact details can be set """
156        # add a logging handler that captures the info log output
157        log_output = StringIO()
158        debug_handler = logging.StreamHandler(log_output)
159        acme_tiny.LOGGER.addHandler(debug_handler)
160        # call acme_tiny with new contact details
161        old_stdout = sys.stdout
162        sys.stdout = StringIO()
163        result = acme_tiny.main([
164            "--account-key", KEYS['account_key'].name,
165            "--csr", KEYS['domain_csr'].name,
166            "--acme-dir", self.tempdir,
167            "--directory-url", self.DIR_URL,
168            "--contact", "mailto:devteam@gethttpsforfree.com", "mailto:boss@gethttpsforfree.com",
169        ])
170        sys.stdout.seek(0)
171        crt = sys.stdout.read().encode("utf8")
172        sys.stdout = old_stdout
173        log_output.seek(0)
174        log_string = log_output.read().encode("utf8")
175        # make sure the certificate was issued and the contact details were updated
176        out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE, stdout=PIPE, stderr=PIPE).communicate(crt)
177        self.assertIn("Issuer: CN=Fake LE Intermediate", out.decode("utf8"))
178        self.assertIn("Updated contact details:\nmailto:devteam@gethttpsforfree.com\nmailto:boss@gethttpsforfree.com", log_string.decode("utf8"))
179        # remove logging capture
180        acme_tiny.LOGGER.removeHandler(debug_handler)
181
182