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") 

11ACMETIMEOUT = os.getenv("GITLABCI_ACMETIMEOUT", "10") 

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

13DNSNAMESERVER = os.getenv("GITLABCI_DNSNAMESERVER", "") 

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

15DNSTIMEOUT = os.getenv("GITLABCI_DNSTIMEOUT", "10") 

16TSIGKEYNAME = os.getenv("GITLABCI_TSIGKEYNAME", "") 

17TSIGKEYVALUE = os.getenv("GITLABCI_TSIGKEYVALUE", "") 

18TSIGALGORITHM = os.getenv("GITLABCI_TSIGALGORITHM", "") 

19CONTACT = os.getenv("GITLABCI_CONTACT") 

20 

21 

22def generate_config(account_key_path=None): 

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

24 # Account key should be created if not given 

25 if account_key_path is None: 

26 account_key = NamedTemporaryFile(delete=False) 

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

28 account_key_path = account_key.name 

29 

30 # Domain key and CSR 

31 domain_key = NamedTemporaryFile(delete=False) 

32 domain_csr = NamedTemporaryFile(delete=False) 

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

34 san_conf = NamedTemporaryFile(delete=False) 

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

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

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

38 san_conf.seek(0) 

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

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

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

42 os.remove(san_conf.name) 

43 else: 

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

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

46 

47 # acme-dns-tiny configuration 

48 parser = configparser.ConfigParser() 

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

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

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

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

53 if CONTACT: 

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

55 elif "Contacts" in parser: 

56 del parser["acmednstiny"]["Contacts"] 

57 if ACMETIMEOUT: 

58 parser["acmednstiny"]["Timeout"] = ACMETIMEOUT 

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

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

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

62 parser["DNS"]["NameServer"] = DNSNAMESERVER 

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

64 if DNSTIMEOUT: 

65 parser["DNS"]["Timeout"] = DNSTIMEOUT 

66 

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

68 

69 

70def generate_acme_dns_tiny_unit_test_config(): 

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

72 # Configuration missing DNS section 

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

74 os.remove(domain_key) 

75 

76 missing_tsigkeyring = NamedTemporaryFile(delete=False) 

77 config["TSIGKeyring"] = {} 

78 with open(missing_tsigkeyring.name, 'w') as configfile: 

79 config.write(configfile) 

80 

81 return {"missing_tsigkeyring": missing_tsigkeyring.name} 

82 

83 

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

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

86 # Simple configuration with good options 

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

88 os.remove(domain_key) 

89 

90 good_cname = NamedTemporaryFile(delete=False) 

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

92 config.write(configfile) 

93 

94 # Simple configuration with good options, without contacts field 

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

96 os.remove(domain_key) 

97 

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

99 

100 good_cname_without_contacts = NamedTemporaryFile(delete=False) 

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

102 config.write(configfile) 

103 

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

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

106 os.remove(domain_key) 

107 

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

109 

110 good_cname_without_csr = NamedTemporaryFile(delete=False) 

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

112 config.write(configfile) 

113 

114 # Configuration with CSR containing a wildcard domain 

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

116 

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

118 san_conf = NamedTemporaryFile(delete=False) 

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

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

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

122 san_conf.seek(0) 

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

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

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

126 os.remove(san_conf.name) 

127 else: 

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

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

130 os.remove(domain_key) 

131 

132 wild_cname = NamedTemporaryFile(delete=False) 

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

134 config.write(configfile) 

135 

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

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

138 

139 san_conf = NamedTemporaryFile(delete=False) 

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

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

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

143 san_conf.seek(0) 

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

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

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

147 os.remove(san_conf.name) 

148 os.remove(domain_key) 

149 

150 good_san = NamedTemporaryFile(delete=False) 

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

152 config.write(configfile) 

153 

154 # Configuration with CSR containing a wildcard domain inside subjetcAltName 

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

156 

157 wild_san_conf = NamedTemporaryFile(delete=False) 

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

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

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

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

162 wild_san_conf.seek(0) 

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

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

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

166 os.remove(wild_san_conf.name) 

167 os.remove(domain_key) 

168 

169 wild_san = NamedTemporaryFile(delete=False) 

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

171 config.write(configfile) 

172 

173 # Invalid TSIG key name 

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

175 os.remove(domain_key) 

176 

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

178 

179 invalid_tsig_name = NamedTemporaryFile(delete=False) 

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

181 config.write(configfile) 

182 

183 return { 

184 # configs 

185 "good_cname": good_cname.name, 

186 "good_cname_without_contacts": good_cname_without_contacts.name, 

187 "good_cname_without_csr": good_cname_without_csr.name, 

188 "wild_cname": wild_cname.name, 

189 "good_san": good_san.name, 

190 "wild_san": wild_san.name, 

191 "invalid_tsig_name": invalid_tsig_name.name, 

192 # cname CSR file to use with good_cname_without_csr as argument 

193 "cname_csr": cname_csr, 

194 } 

195 

196 

197def generate_acme_account_rollover_config(): 

198 """Generate config for acme_account_rollover script""" 

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

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

201 os.remove(domain_key) 

202 

203 # New account key 

204 new_account_key = NamedTemporaryFile(delete=False) 

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

206 

207 rollover_account = NamedTemporaryFile(delete=False) 

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

209 config.write(configfile) 

210 

211 return { 

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

213 "config": rollover_account.name, 

214 "old_account_key": old_account_key, 

215 "new_account_key": new_account_key.name 

216 } 

217 

218 

219def generate_acme_account_deactivate_config(): 

220 """Generate config for acme_account_deactivate script""" 

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

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

223 os.remove(domain_key) 

224 

225 deactivate_account = NamedTemporaryFile(delete=False) 

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

227 config.write(configfile) 

228 

229 return { 

230 "config": deactivate_account.name, 

231 "key": account_key 

232 }