|
5 | 5 | import pprint |
6 | 6 | import shutil |
7 | 7 | import tempfile |
| 8 | +import zipfile |
8 | 9 | from subprocess import * |
9 | 10 |
|
10 | 11 | startdate = "20180829142316Z" |
@@ -224,6 +225,107 @@ def print_cert(path): |
224 | 225 | pprint.pprint(_ssl._test_decode_cert(path)) |
225 | 226 |
|
226 | 227 |
|
| 228 | +# The distrusted test material (compromised eMudhra CA certs and a leaf signed |
| 229 | +# by them) lives in a password-protected zip so it doesn't trip security |
| 230 | +# scanners that cannot tell internal test fixtures from production secrets and |
| 231 | +# flag any certificate or private key they find. It is encrypted, not merely |
| 232 | +# zipped, because such scanners read inside plain archives. The password is |
| 233 | +# fixed, public, and not a secret. |
| 234 | +DISTRUSTED_ZIP = 'distrusted.zip' |
| 235 | +DISTRUSTED_ZIP_DIR = 'distrusted' |
| 236 | +DISTRUSTED_ZIP_CHOSEN_KNIGHT = 'Brave Sir Robin' |
| 237 | + |
| 238 | +# ASN.1 template for the intermediate's compromised key: secp384r1 scalar 4. |
| 239 | +# openssl derives the public key when the SEC1 structure omits it. |
| 240 | +distrusted_key_template = """ |
| 241 | + asn1 = SEQUENCE:eckey |
| 242 | + [eckey] |
| 243 | + version = INTEGER:1 |
| 244 | + priv = FORMAT:HEX,OCTETSTRING:{scalar} |
| 245 | + params = EXPLICIT:0,OID:secp384r1 |
| 246 | + """ |
| 247 | + |
| 248 | + |
| 249 | +def make_distrusted_spki_cert(): |
| 250 | + # leaf signed by the distrusted intermediate, chains to the distrusted root |
| 251 | + print("creating distrusted-SPKI test cert") |
| 252 | + # The standard library can read but not write encrypted zip files, so we |
| 253 | + # need the Info-ZIP "zip" command to repackage the archive. |
| 254 | + if shutil.which('zip') is None: |
| 255 | + raise SystemExit( |
| 256 | + f"the 'zip' command is required to write the encrypted {DISTRUSTED_ZIP} " |
| 257 | + "(the zipfile module cannot create encrypted archives); " |
| 258 | + "please install Info-ZIP's zip utility" |
| 259 | + ) |
| 260 | + |
| 261 | + # the root and intermediate are real certs we cannot re-mint; carry them |
| 262 | + # over from the existing archive |
| 263 | + pw = DISTRUSTED_ZIP_CHOSEN_KNIGHT.encode() |
| 264 | + with zipfile.ZipFile(DISTRUSTED_ZIP) as z: |
| 265 | + root = z.read(DISTRUSTED_ZIP_DIR + '/distrusted_root.pem', pw) |
| 266 | + intermediate = z.read(DISTRUSTED_ZIP_DIR + '/distrusted_intermediate.pem', pw) |
| 267 | + |
| 268 | + with tempfile.TemporaryDirectory() as workdir: |
| 269 | + stage = os.path.join(workdir, DISTRUSTED_ZIP_DIR) |
| 270 | + os.mkdir(stage) |
| 271 | + req_file = os.path.join(workdir, 'req.cnf') |
| 272 | + key_file = os.path.join(workdir, 'leaf.key') |
| 273 | + csr_file = os.path.join(workdir, 'leaf.csr') |
| 274 | + cert_file = os.path.join(workdir, 'leaf.pem') |
| 275 | + cakey_conf = os.path.join(workdir, 'cakey.cnf') |
| 276 | + cakey_der = os.path.join(workdir, 'cakey.der') |
| 277 | + cakey_file = os.path.join(workdir, 'cakey.pem') |
| 278 | + |
| 279 | + inter_file = os.path.join(stage, 'distrusted_intermediate.pem') |
| 280 | + with open(os.path.join(stage, 'distrusted_root.pem'), 'wb') as f: |
| 281 | + f.write(root) |
| 282 | + with open(inter_file, 'wb') as f: |
| 283 | + f.write(intermediate) |
| 284 | + |
| 285 | + # recreate the intermediate's key (secp384r1 scalar 4) on the fly |
| 286 | + with open(cakey_conf, 'w') as f: |
| 287 | + f.write(distrusted_key_template.format(scalar='00' * 47 + '04')) |
| 288 | + check_call(['openssl', 'asn1parse', '-genconf', cakey_conf, |
| 289 | + '-out', cakey_der, '-noout']) |
| 290 | + check_call(['openssl', 'pkey', '-in', cakey_der, |
| 291 | + '-inform', 'DER', '-out', cakey_file]) |
| 292 | + |
| 293 | + req = req_template.format( |
| 294 | + hostname='localhost', |
| 295 | + extra_san='', |
| 296 | + startdate=startdate, |
| 297 | + enddate=enddate |
| 298 | + ) |
| 299 | + with open(req_file, 'w') as f: |
| 300 | + f.write(req) |
| 301 | + # ECC leaf to match the ECC intermediate |
| 302 | + check_call(['openssl', 'req', '-new', '-nodes', '-days', '7000', |
| 303 | + '-newkey', 'param:secp384r1.pem', '-keyout', key_file, |
| 304 | + '-config', req_file, '-out', csr_file]) |
| 305 | + # sign with the distrusted intermediate, not the test CA |
| 306 | + check_call(['openssl', 'ca', '-config', req_file, |
| 307 | + '-extensions', 'req_x509_extensions_full', |
| 308 | + '-cert', inter_file, '-keyfile', cakey_file, |
| 309 | + '-out', cert_file, '-outdir', 'cadir', |
| 310 | + '-policy', 'policy_anything', '-batch', '-infiles', csr_file]) |
| 311 | + with open(key_file) as f: |
| 312 | + key = f.read() |
| 313 | + with open(cert_file) as f: |
| 314 | + cert = f.read() |
| 315 | + # server sends leaf + intermediate, client trusts the root |
| 316 | + with open(os.path.join(stage, 'distrusted_keycert.pem'), 'w') as f: |
| 317 | + f.write(key) |
| 318 | + f.write(cert) |
| 319 | + f.write(intermediate.decode()) |
| 320 | + |
| 321 | + # repackage the encrypted archive (zipfile cannot write encryption) |
| 322 | + zippath = os.path.join(here, DISTRUSTED_ZIP) |
| 323 | + if os.path.exists(zippath): |
| 324 | + os.remove(zippath) |
| 325 | + check_call(['zip', '-q', '-X', '-P', DISTRUSTED_ZIP_CHOSEN_KNIGHT, '-r', |
| 326 | + zippath, DISTRUSTED_ZIP_DIR], cwd=workdir) |
| 327 | + |
| 328 | + |
227 | 329 | if __name__ == '__main__': |
228 | 330 | os.chdir(here) |
229 | 331 | cert, key = make_cert_key('localhost', ext='req_x509_extensions_simple') |
@@ -309,6 +411,8 @@ def print_cert(path): |
309 | 411 | f.write(key) |
310 | 412 | f.write(cert) |
311 | 413 |
|
| 414 | + make_distrusted_spki_cert() |
| 415 | + |
312 | 416 | unmake_ca() |
313 | 417 | print("update Lib/test/test_ssl.py and Lib/test/test_asyncio/utils.py") |
314 | 418 | print_cert('keycert.pem') |
|
0 commit comments