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