Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(37)

Side by Side Diff: Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/sslutils/openssl.py

Issue 1154373005: Introduce WPTServe for running W3C Blink Layout tests (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: Add executable bit to pass permchecks. Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 import functools
2 import os
3 import shutil
4 import subprocess
5 import tempfile
6 from datetime import datetime
7
8 class OpenSSL(object):
9 def __init__(self, logger, binary, base_path, conf_path, hosts, duration,
10 base_conf_path=None):
11 """Context manager for interacting with OpenSSL.
12 Creates a config file for the duration of the context.
13
14 :param logger: stdlib logger or python structured logger
15 :param binary: path to openssl binary
16 :param base_path: path to directory for storing certificates
17 :param conf_path: path for configuration file storing configuration data
18 :param hosts: list of hosts to include in configuration (or None if not
19 generating host certificates)
20 :param duration: Certificate duration in days"""
21
22 self.base_path = base_path
23 self.binary = binary
24 self.conf_path = conf_path
25 self.base_conf_path = base_conf_path
26 self.logger = logger
27 self.proc = None
28 self.cmd = []
29 self.hosts = hosts
30 self.duration = duration
31
32 def __enter__(self):
33 with open(self.conf_path, "w") as f:
34 f.write(get_config(self.base_path, self.hosts, self.duration))
35 return self
36
37 def __exit__(self, *args, **kwargs):
38 os.unlink(self.conf_path)
39
40 def log(self, line):
41 if hasattr(self.logger, "process_output"):
42 self.logger.process_output(self.proc.pid if self.proc is not None el se None,
43 line.decode("utf8", "replace"),
44 command=" ".join(self.cmd))
45 else:
46 self.logger.debug(line)
47
48 def __call__(self, cmd, *args, **kwargs):
49 """Run a command using OpenSSL in the current context.
50
51 :param cmd: The openssl subcommand to run
52 :param *args: Additional arguments to pass to the command
53 """
54 self.cmd = [self.binary, cmd]
55 if cmd != "x509":
56 self.cmd += ["-config", self.conf_path]
57 self.cmd += list(args)
58
59 env = os.environ.copy()
60 if self.base_conf_path is not None:
61 env["OPENSSL_CONF"] = self.base_conf_path.encode("utf8")
62
63 self.proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=su bprocess.STDOUT,
64 env=env)
65 stdout, stderr = self.proc.communicate()
66 self.log(stdout)
67 if self.proc.returncode != 0:
68 raise subprocess.CalledProcessError(self.proc.returncode, self.cmd,
69 output=stdout)
70
71 self.cmd = []
72 self.proc = None
73 return stdout
74
75
76 def make_subject(common_name,
77 country=None,
78 state=None,
79 locality=None,
80 organization=None,
81 organization_unit=None):
82 args = [("country", "C"),
83 ("state", "ST"),
84 ("locality", "L"),
85 ("organization", "O"),
86 ("organization_unit", "OU"),
87 ("common_name", "CN")]
88
89 rv = []
90
91 for var, key in args:
92 value = locals()[var]
93 if value is not None:
94 rv.append("/%s=%s" % (key, value.replace("/", "\\/")))
95
96 return "".join(rv)
97
98 def make_alt_names(hosts):
99 rv = []
100 for name in hosts:
101 rv.append("DNS:%s" % name)
102 return ",".join(rv)
103
104 def get_config(root_dir, hosts, duration=30):
105 if hosts is None:
106 san_line = ""
107 else:
108 san_line = "subjectAltName = %s" % make_alt_names(hosts)
109
110 if os.path.sep == "\\":
111 # This seems to be needed for the Shining Light OpenSSL on
112 # Windows, at least.
113 root_dir = root_dir.replace("\\", "\\\\")
114
115 rv = """[ ca ]
116 default_ca = CA_default
117
118 [ CA_default ]
119 dir = %(root_dir)s
120 certs = $dir
121 new_certs_dir = $certs
122 crl_dir = $dir%(sep)scrl
123 database = $dir%(sep)sindex.txt
124 private_key = $dir%(sep)scakey.pem
125 certificate = $dir%(sep)scacert.pem
126 serial = $dir%(sep)sserial
127 crldir = $dir%(sep)scrl
128 crlnumber = $dir%(sep)scrlnumber
129 crl = $crldir%(sep)scrl.pem
130 RANDFILE = $dir%(sep)sprivate%(sep)s.rand
131 x509_extensions = usr_cert
132 name_opt = ca_default
133 cert_opt = ca_default
134 default_days = %(duration)d
135 default_crl_days = %(duration)d
136 default_md = sha256
137 preserve = no
138 policy = policy_anything
139 copy_extensions = copy
140
141 [ policy_anything ]
142 countryName = optional
143 stateOrProvinceName = optional
144 localityName = optional
145 organizationName = optional
146 organizationalUnitName = optional
147 commonName = supplied
148 emailAddress = optional
149
150 [ req ]
151 default_bits = 2048
152 default_keyfile = privkey.pem
153 distinguished_name = req_distinguished_name
154 attributes = req_attributes
155 x509_extensions = v3_ca
156
157 # Passwords for private keys if not present they will be prompted for
158 # input_password = secret
159 # output_password = secret
160 string_mask = utf8only
161 req_extensions = v3_req
162
163 [ req_distinguished_name ]
164 countryName = Country Name (2 letter code)
165 countryName_default = AU
166 countryName_min = 2
167 countryName_max = 2
168 stateOrProvinceName = State or Province Name (full name)
169 stateOrProvinceName_default =
170 localityName = Locality Name (eg, city)
171 0.organizationName = Organization Name
172 0.organizationName_default = Web Platform Tests
173 organizationalUnitName = Organizational Unit Name (eg, section)
174 #organizationalUnitName_default =
175 commonName = Common Name (e.g. server FQDN or YOUR name)
176 commonName_max = 64
177 emailAddress = Email Address
178 emailAddress_max = 64
179
180 [ req_attributes ]
181
182 [ usr_cert ]
183 basicConstraints=CA:false
184 subjectKeyIdentifier=hash
185 authorityKeyIdentifier=keyid,issuer
186
187 [ v3_req ]
188 basicConstraints = CA:FALSE
189 keyUsage = nonRepudiation, digitalSignature, keyEncipherment
190 extendedKeyUsage = serverAuth
191 %(san_line)s
192
193 [ v3_ca ]
194 basicConstraints = CA:true
195 subjectKeyIdentifier=hash
196 authorityKeyIdentifier=keyid:always,issuer:always
197 keyUsage = keyCertSign
198 """ % {"root_dir": root_dir,
199 "san_line": san_line,
200 "duration": duration,
201 "sep": os.path.sep.replace("\\", "\\\\")}
202
203 return rv
204
205 class OpenSSLEnvironment(object):
206 ssl_enabled = True
207
208 def __init__(self, logger, openssl_binary="openssl", base_path=None,
209 password="web-platform-tests", force_regenerate=False,
210 duration=30, base_conf_path=None):
211 """SSL environment that creates a local CA and host certificate using Op enSSL.
212
213 By default this will look in base_path for existing certificates that ar e still
214 valid and only create new certificates if there aren't any. This behavio ur can
215 be adjusted using the force_regenerate option.
216
217 :param logger: a stdlib logging compatible logger or mozlog structured l ogger
218 :param openssl_binary: Path to the OpenSSL binary
219 :param base_path: Path in which certificates will be stored. If None, a temporary
220 directory will be used and removed when the server shu ts down
221 :param password: Password to use
222 :param force_regenerate: Always create a new certificate even if one alr eady exists.
223 """
224 self.logger = logger
225
226 self.temporary = False
227 if base_path is None:
228 base_path = tempfile.mkdtemp()
229 self.temporary = True
230
231 self.base_path = os.path.abspath(base_path)
232 self.password = password
233 self.force_regenerate = force_regenerate
234 self.duration = duration
235 self.base_conf_path = base_conf_path
236
237 self.path = None
238 self.binary = openssl_binary
239 self.openssl = None
240
241 self._ca_cert_path = None
242 self._ca_key_path = None
243 self.host_certificates = {}
244
245 def __enter__(self):
246 if not os.path.exists(self.base_path):
247 os.makedirs(self.base_path)
248
249 path = functools.partial(os.path.join, self.base_path)
250
251 with open(path("index.txt"), "w"):
252 pass
253 with open(path("serial"), "w") as f:
254 f.write("01")
255
256 self.path = path
257
258 return self
259
260 def __exit__(self, *args, **kwargs):
261 if self.temporary:
262 shutil.rmtree(self.base_path)
263
264 def _config_openssl(self, hosts):
265 conf_path = self.path("openssl.cfg")
266 return OpenSSL(self.logger, self.binary, self.base_path, conf_path, host s,
267 self.duration, self.base_conf_path)
268
269 def ca_cert_path(self):
270 """Get the path to the CA certificate file, generating a
271 new one if needed"""
272 if self._ca_cert_path is None and not self.force_regenerate:
273 self._load_ca_cert()
274 if self._ca_cert_path is None:
275 self._generate_ca()
276 return self._ca_cert_path
277
278 def _load_ca_cert(self):
279 key_path = self.path("cakey.pem")
280 cert_path = self.path("cacert.pem")
281
282 if self.check_key_cert(key_path, cert_path, None):
283 self.logger.info("Using existing CA cert")
284 self._ca_key_path, self._ca_cert_path = key_path, cert_path
285
286 def check_key_cert(self, key_path, cert_path, hosts):
287 """Check that a key and cert file exist and are valid"""
288 if not os.path.exists(key_path) or not os.path.exists(cert_path):
289 return False
290
291 with self._config_openssl(hosts) as openssl:
292 end_date_str = openssl("x509",
293 "-noout",
294 "-enddate",
295 "-in", cert_path).split("=", 1)[1].strip()
296 # Not sure if this works in other locales
297 end_date = datetime.strptime(end_date_str, "%b %d %H:%M:%S %Y %Z")
298 # Should have some buffer here e.g. 1 hr
299 if end_date < datetime.now():
300 return False
301
302 #TODO: check the key actually signed the cert.
303 return True
304
305 def _generate_ca(self):
306 path = self.path
307 self.logger.info("Generating new CA in %s" % self.base_path)
308
309 key_path = path("cakey.pem")
310 req_path = path("careq.pem")
311 cert_path = path("cacert.pem")
312
313 with self._config_openssl(None) as openssl:
314 openssl("req",
315 "-batch",
316 "-new",
317 "-newkey", "rsa:2048",
318 "-keyout", key_path,
319 "-out", req_path,
320 "-subj", make_subject("web-platform-tests"),
321 "-passout", "pass:%s" % self.password)
322
323 openssl("ca",
324 "-batch",
325 "-create_serial",
326 "-keyfile", key_path,
327 "-passin", "pass:%s" % self.password,
328 "-selfsign",
329 "-extensions", "v3_ca",
330 "-in", req_path,
331 "-out", cert_path)
332
333 os.unlink(req_path)
334
335 self._ca_key_path, self._ca_cert_path = key_path, cert_path
336
337 def host_cert_path(self, hosts):
338 """Get a tuple of (private key path, certificate path) for a host,
339 generating new ones if necessary.
340
341 hosts must be a list of all hosts to appear on the certificate, with
342 the primary hostname first."""
343 hosts = tuple(hosts)
344 if hosts not in self.host_certificates:
345 if not self.force_regenerate:
346 key_cert = self._load_host_cert(hosts)
347 else:
348 key_cert = None
349 if key_cert is None:
350 key, cert = self._generate_host_cert(hosts)
351 else:
352 key, cert = key_cert
353 self.host_certificates[hosts] = key, cert
354
355 return self.host_certificates[hosts]
356
357 def _load_host_cert(self, hosts):
358 host = hosts[0]
359 key_path = self.path("%s.key" % host)
360 cert_path = self.path("%s.pem" % host)
361
362 # TODO: check that this cert was signed by the CA cert
363 if self.check_key_cert(key_path, cert_path, hosts):
364 self.logger.info("Using existing host cert")
365 return key_path, cert_path
366
367 def _generate_host_cert(self, hosts):
368 host = hosts[0]
369 if self._ca_key_path is None:
370 self._generate_ca()
371 ca_key_path = self._ca_key_path
372
373 assert os.path.exists(ca_key_path)
374
375 path = self.path
376
377 req_path = path("wpt.req")
378 cert_path = path("%s.pem" % host)
379 key_path = path("%s.key" % host)
380
381 self.logger.info("Generating new host cert")
382
383 with self._config_openssl(hosts) as openssl:
384 openssl("req",
385 "-batch",
386 "-newkey", "rsa:2048",
387 "-keyout", key_path,
388 "-in", ca_key_path,
389 "-nodes",
390 "-out", req_path)
391
392 openssl("ca",
393 "-batch",
394 "-in", req_path,
395 "-passin", "pass:%s" % self.password,
396 "-subj", make_subject(host),
397 "-out", cert_path)
398
399 os.unlink(req_path)
400
401 return key_path, cert_path
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698