1"""Tests for acme_dns_tiny script to be run with real ACME server""" 

2import unittest 

3import sys 

4import os 

5import subprocess 

6import configparser 

7from io import StringIO 

8import dns.version 

9import acme_dns_tiny 

10from tests.config_factory import generate_acme_dns_tiny_config 

11from tools.acme_account_deactivate import account_deactivate 

12 

13ACME_DIRECTORY = os.getenv("GITLABCI_ACMEDIRECTORY_V2", 

14 "https://acme-staging-v02.api.letsencrypt.org/directory") 

15ACME_TIMEOUT = os.getenv("GITLABCI_ACMETIMEOUT", "10") 

16 

17 

18def _openssl(command, options, communicate=None): 

19 """Helper function to run openssl command.""" 

20 with subprocess.Popen(["openssl", command] + options, 

21 stdin=subprocess.PIPE, stdout=subprocess.PIPE, 

22 stderr=subprocess.PIPE) as openssl: 

23 out, err = openssl.communicate(communicate) 

24 if openssl.returncode != 0: 

25 raise IOError("OpenSSL Error: {0}".format(err)) 

26 return out.decode("utf8") 

27 

28 

29class TestACMEDNSTiny(unittest.TestCase): 

30 """Tests for acme_dns_tiny.get_crt().""" 

31 

32 @classmethod 

33 def setUpClass(cls): 

34 print("Init acme_dns_tiny with python modules:") 

35 print(" - python: {0}".format(sys.version)) 

36 print(" - dns python: {0}".format(dns.version.version)) 

37 cls.configs = generate_acme_dns_tiny_config() 

38 sys.stdout.flush() 

39 super(TestACMEDNSTiny, cls).setUpClass() 

40 

41 # To clean ACME staging server and close correctly temporary files 

42 # pylint: disable=bare-except 

43 @classmethod 

44 def tearDownClass(cls): 

45 # close temp files correctly 

46 for conf_name, file in cls.configs.items(): 

47 # for each configuration file, deactivate the account and remove linked temporary files 

48 if conf_name != "cname_csr": 

49 parser = configparser.ConfigParser() 

50 parser.read(file) 

51 try: 

52 account_deactivate( 

53 parser["acmednstiny"]["AccountKeyFile"], ACME_DIRECTORY, ACME_TIMEOUT) 

54 except: 

55 pass 

56 try: 

57 os.remove(parser["acmednstiny"]["AccountKeyFile"]) 

58 except: 

59 pass 

60 try: 

61 os.remove(parser["acmednstiny"]["CSRFile"]) 

62 except: 

63 pass 

64 try: 

65 os.remove(file) 

66 except: 

67 pass 

68 super(TestACMEDNSTiny, cls).tearDownClass() 

69 

70 # helper function to valid success by making assertion on returned certificate chain 

71 def _assert_certificate_chain(self, cert_chain): 

72 # Output have to contain at least two certificates to create a chain 

73 certlist = list(filter(None, cert_chain.split("-----BEGIN CERTIFICATE-----"))) 

74 self.assertTrue(len(certlist) >= 2) 

75 for cert in certlist: 

76 self.assertIn("-----END CERTIFICATE-----", cert) 

77 # Use openssl to check validity of chain and simple test of readability 

78 readablecertchain = _openssl("x509", ["-text", "-noout"], 

79 cert_chain.encode("utf8")) 

80 self.assertIn("Issuer", readablecertchain) 

81 

82 def test_success_cn(self): 

83 """Successfully issue a certificate via common name.""" 

84 old_stdout = sys.stdout 

85 sys.stdout = StringIO() 

86 

87 acme_dns_tiny.main([self.configs['good_cname'], "--verbose"]) 

88 certchain = sys.stdout.getvalue() 

89 

90 sys.stdout.close() 

91 sys.stdout = old_stdout 

92 

93 self._assert_certificate_chain(certchain) 

94 

95 def test_success_cn_without_contacts(self): 

