1"""Create real temporary ACME dns tiny configurations to run tests with real server""" 

2import os 

3import configparser 

4from tempfile import NamedTemporaryFile 

5from subprocess import Popen 

6 

7# domain with server.py running on it for testing 

8DOMAIN = os.getenv("GITLABCI_DOMAIN") 

9ACMEDIRECTORY = os.getenv("GITLABCI_ACMEDIRECTORY_V2", 

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

11IS_PEBBLE = ACMEDIRECTORY.startswith('https://pebble') 

12DNSHOST = os.getenv("GITLABCI_DNSHOST") 

13DNSHOSTIP = os.getenv("GITLABCI_DNSHOSTIP") 

14DNSZONE = os.getenv("GITLABCI_DNSZONE") 

15DNSPORT = os.getenv("GITLABCI_DNSPORT", "53") 

16DNSTTL = os.getenv("GITLABCI_DNSTTL", "10") 

17TSIGKEYNAME = os.getenv("GITLABCI_TSIGKEYNAME") 

18TSIGKEYVALUE = os.getenv("GITLABCI_TSIGKEYVALUE") 

19TSIGALGORITHM = os.getenv("GITLABCI_TSIGALGORITHM") 

20CONTACT = os.getenv("GITLABCI_CONTACT") 

21 

22 

23def generate_config(account_key_path=None): 

24 """Generate basic acme-dns-tiny configuration""" 

25 # Account key should be created if not given 

26 if account_key_path is None: 

27 account_key = NamedTemporaryFile(delete=False) 

28 Popen(["openssl", "genrsa", "-out", account_key.name, "2048"]).wait() 

29 account_key_path = account_key.name 

30 

31 # Domain key and CSR 

32 domain_key = NamedTemporaryFile(delete=False) 

33 domain_csr = NamedTemporaryFile(delete=False) 

34 if IS_PEBBLE: # Pebble server enforces usage of SAN instead of CN 

35 san_conf = NamedTemporaryFile(delete=False) 

36 with open("/etc/ssl/openssl.cnf", 'r') as opensslcnf: 

37 san_conf.write(opensslcnf.read().encode("utf8")) 

38 san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0}\n".format(DOMAIN).encode("utf8")) 

39 san_conf.seek(0) 

40 Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key.name, 

41 "-subj", "/", "-reqexts", "SAN", "-config", san_conf.name, 

42 "-out", domain_csr.name]).wait() 

43 os.remove(san_conf.name) 

44 else: 

45 Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key.name, 

46 "-subj", "/CN={0}".format(DOMAIN), "-out", domain_csr.name]).wait() 

47 

48 # acme-dns-tiny configuration 

49 parser = configparser.ConfigParser() 

50 parser.read("./example.ini") 

51 parser["acmednstiny"]["AccountKeyFile"] = account_key_path 

52 parser["acmednstiny"]["CSRFile"] = domain_csr.name 

53 parser["acmednstiny"]["ACMEDirectory"] = ACMEDIRECTORY 

54 if CONTACT: 

55 parser["acmednstiny"]["Contacts"] = "mailto:{0}".format(CONTACT) 

56 elif "Contacts" in parser: 

57 del parser["acmednstiny"]["Contacts"] 

58 parser["TSIGKeyring"]["KeyName"] = TSIGKEYNAME 

59 parser["TSIGKeyring"]["KeyValue"] = TSIGKEYVALUE 

60 parser["TSIGKeyring"]["Algorithm"] = TSIGALGORITHM 

61 parser["DNS"]["Host"] = DNSHOST 

62 parser["DNS"]["Port"] = DNSPORT 

63 parser["DNS"]["Zone"] = DNSZONE 

64 parser["DNS"]["TTL"] = DNSTTL 

65 

66 return account_key_path, domain_key.name, domain_csr.name, parser 

67 

68 

69def generate_acme_dns_tiny_unit_test_config(): 

70 """Genereate acme_dns_tiny configurations used for unit tests""" 

71 # Configuration missing DNS section 

72 _, domain_key, _, config = generate_config() 

73 os.remove(domain_key) 

74 

75 missing_dns = NamedTemporaryFile(delete=False) 

76 config["DNS"] = {} 

77 with open(missing_dns.name, 'w') as configfile: 

78 config.write(configfile) 

79 

80 return {"missing_dns": missing_dns.name} 

81 

82 

83def generate_acme_dns_tiny_config(): # pylint: disable=too-many-locals,too-many-statements 

84 """Generate acme_dns_tiny configuration with account and domain keys""" 

85 # Simple configuration with good options 

86 account_key, domain_key, _, config = generate_config() 

87 os.remove(domain_key) 

88 

89 good_cname = NamedTemporaryFile(delete=False) 

90 with open(good_cname.name, 'w') as configfile: 

91 config.write(configfile) 

92 

93 # Simple configuration with good options, without contacts field 

94 _, domain_key, _, config = generate_config(account_key) 

95 os.remove(domain_key) 

96 

97 config.remove_option("acmednstiny", "Contacts") 

98 

99 good_cname_without_contacts = NamedTemporaryFile(delete=False) 

100 with open(good_cname_without_contacts.name, 'w') as configfile: 

101 config.write(configfile) 

102 

103 # Simple configuration without CSR in configuration (will be passed as argument) 

104 _, domain_key, cname_csr, config = generate_config(account_key) 

105 os.remove(domain_key) 

106 

107 config.remove_option("acmednstiny", "CSRFile") 

108 

109 good_cname_without_csr = NamedTemporaryFile(delete=False) 

110 with open(good_cname_without_csr.name, 'w') as configfile: 

111 config.write(configfile) 

112 

113 # Configuration with CSR containing a wildcard domain 

