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

Side by Side Diff: net/data/verify_certificate_chain_unittest/common.py

Issue 1414393008: Add scripts to generate simple test data for certificate verification. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@key_usages
Patch Set: Created 5 years, 1 month 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 #!/usr/bin/python
2 # Copyright (c) 2015 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Set of helpers to generate signed X.509 certificates.
7
8 This works by shelling out calls to the 'openssl req' and 'openssl ca'
9 commands, and passing the appropriate command line flags and configuration file
10 (.cnf).
11 """
12
13 import base64
14 import os
15 import shutil
16 import subprocess
17 import sys
18
19 sys.path.insert(0, os.path.dirname(__file__))
mattm 2015/10/29 01:47:18 This is unnecessary, can import files in the same
eroman 2015/10/31 00:34:25 Done.
20 import openssl_conf
21
22 # Enum for the "type" of certificate that is to be created. This is used to
23 # select sane defaults for the .cnf file and command line flags, but they can
24 # all be overridden.
25 TYPE_CA = 2
26 TYPE_END_ENTITY = 3
27
28 # January 1st, 2015 midnight UTC
29 JANUARY_1_2015_UTC = '150101120000Z'
30
31 # January 1st, 2016 midnight UTC
32 JANUARY_1_2016_UTC = '160101120000Z'
33
34 # March 2nd, 2015 midnight UTC
35 DEFAULT_TIME = '150302120000Z'
36
37 g_next_path_id = 1
38
39 # Base directory into which all temporary output files will be saved.
40 g_out_dir = None
41
42 g_invoking_script_name = None
mattm 2015/10/29 01:47:18 You could use sys.argv[0] to get it instead of hav
eroman 2015/10/31 00:34:25 Done (good idea, simplifies the boilerplate for ea
43
44
45 def GetUniquePathId(name):
46 """Returns a base filename that contains 'name', but is unique to the output
47 directory"""
48 global g_next_path_id
49 path_id = '%s_%d' % (name, g_next_path_id)
50 g_next_path_id += 1
51 return path_id
52
53
54 class Certificate(object):
55 """Helper for building an X.509 certificate."""
56
57 def __init__(self, name, cert_type, issuer):
58 # The name will be used for the subject's CN, and also as a component of
59 # the temporary filenames to help with debugging.
60 self.name = name
61 self.path_id = GetUniquePathId(name)
62
63 # The issuer is also a Certificate object. Passing |None| means it is a
64 # self-signed certificate.
65 self.issuer = issuer
66 if issuer is None:
67 self.issuer = self
68
69 # The config contains all the OpenSSL options that will be passed via a
70 # .cnf file. Set up defaults.
71 self.config = openssl_conf.Config()
72 self.InitConfig()
73
74 # Some settings need to be passed as flags rather than in the .cnf file.
75 # Technically these can be set though a .cnf, however doing so makes it
76 # sticky to the issuing certificate, rather than selecting it per
77 # subordinate certificate.
78 self.validity_flags = []
79 self.md_flags = []
80
81 # By default OpenSSL will use the current time for the start time. Instead
82 # default to using a fixed timestamp for more predictabl results each time
83 # the certificates are re-generated.
84 self.SetValidityRange(JANUARY_1_2015_UTC, JANUARY_1_2016_UTC)
85
86 # Use SHA-256 when THIS certificate is signed (setting it in the
87 # configuration would instead set the hash to use when signing other
88 # certificates with this one).
89 self.SetSignatureHash('sha256')
90
91 # Set appropriate key usages and basic constraints. For flexibility in
92 # testing (since want to generate some flawed certificates) these are set
93 # on a per-certificate basis rather than automatically when signing.
94 if cert_type == TYPE_END_ENTITY:
95 self.GetExtensions().SetProperty('keyUsage',
96 'critical,digitalSignature,keyEncipherment')
97 self.GetExtensions().SetProperty('extendedKeyUsage',
98 'serverAuth,clientAuth')
99 else:
100 self.GetExtensions().SetProperty('keyUsage',
101 'critical,keyCertSign,cRLSign')
102 self.GetExtensions().SetProperty('basicConstraints', 'critical,CA:true')
103
104 # Tracks whether the PEM file for this certificate has been written (since
105 # generation is done lazily).
106 self.finalized = False
107
108 # Initialize any files that will be needed if this certificate is used to
109 # sign other certificates. Starts off serial numbers at 1, and will
110 # increment them for each signed certificate.
111 WriteStringToFile('01\n', self.GetSerialPath())
112 WriteStringToFile('', self.GetDatabasePath())
113
114
115 def GenerateRsaKey(self, size_bits):
116 """Generates an RSA private key for the certificate."""
117 subprocess.check_call(
118 ['openssl', 'genrsa', '-out', self.GetKeyPath(), str(size_bits)])
119
120
121 def GenerateEcKey(self, named_curve):
122 """Generates an EC private key for the certificate. |named_curve| can be
123 something like secp384r1"""
124 subprocess.check_call(
125 ['openssl', 'ecparam', '-out', self.GetKeyPath(), '-name', named_curve,
126 '-genkey'])
127
128
129 def SetValidityRange(self, start_date, end_date):
130 """Sets the Validity notBefore and notAfter properties for the
131 certificate"""
132 self.validity_flags = ['-startdate', start_date, '-enddate', end_date]
133
134
135 def SetSignatureHash(self, md):
136 """Sets the hash function that will be used when signing this certificate.
137 Can be sha1, sha256, sha512, md5, etc."""
138 self.md_flags = ['-md', md]
139
140
141 def GetExtensions(self):
142 return self.config.GetSection('req_ext')
143
144
145 def GetPath(self, suffix):
146 """Forms a path to an output file for this certificate, containing the
147 indicated suffix. The certificate's name will be used as its basis."""
148 return os.path.join(g_out_dir, '%s%s' % (self.path_id, suffix))
149
150
151 def GetKeyPath(self):
152 return self.GetPath('.key')
153
154
155 def GetCertPath(self):
156 return self.GetPath('.pem')
157
158
159 def GetSerialPath(self):
160 return self.GetPath('.serial')
161
162
163 def GetCsrPath(self):
164 return self.GetPath('.csr')
165
166
167 def GetDatabasePath(self):
168 return self.GetPath('.db')
169
170
171 def GetConfigPath(self):
172 return self.GetPath('.cnf')
173
174
175 def GetCertPem(self):
176 # Finish generating a .pem file for the certificate.
177 self.Finalize()
178
179 # Read the certificate data.
180 with open(self.GetCertPath(), 'r') as f:
181 return f.read()
182
183
184 def Finalize(self):
185 """Finishes the certificate creation process. This generates any needed
186 key, creates and signs the CSR. On completion the resulting PEM file can be
187 found at self.GetCertPath()"""
188
189 if self.finalized:
190 return # Already finalized, no work needed.
191
192 self.finalized = True
193
194 # Ensure that the issuer has been "finalized", since its outputs need to be
195 # accessible. Note that self.issuer could be the same as self.
196 self.issuer.Finalize()
197
198 # Ensure the certificate has a key. Callers have the option to generate a
199 # different type of key, but if that was not done default to a new 2048-bit
200 # RSA key.
201 if not os.path.isfile(self.GetKeyPath()):
202 self.GenerateRsaKey(2048)
203
204
mattm 2015/10/29 01:47:17 nit: extra line
eroman 2015/10/31 00:34:25 Done.
205 # Serialize the config to a file.
206 self.config.WriteToFile(self.GetConfigPath())
207
208 # Create a CSR.
209 subprocess.check_call(
210 ['openssl', 'req', '-new',
211 '-key', self.GetKeyPath(),
212 '-out', self.GetCsrPath(),
213 '-config', self.GetConfigPath()])
214
215 cmd = ['openssl', 'ca', '-batch', '-in',
216 self.GetCsrPath(), '-out', self.GetCertPath(), '-config',
217 self.issuer.GetConfigPath()]
218
219 if self.issuer == self:
220 cmd.append('-selfsign')
221
222 # Add in any extra flags.
223 cmd.extend(self.validity_flags)
224 cmd.extend(self.md_flags)
225
226 # Run the 'openssl ca' command.
227 subprocess.check_call(cmd)
228
229
230 def InitConfig(self):
231 """Initializes default properties in the certificate .cnf file that are
232 generic enough to work for all certificates (but can be overridden later).
233 """
234
235 # --------------------------------------
236 # 'req' section
237 # --------------------------------------
238
239 section = self.config.GetSection('req')
240
241 section.SetProperty('encrypt_key', 'no')
242 section.SetProperty('utf8', 'yes')
243 section.SetProperty('string_mask', 'utf8only')
244 section.SetProperty('prompt', 'no')
245 section.SetProperty('distinguished_name', 'req_dn')
246 section.SetProperty('req_extensions', 'req_ext')
247
248 # --------------------------------------
249 # 'req_dn' section
250 # --------------------------------------
251
252 # This section describes the certificate subject's distinguished name.
253
254 section = self.config.GetSection('req_dn')
255 section.SetProperty('commonName', '"%s"' % (self.name))
256
257 # --------------------------------------
258 # 'req_ext' section
259 # --------------------------------------
260
261 # This section describes the certificate's extensions.
262
263 section = self.config.GetSection('req_ext')
264 section.SetProperty('subjectKeyIdentifier', 'hash')
265
266 # --------------------------------------
267 # SECTIONS FOR CAs
268 # --------------------------------------
269
270 # The following sections are used by the 'openssl ca' and relate to the
271 # signing operation. They are not needed for end-entity certificate
272 # configurations, but only if this certifiate will be used to sign other
273 # certificates.
274
275 # --------------------------------------
276 # 'ca' section
277 # --------------------------------------
278
279 section = self.config.GetSection('ca')
280 section.SetProperty('default_ca', 'root_ca')
281
282 section = self.config.GetSection('root_ca')
283 section.SetProperty('certificate', self.GetCertPath())
284 section.SetProperty('private_key', self.GetKeyPath())
285 section.SetProperty('new_certs_dir', g_out_dir)
286 section.SetProperty('serial', self.GetSerialPath())
287 section.SetProperty('database', self.GetDatabasePath())
288 section.SetProperty('unique_subject', 'no')
289
290 # These will get overridden via command line flags.
291 section.SetProperty('default_days', '365')
292 section.SetProperty('default_md', 'sha256')
293
294 section.SetProperty('policy', 'policy_anything')
295 section.SetProperty('email_in_dn', 'no')
296 section.SetProperty('preserve', 'yes')
297 section.SetProperty('name_opt', 'multiline,-esc_msb,utf8')
298 section.SetProperty('cert_opt', 'ca_default')
299 section.SetProperty('copy_extensions', 'copy')
300 section.SetProperty('x509_extensions', 'signing_ca_ext')
301 section.SetProperty('default_crl_days', '30')
302 section.SetProperty('crl_extensions', 'crl_ext')
303
304 section = self.config.GetSection('policy_anything')
305 section.SetProperty('domainComponent', 'optional')
306 section.SetProperty('countryName', 'optional')
307 section.SetProperty('stateOrProvinceName', 'optional')
308 section.SetProperty('localityName', 'optional')
309 section.SetProperty('organizationName', 'optional')
310 section.SetProperty('organizationalUnitName', 'optional')
311 section.SetProperty('commonName', 'optional')
312 section.SetProperty('emailAddress', 'optional')
313
314 section = self.config.GetSection('signing_ca_ext')
315 section.SetProperty('subjectKeyIdentifier', 'hash')
316 section.SetProperty('authorityKeyIdentifier', 'keyid:always')
317 section.SetProperty('authorityInfoAccess', '@issuer_info')
318 section.SetProperty('crlDistributionPoints', '@crl_info')
319
320 section = self.config.GetSection('issuer_info')
321 section.SetProperty('caIssuers;URI.0',
322 'http://url-for-aia/%s.cer' % (self.name))
323
324 section = self.config.GetSection('crl_info')
325 section.SetProperty('URI.0', 'http://url-for-crl/%s.crl' % (self.name))
326
327 section = self.config.GetSection('crl_ext')
328 section.SetProperty('authorityKeyIdentifier', 'keyid:always')
329 section.SetProperty('authorityInfoAccess', '@issuer_info')
330
331
332 def DataToPem(block_header, block_data):
333 return '-----BEGIN %s-----\n%s\n-----END %s-----\n' % (block_header,
334 base64.b64encode(block_data), block_header)
335
336
337 def WriteTestFile(description, chain, trusted_certs, utc_time, verify_result):
338 """Writes a test file that contains all the inputs necessary to run a
339 verification on a certificate chain"""
340
341 # Prepend the script name that generated the file to the description.
342 test_data = '[Created by: %s.py]\n\n%s\n' % (g_invoking_script_name,
343 description)
344
345 # Write the certificate chain to the output file.
346 for cert in chain:
347 test_data += '\n' + cert.GetCertPem()
348
349 # Write the trust store.
350 for cert in trusted_certs:
351 cert_data = cert.GetCertPem()
352 # Use a different block type in the .pem file.
353 cert_data = cert_data.replace('CERTIFICATE', 'TRUSTED_CERTIFICATE')
354 test_data += '\n' + cert_data
355
356 test_data += '\n' + DataToPem('TIME', utc_time)
357
358 if verify_result:
359 verify_result = 'SUCCESS'
mattm 2015/10/29 01:47:17 I found it confusing to overwrite the original arg
eroman 2015/10/31 00:34:24 Done (I didn't know that is how you did tertiary o
360 else:
361 verify_result = 'FAIL'
362
363 test_data += '\n' + DataToPem('VERIFY_RESULT', verify_result)
364
365 # Save the chain using the same name as the script that generated it (but
366 # with the 'generate-' prefix stripped).
367 stripped_script_name = g_invoking_script_name
368 parts = g_invoking_script_name.split('generate-', 1)
369 if len(parts) > 0:
mattm 2015/10/29 01:47:17 should be > 1
eroman 2015/10/31 00:34:25 No longer relevant (in fact I merged this logic in
370 stripped_script_name = parts[1]
mattm 2015/10/29 01:47:18 Or you could just do: stripped_script_name = g_inv
eroman 2015/10/31 00:34:25 I opted for this: # Strip the leading 'generate
371
372 final_output_path = os.path.join('%s.pem' % (stripped_script_name))
373
374 WriteStringToFile(test_data, final_output_path)
375
376
377 def WriteStringToFile(data, path):
378 with open(path, 'w') as f:
379 f.write(data)
380
381
382 def Init(invoking_script_path):
383 """Creates an output directory for all the temporary files generated by the
384 calling script.
385 """
386
387 global g_out_dir
388 global g_invoking_script_name
389
390 g_invoking_script_name = os.path.splitext(
391 os.path.basename(invoking_script_path))[0]
392
393 # Ensure that out/ exists
394 if not os.path.exists('out'):
395 os.makedirs('out')
mattm 2015/10/29 01:47:17 no need for os.makedirs here, use os.mkdir? Actua
eroman 2015/10/31 00:34:25 Done (just keeping the on call to mkdirs)
396
397 # Use an output directory with the same name as the invoking script.
398 g_out_dir = os.path.join('out', g_invoking_script_name)
399 sys.stdout.write('Creating output directory: %s\n' % (g_out_dir))
400
401 # Ensure the output directory is empty.
402 shutil.rmtree(g_out_dir, True)
403 os.makedirs(g_out_dir)
404
405
406 def CreateSelfSignedRootCertificate(name):
407 return Certificate(name, TYPE_CA, None)
408
409
410 def CreateIntermediaryCertificate(name, issuer):
411 return Certificate(name, TYPE_CA, issuer)
412
413
414 def CreateEndEntityCertificate(name, issuer):
415 return Certificate(name, TYPE_END_ENTITY, issuer)
416
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698