96 """Successfully issue a certificate via CN, but without Contacts field.""" 

97 old_stdout = sys.stdout 

98 sys.stdout = StringIO() 

99 

100 acme_dns_tiny.main([self.configs['good_cname_without_contacts'], "--verbose"]) 

101 certchain = sys.stdout.getvalue() 

102 

103 sys.stdout.close() 

104 sys.stdout = old_stdout 

105 

106 self._assert_certificate_chain(certchain) 

107 

108 def test_success_cn_with_csr_option(self): 

109 """Successfully issue a certificate using CSR option outside from the config file.""" 

110 old_stdout = sys.stdout 

111 sys.stdout = StringIO() 

112 

113 acme_dns_tiny.main(["--csr", self.configs['cname_csr'], 

114 self.configs['good_cname_without_csr'], "--verbose"]) 

115 certchain = sys.stdout.getvalue() 

116 

117 sys.stdout.close() 

118 sys.stdout = old_stdout 

119 

120 self._assert_certificate_chain(certchain) 

121 

122 def test_success_wild_cn(self): 

123 """Successfully issue a certificate via a wildcard common name.""" 

124 old_stdout = sys.stdout 

125 sys.stdout = StringIO() 

126 

127 acme_dns_tiny.main([self.configs['wild_cname'], "--verbose"]) 

128 certchain = sys.stdout.getvalue() 

129 

130 sys.stdout.close() 

131 sys.stdout = old_stdout 

132 

133 self._assert_certificate_chain(certchain) 

134 

135 def test_success_san(self): 

136 """Successfully issue a certificate via subject alt name.""" 

137 old_stdout = sys.stdout 

138 sys.stdout = StringIO() 

139 

140 acme_dns_tiny.main([self.configs['good_san'], "--verbose"]) 

141 certchain = sys.stdout.getvalue() 

142 

143 sys.stdout.close() 

144 sys.stdout = old_stdout 

145 

146 self._assert_certificate_chain(certchain) 

147 

148 def test_success_wildsan(self): 

149 """Successfully issue a certificate via wildcard in subject alt name.""" 

150 old_stdout = sys.stdout 

151 sys.stdout = StringIO() 

152 

153 acme_dns_tiny.main([self.configs['wild_san']]) 

154 certchain = sys.stdout.getvalue() 

155 

156 sys.stdout.close() 

157 sys.stdout = old_stdout 

158 

159 self._assert_certificate_chain(certchain) 

160 

161 def test_success_cli(self): 

162 """Successfully issue a certificate via command line interface.""" 

163 with subprocess.Popen(["python3", "acme_dns_tiny.py", 

164 self.configs['good_cname'], "--verbose"], 

165 stdout=subprocess.PIPE, stderr=subprocess.PIPE) as cli: 

166 certout, _ = cli.communicate() 

167 certchain = certout.decode("utf8") 

168 self._assert_certificate_chain(certchain) 

169 

170 def test_success_cli_with_csr_option(self): 

171 """Successfully issue a certificate via command line interface using CSR option.""" 

172 with subprocess.Popen(["python3", "acme_dns_tiny.py", "--csr", self.configs['cname_csr'], 

173 self.configs['good_cname_without_csr'], "--verbose"], 

174 stdout=subprocess.PIPE, stderr=subprocess.PIPE) as cli: 

175 certout, _ = cli.communicate() 

176 certchain = certout.decode("utf8") 

177 self._assert_certificate_chain(certchain) 

178 

179 def test_failure_dns_update_tsigkeyname(self): 

180 """Fail to update DNS records by invalid TSIG Key name.""" 

181 self.assertRaisesRegex(RuntimeError, 

182 "Unable to add DNS resource to _acme-challenge.{0}." 

183 .format(os.getenv("GITLABCI_DOMAIN")), 

184 acme_dns_tiny.main, [self.configs['invalid_tsig_name'], 

185 "--verbose"]) 

186 

187 

188if __name__ == "__main__": # pragma: no cover 

189 unittest.main()