114 _, domain_key, domain_csr, config = generate_config(account_key) 

115 

116 if IS_PEBBLE: # Pebble server enforces usage of SAN instead of CN 

117 san_conf = NamedTemporaryFile(delete=False) 

118 with open("/etc/ssl/openssl.cnf", 'r') as opensslcnf: 

119 san_conf.write(opensslcnf.read().encode("utf8")) 

120 san_conf.write("\n[SAN]\nsubjectAltName=DNS:*.{0}\n".format(DOMAIN).encode("utf8")) 

121 san_conf.seek(0) 

122 Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key, 

123 "-subj", "/", "-reqexts", "SAN", "-config", san_conf.name, 

124 "-out", domain_csr]).wait() 

125 os.remove(san_conf.name) 

126 else: 

127 Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key, 

128 "-subj", "/CN=*.{0}".format(DOMAIN), "-out", domain_csr]).wait() 

129 os.remove(domain_key) 

130 

131 wild_cname = NamedTemporaryFile(delete=False) 

132 with open(wild_cname.name, 'w') as configfile: 

133 config.write(configfile) 

134 

135 # Configuration with IP as DNS Host 

136 _, domain_key, _, config = generate_config(account_key) 

137 os.remove(domain_key) 

138 

139 config["DNS"]["Host"] = DNSHOSTIP 

140 

141 dns_host_ip = NamedTemporaryFile(delete=False) 

142 with open(dns_host_ip.name, 'w') as configfile: 

143 config.write(configfile) 

144 

145 # Configuration with CSR using subject alt-name domain instead of CN (common name) 

146 _, domain_key, domain_csr, config = generate_config(account_key) 

147 

148 san_conf = NamedTemporaryFile(delete=False) 

149 with open("/etc/ssl/openssl.cnf", 'r') as opensslcnf: 

150 san_conf.write(opensslcnf.read().encode("utf8")) 

151 san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0},DNS:www.{0}\n".format(DOMAIN).encode("utf8")) 

152 san_conf.seek(0) 

153 Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key, 

154 "-subj", "/", "-reqexts", "SAN", "-config", san_conf.name, 

155 "-out", domain_csr]).wait() 

156 os.remove(san_conf.name) 

157 os.remove(domain_key) 

158 

159 good_san = NamedTemporaryFile(delete=False) 

160 with open(good_san.name, 'w') as configfile: 

161 config.write(configfile) 

162 

163 # Configuration with CSR containing a wildcard domain inside subjetcAltName 

164 _, domain_key, domain_csr, config = generate_config(account_key) 

165 

166 wild_san_conf = NamedTemporaryFile(delete=False) 

167 with open("/etc/ssl/openssl.cnf", 'r') as opensslcnf: 

168 wild_san_conf.write(opensslcnf.read().encode("utf8")) 

169 wild_san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0},DNS:*.{0}\n" 

170 .format(DOMAIN).encode("utf8")) 

171 wild_san_conf.seek(0) 

172 Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key, 

173 "-subj", "/", "-reqexts", "SAN", "-config", wild_san_conf.name, 

174 "-out", domain_csr]).wait() 

175 os.remove(wild_san_conf.name) 

176 os.remove(domain_key) 

177 

178 wild_san = NamedTemporaryFile(delete=False) 

179 with open(wild_san.name, 'w') as configfile: 

180 config.write(configfile) 

181 

182 # Invalid TSIG key name 

183 _, domain_key, _, config = generate_config(account_key) 

184 os.remove(domain_key) 

185 

186 config["TSIGKeyring"]["KeyName"] = "{0}.invalid".format(TSIGKEYNAME) 

187 

188 invalid_tsig_name = NamedTemporaryFile(delete=False) 

189 with open(invalid_tsig_name.name, 'w') as configfile: 

190 config.write(configfile) 

191 

192 return { 

193 # configs 

194 "good_cname": good_cname.name, 

195 "good_cname_without_contacts": good_cname_without_contacts.name, 

196 "good_cname_without_csr": good_cname_without_csr.name, 

197 "wild_cname": wild_cname.name, 

198 "dns_host_ip": dns_host_ip.name, 

199 "good_san": good_san.name, 

200 "wild_san": wild_san.name, 

201 "invalid_tsig_name": invalid_tsig_name.name, 

202 # cname CSR file to use with good_cname_without_csr as argument 

203 "cname_csr": cname_csr, 

204 } 

205 

206 

207def generate_acme_account_rollover_config(): 

208 """Generate config for acme_account_rollover script""" 

209 # Old account key is directly created by the config generator 

210 old_account_key, domain_key, _, config = generate_config() 

211 os.remove(domain_key) 

212 

213 # New account key 

214 new_account_key = NamedTemporaryFile(delete=False) 

215 Popen(["openssl", "genrsa", "-out", new_account_key.name, "2048"]).wait() 

216 

217 rollover_account = NamedTemporaryFile(delete=False) 

218 with open(rollover_account.name, 'w') as configfile: 

219 config.write(configfile) 

220 

221 return { 

222 # config and keys (returned to keep files on system) 

223 "config": rollover_account.name, 

224 "old_account_key": old_account_key, 

225 "new_account_key": new_account_key.name 

226 } 

227 

228 

229def generate_acme_account_deactivate_config(): 

230 """Generate config for acme_account_deactivate script""" 

231 # Account key is created by the by the config generator 

232 account_key, domain_key, _, config = generate_config() 

233 os.remove(domain_key) 

234 

235 deactivate_account = NamedTemporaryFile(delete=False) 

236 with open(deactivate_account.name, 'w') as configfile: 

237 config.write(configfile) 

238 

239 return { 

240 "config": deactivate_account.name, 

241 "key": account_key 

242 }