mirror of
https://github.com/python/cpython
synced 2026-11-06 09:02:40 +03:00
The ssl module now rejects TLS certificate chains that contain any of a set of
compromised eMudhra intermediate CA keys. This is an extraordinary measure and
a last line of defense: removing trust is primarily the job of the platform root
store, and the built-in denylist only blocks connections while that update has
not yet reached the OS, a container image, or a bundled trust store in various
Python packages. Entries are added to the denylist only under a very high
threshold.
The denylist matches on the public key (the SHA-256 digest of the RFC-5280
SubjectPublicKeyInfo), so it also covers the cross-signed certificates that
reuse one of the distrusted keys.
(cherry picked from commit 7dd8b4736a)
Co-authored-by: Christian Heimes <christian@python.org>
419 lines
14 KiB
Python
419 lines
14 KiB
Python
"""Make the custom certificate and private key files used by test_ssl
|
|
and friends."""
|
|
|
|
import os
|
|
import pprint
|
|
import shutil
|
|
import tempfile
|
|
import zipfile
|
|
from subprocess import *
|
|
|
|
startdate = "20180829142316Z"
|
|
enddate = "20371028142316Z"
|
|
|
|
req_template = """
|
|
[ default ]
|
|
base_url = http://testca.pythontest.net/testca
|
|
|
|
[req]
|
|
distinguished_name = req_distinguished_name
|
|
prompt = no
|
|
|
|
[req_distinguished_name]
|
|
C = XY
|
|
L = Castle Anthrax
|
|
O = Python Software Foundation
|
|
CN = {hostname}
|
|
|
|
[req_x509_extensions_nosan]
|
|
|
|
[req_x509_extensions_simple]
|
|
subjectAltName = @san
|
|
|
|
[req_x509_extensions_full]
|
|
subjectAltName = @san
|
|
keyUsage = critical,keyEncipherment,digitalSignature
|
|
extendedKeyUsage = serverAuth,clientAuth
|
|
basicConstraints = critical,CA:false
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer:always
|
|
authorityInfoAccess = @issuer_ocsp_info
|
|
crlDistributionPoints = @crl_info
|
|
|
|
[ issuer_ocsp_info ]
|
|
caIssuers;URI.0 = $base_url/pycacert.cer
|
|
OCSP;URI.0 = $base_url/ocsp/
|
|
|
|
[ crl_info ]
|
|
URI.0 = $base_url/revocation.crl
|
|
|
|
[san]
|
|
DNS.1 = {hostname}
|
|
{extra_san}
|
|
|
|
[dir_sect]
|
|
C = XY
|
|
L = Castle Anthrax
|
|
O = Python Software Foundation
|
|
CN = dirname example
|
|
|
|
[princ_name]
|
|
realm = EXP:0, GeneralString:KERBEROS.REALM
|
|
principal_name = EXP:1, SEQUENCE:principal_seq
|
|
|
|
[principal_seq]
|
|
name_type = EXP:0, INTEGER:1
|
|
name_string = EXP:1, SEQUENCE:principals
|
|
|
|
[principals]
|
|
princ1 = GeneralString:username
|
|
|
|
[ ca ]
|
|
default_ca = CA_default
|
|
|
|
[ CA_default ]
|
|
dir = cadir
|
|
database = $dir/index.txt
|
|
crlnumber = $dir/crl.txt
|
|
default_md = sha256
|
|
startdate = {startdate}
|
|
default_startdate = {startdate}
|
|
enddate = {enddate}
|
|
default_enddate = {enddate}
|
|
default_days = 7000
|
|
default_crl_days = 7000
|
|
certificate = pycacert.pem
|
|
private_key = pycakey.pem
|
|
serial = $dir/serial
|
|
RANDFILE = $dir/.rand
|
|
policy = policy_match
|
|
|
|
[ policy_match ]
|
|
countryName = match
|
|
stateOrProvinceName = optional
|
|
organizationName = match
|
|
organizationalUnitName = optional
|
|
commonName = supplied
|
|
emailAddress = optional
|
|
|
|
[ policy_anything ]
|
|
countryName = optional
|
|
stateOrProvinceName = optional
|
|
localityName = optional
|
|
organizationName = optional
|
|
organizationalUnitName = optional
|
|
commonName = supplied
|
|
emailAddress = optional
|
|
|
|
|
|
[ v3_ca ]
|
|
|
|
subjectKeyIdentifier=hash
|
|
authorityKeyIdentifier=keyid:always,issuer
|
|
basicConstraints = critical, CA:true
|
|
keyUsage = critical, digitalSignature, keyCertSign, cRLSign
|
|
|
|
"""
|
|
|
|
here = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
|
|
def make_cert_key(hostname, sign=False, extra_san='',
|
|
ext='req_x509_extensions_full', key='rsa:3072'):
|
|
print("creating cert for " + hostname)
|
|
tempnames = []
|
|
for i in range(3):
|
|
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
tempnames.append(f.name)
|
|
req_file, cert_file, key_file = tempnames
|
|
try:
|
|
req = req_template.format(
|
|
hostname=hostname,
|
|
extra_san=extra_san,
|
|
startdate=startdate,
|
|
enddate=enddate
|
|
)
|
|
with open(req_file, 'w') as f:
|
|
f.write(req)
|
|
args = ['req', '-new', '-nodes', '-days', '7000',
|
|
'-newkey', key, '-keyout', key_file,
|
|
'-extensions', ext,
|
|
'-config', req_file]
|
|
if sign:
|
|
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
tempnames.append(f.name)
|
|
reqfile = f.name
|
|
args += ['-out', reqfile ]
|
|
|
|
else:
|
|
args += ['-x509', '-out', cert_file ]
|
|
check_call(['openssl'] + args)
|
|
|
|
if sign:
|
|
args = [
|
|
'ca',
|
|
'-config', req_file,
|
|
'-extensions', ext,
|
|
'-out', cert_file,
|
|
'-outdir', 'cadir',
|
|
'-policy', 'policy_anything',
|
|
'-batch', '-infiles', reqfile
|
|
]
|
|
check_call(['openssl'] + args)
|
|
|
|
|
|
with open(cert_file, 'r') as f:
|
|
cert = f.read()
|
|
with open(key_file, 'r') as f:
|
|
key = f.read()
|
|
return cert, key
|
|
finally:
|
|
for name in tempnames:
|
|
os.remove(name)
|
|
|
|
TMP_CADIR = 'cadir'
|
|
|
|
def unmake_ca():
|
|
shutil.rmtree(TMP_CADIR)
|
|
|
|
def make_ca():
|
|
os.mkdir(TMP_CADIR)
|
|
with open(os.path.join('cadir','index.txt'),'a+') as f:
|
|
pass # empty file
|
|
with open(os.path.join('cadir','crl.txt'),'a+') as f:
|
|
f.write("00")
|
|
with open(os.path.join('cadir','index.txt.attr'),'w+') as f:
|
|
f.write('unique_subject = no')
|
|
# random start value for serial numbers
|
|
with open(os.path.join('cadir','serial'), 'w') as f:
|
|
f.write('CB2D80995A69525B\n')
|
|
|
|
with tempfile.NamedTemporaryFile("w") as t:
|
|
req = req_template.format(
|
|
hostname='our-ca-server',
|
|
extra_san='',
|
|
startdate=startdate,
|
|
enddate=enddate
|
|
)
|
|
t.write(req)
|
|
t.flush()
|
|
with tempfile.NamedTemporaryFile() as f:
|
|
args = ['req', '-config', t.name, '-new',
|
|
'-nodes',
|
|
'-newkey', 'rsa:3072',
|
|
'-keyout', 'pycakey.pem',
|
|
'-out', f.name,
|
|
'-subj', '/C=XY/L=Castle Anthrax/O=Python Software Foundation CA/CN=our-ca-server']
|
|
check_call(['openssl'] + args)
|
|
args = ['ca', '-config', t.name,
|
|
'-out', 'pycacert.pem', '-batch', '-outdir', TMP_CADIR,
|
|
'-keyfile', 'pycakey.pem',
|
|
'-selfsign', '-extensions', 'v3_ca', '-infiles', f.name ]
|
|
check_call(['openssl'] + args)
|
|
args = ['ca', '-config', t.name, '-gencrl', '-out', 'revocation.crl']
|
|
check_call(['openssl'] + args)
|
|
|
|
# capath hashes depend on subject!
|
|
check_call([
|
|
'openssl', 'x509', '-in', 'pycacert.pem', '-out', 'capath/ceff1710.0'
|
|
])
|
|
shutil.copy('capath/ceff1710.0', 'capath/b1930218.0')
|
|
|
|
|
|
def print_cert(path):
|
|
import _ssl
|
|
pprint.pprint(_ssl._test_decode_cert(path))
|
|
|
|
|
|
# The distrusted test material (compromised eMudhra CA certs and a leaf signed
|
|
# by them) lives in a password-protected zip so it doesn't trip security
|
|
# scanners that cannot tell internal test fixtures from production secrets and
|
|
# flag any certificate or private key they find. It is encrypted, not merely
|
|
# zipped, because such scanners read inside plain archives. The password is
|
|
# fixed, public, and not a secret.
|
|
DISTRUSTED_ZIP = 'distrusted.zip'
|
|
DISTRUSTED_ZIP_DIR = 'distrusted'
|
|
DISTRUSTED_ZIP_CHOSEN_KNIGHT = 'Brave Sir Robin'
|
|
|
|
# ASN.1 template for the intermediate's compromised key: secp384r1 scalar 4.
|
|
# openssl derives the public key when the SEC1 structure omits it.
|
|
distrusted_key_template = """
|
|
asn1 = SEQUENCE:eckey
|
|
[eckey]
|
|
version = INTEGER:1
|
|
priv = FORMAT:HEX,OCTETSTRING:{scalar}
|
|
params = EXPLICIT:0,OID:secp384r1
|
|
"""
|
|
|
|
|
|
def make_distrusted_spki_cert():
|
|
# leaf signed by the distrusted intermediate, chains to the distrusted root
|
|
print("creating distrusted-SPKI test cert")
|
|
# The standard library can read but not write encrypted zip files, so we
|
|
# need the Info-ZIP "zip" command to repackage the archive.
|
|
if shutil.which('zip') is None:
|
|
raise SystemExit(
|
|
f"the 'zip' command is required to write the encrypted {DISTRUSTED_ZIP} "
|
|
"(the zipfile module cannot create encrypted archives); "
|
|
"please install Info-ZIP's zip utility"
|
|
)
|
|
|
|
# the root and intermediate are real certs we cannot re-mint; carry them
|
|
# over from the existing archive
|
|
pw = DISTRUSTED_ZIP_CHOSEN_KNIGHT.encode()
|
|
with zipfile.ZipFile(DISTRUSTED_ZIP) as z:
|
|
root = z.read(DISTRUSTED_ZIP_DIR + '/distrusted_root.pem', pw)
|
|
intermediate = z.read(DISTRUSTED_ZIP_DIR + '/distrusted_intermediate.pem', pw)
|
|
|
|
with tempfile.TemporaryDirectory() as workdir:
|
|
stage = os.path.join(workdir, DISTRUSTED_ZIP_DIR)
|
|
os.mkdir(stage)
|
|
req_file = os.path.join(workdir, 'req.cnf')
|
|
key_file = os.path.join(workdir, 'leaf.key')
|
|
csr_file = os.path.join(workdir, 'leaf.csr')
|
|
cert_file = os.path.join(workdir, 'leaf.pem')
|
|
cakey_conf = os.path.join(workdir, 'cakey.cnf')
|
|
cakey_der = os.path.join(workdir, 'cakey.der')
|
|
cakey_file = os.path.join(workdir, 'cakey.pem')
|
|
|
|
inter_file = os.path.join(stage, 'distrusted_intermediate.pem')
|
|
with open(os.path.join(stage, 'distrusted_root.pem'), 'wb') as f:
|
|
f.write(root)
|
|
with open(inter_file, 'wb') as f:
|
|
f.write(intermediate)
|
|
|
|
# recreate the intermediate's key (secp384r1 scalar 4) on the fly
|
|
with open(cakey_conf, 'w') as f:
|
|
f.write(distrusted_key_template.format(scalar='00' * 47 + '04'))
|
|
check_call(['openssl', 'asn1parse', '-genconf', cakey_conf,
|
|
'-out', cakey_der, '-noout'])
|
|
check_call(['openssl', 'pkey', '-in', cakey_der,
|
|
'-inform', 'DER', '-out', cakey_file])
|
|
|
|
req = req_template.format(
|
|
hostname='localhost',
|
|
extra_san='',
|
|
startdate=startdate,
|
|
enddate=enddate
|
|
)
|
|
with open(req_file, 'w') as f:
|
|
f.write(req)
|
|
# ECC leaf to match the ECC intermediate
|
|
check_call(['openssl', 'req', '-new', '-nodes', '-days', '7000',
|
|
'-newkey', 'param:secp384r1.pem', '-keyout', key_file,
|
|
'-config', req_file, '-out', csr_file])
|
|
# sign with the distrusted intermediate, not the test CA
|
|
check_call(['openssl', 'ca', '-config', req_file,
|
|
'-extensions', 'req_x509_extensions_full',
|
|
'-cert', inter_file, '-keyfile', cakey_file,
|
|
'-out', cert_file, '-outdir', 'cadir',
|
|
'-policy', 'policy_anything', '-batch', '-infiles', csr_file])
|
|
with open(key_file) as f:
|
|
key = f.read()
|
|
with open(cert_file) as f:
|
|
cert = f.read()
|
|
# server sends leaf + intermediate, client trusts the root
|
|
with open(os.path.join(stage, 'distrusted_keycert.pem'), 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
f.write(intermediate.decode())
|
|
|
|
# repackage the encrypted archive (zipfile cannot write encryption)
|
|
zippath = os.path.join(here, DISTRUSTED_ZIP)
|
|
if os.path.exists(zippath):
|
|
os.remove(zippath)
|
|
check_call(['zip', '-q', '-X', '-P', DISTRUSTED_ZIP_CHOSEN_KNIGHT, '-r',
|
|
zippath, DISTRUSTED_ZIP_DIR], cwd=workdir)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
os.chdir(here)
|
|
cert, key = make_cert_key('localhost', ext='req_x509_extensions_simple')
|
|
with open('ssl_cert.pem', 'w') as f:
|
|
f.write(cert)
|
|
with open('ssl_key.pem', 'w') as f:
|
|
f.write(key)
|
|
print("password protecting ssl_key.pem in ssl_key.passwd.pem")
|
|
check_call(['openssl','pkey','-in','ssl_key.pem','-out','ssl_key.passwd.pem','-aes256','-passout','pass:somepass'])
|
|
check_call(['openssl','pkey','-in','ssl_key.pem','-out','keycert.passwd.pem','-aes256','-passout','pass:somepass'])
|
|
|
|
with open('keycert.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
with open('keycert.passwd.pem', 'a+') as f:
|
|
f.write(cert)
|
|
|
|
# For certificate matching tests
|
|
make_ca()
|
|
cert, key = make_cert_key('fakehostname', ext='req_x509_extensions_simple')
|
|
with open('keycert2.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
cert, key = make_cert_key('localhost', sign=True)
|
|
with open('keycert3.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
check_call(['openssl', 'x509', '-outform', 'pem', '-in', 'keycert3.pem', '-out', 'cert3.pem'])
|
|
|
|
cert, key = make_cert_key('fakehostname', sign=True)
|
|
with open('keycert4.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
cert, key = make_cert_key(
|
|
'localhost-ecc', sign=True, key='param:secp384r1.pem'
|
|
)
|
|
with open('keycertecc.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
extra_san = [
|
|
'otherName.1 = 1.2.3.4;UTF8:some other identifier',
|
|
'otherName.2 = 1.3.6.1.5.2.2;SEQUENCE:princ_name',
|
|
'email.1 = user@example.org',
|
|
'DNS.2 = www.example.org',
|
|
# GEN_X400
|
|
'dirName.1 = dir_sect',
|
|
# GEN_EDIPARTY
|
|
'URI.1 = https://www.python.org/',
|
|
'IP.1 = 127.0.0.1',
|
|
'IP.2 = ::1',
|
|
'RID.1 = 1.2.3.4.5',
|
|
]
|
|
|
|
cert, key = make_cert_key('allsans', sign=True, extra_san='\n'.join(extra_san))
|
|
with open('allsans.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
extra_san = [
|
|
# könig (king)
|
|
'DNS.2 = xn--knig-5qa.idn.pythontest.net',
|
|
# königsgäßchen (king's alleyway)
|
|
'DNS.3 = xn--knigsgsschen-lcb0w.idna2003.pythontest.net',
|
|
'DNS.4 = xn--knigsgchen-b4a3dun.idna2008.pythontest.net',
|
|
# βόλοσ (marble)
|
|
'DNS.5 = xn--nxasmq6b.idna2003.pythontest.net',
|
|
'DNS.6 = xn--nxasmm1c.idna2008.pythontest.net',
|
|
]
|
|
|
|
# IDN SANS, signed
|
|
cert, key = make_cert_key('idnsans', sign=True, extra_san='\n'.join(extra_san))
|
|
with open('idnsans.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
cert, key = make_cert_key('nosan', sign=True, ext='req_x509_extensions_nosan')
|
|
with open('nosan.pem', 'w') as f:
|
|
f.write(key)
|
|
f.write(cert)
|
|
|
|
make_distrusted_spki_cert()
|
|
|
|
unmake_ca()
|
|
print("update Lib/test/test_ssl.py and Lib/test/test_asyncio/utils.py")
|
|
print_cert('keycert.pem')
|
|
print_cert('keycert3.pem')
|