mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 05:31:20 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			243 lines
		
	
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| 
 | |
| """
 | |
| This script should be called *manually* when we want to upgrade SSLError
 | |
| `library` and `reason` mnemonics to a more recent OpenSSL version. Note
 | |
| that error codes are version specific.
 | |
| 
 | |
| It takes two arguments:
 | |
| 
 | |
| - the path to the OpenSSL folder with the correct git checkout (see below)
 | |
| - the path to the header file to be generated, usually
 | |
| 
 | |
|     Modules/_ssl_data_<MAJOR><MINOR><PATCH>.h
 | |
| 
 | |
| The OpenSSL git checkout should be at a specific tag, using commands like:
 | |
| 
 | |
|     git tag --list 'openssl-*'
 | |
|     git switch --detach openssl-3.4.1
 | |
| 
 | |
| After generating the definitions, compare the result with newest pre-existing file.
 | |
| You can use a command like:
 | |
| 
 | |
|     git diff --no-index Modules/_ssl_data_340.h Modules/_ssl_data_341.h
 | |
| 
 | |
| - If the new version *only* adds new definitions, remove the pre-existing file
 | |
|   and adjust the #include in _ssl.c to point to the new version.
 | |
| - If the new version removes or renumbers some definitions, keep both files and
 | |
|   add a new #include in _ssl.c.
 | |
| 
 | |
| By convention, the latest OpenSSL mnemonics are gathered in the following file:
 | |
| 
 | |
|     Modules/_ssl_data_<MAJOR><MINOR>.h
 | |
| 
 | |
| If those mnemonics are renumbered or removed in a subsequent OpenSSL version,
 | |
| the file is renamed to "Modules/_ssl_data_<MAJOR><MINOR><PATCH>.h" and the
 | |
| latest mnemonics are stored in the patchless file (see below for an example).
 | |
| 
 | |
| A newly supported OpenSSL version should also be added to:
 | |
| 
 | |
| - Tools/ssl/multissltests.py
 | |
| - .github/workflows/build.yml
 | |
| 
 | |
| Example: new mnemonics are added
 | |
| --------------------------------
 | |
| Assume that "Modules/_ssl_data_32x.h" contains the latest mnemonics for
 | |
| CPython and was generated from OpenSSL 3.2.1. If only new mnemonics are
 | |
| added in OpenSSL 3.2.2, the following commands should be executed:
 | |
| 
 | |
|     # in the OpenSSL git directory
 | |
|     git switch --detach openssl-3.2.2
 | |
| 
 | |
|     # in the CPython git directory
 | |
|     python make_ssl_data.py PATH_TO_OPENSSL_GIT_CLONE Modules/_ssl_data_322.h
 | |
|     mv Modules/_ssl_data_322.h Modules/_ssl_data_32.h
 | |
| 
 | |
| Example: mnemonics are renamed/removed
 | |
| --------------------------------------
 | |
| Assume that the existing file is Modules/_ssl_data_34x.h and is based
 | |
| on OpenSSL 3.4.0. Since some mnemonics were renamed in OpenSSL 3.4.1,
 | |
| the following commands should be executed:
 | |
| 
 | |
|     # in the OpenSSL git directory
 | |
|     git switch --detach openssl-3.4.1
 | |
| 
 | |
|     # in the CPython git directory
 | |
|     mv Modules/_ssl_data_34.h Modules/_ssl_data_340.h
 | |
|     python make_ssl_data.py PATH_TO_OPENSSL_GIT_CLONE Modules/_ssl_data_341.h
 | |
|     mv Modules/_ssl_data_341.h Modules/_ssl_data_34.h
 | |
| """
 | |
| 
 | |
| import argparse
 | |
| import datetime
 | |
| import logging
 | |
| import operator
 | |
| import os
 | |
| import re
 | |
| import subprocess
 | |
| 
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| parser = argparse.ArgumentParser(
 | |
|     formatter_class=argparse.RawTextHelpFormatter,
 | |
|     description="Generate SSL data headers from OpenSSL sources"
 | |
| )
 | |
| parser.add_argument("srcdir", help="OpenSSL source directory")
 | |
| parser.add_argument(
 | |
|     "output", nargs="?", default=None,
 | |
|     help="output file (default: standard output)",
 | |
| )
 | |
| 
 | |
| 
 | |
| def error(format_string, *format_args, **kwargs):
 | |
|     # do not use parser.error() to avoid printing short help
 | |
|     logger.error(format_string, *format_args, **kwargs)
 | |
|     raise SystemExit(1)
 | |
| 
 | |
| 
 | |
| def _file_search(fname, pat):
 | |
|     with open(fname, encoding="utf-8") as f:
 | |
|         for line in f:
 | |
|             match = pat.search(line)
 | |
|             if match is not None:
 | |
|                 yield match
 | |
| 
 | |
| 
 | |
| def parse_err_h(args):
 | |
|     """Parse error codes from include/openssl/err.h.in.
 | |
| 
 | |
|     Detected lines match (up to spaces) "#define ERR_LIB_<LIBNAME> <ERRCODE>",
 | |
|     e.g., "# define ERR_LIB_NONE 1".
 | |
|     """
 | |
|     pat = re.compile(r"#\s*define\W+(ERR_LIB_(\w+))\s+(\d+)")
 | |
|     lib2errnum = {}
 | |
|     for match in _file_search(args.err_h, pat):
 | |
|         macroname, libname, num = match.groups()
 | |
|         if macroname in ['ERR_LIB_OFFSET', 'ERR_LIB_MASK']:
 | |
|             # ignore: "# define ERR_LIB_OFFSET                 23L"
 | |
|             # ignore: "# define ERR_LIB_MASK                   0xFF"
 | |
|             continue
 | |
|         actual = int(num)
 | |
|         expect = lib2errnum.setdefault(libname, actual)
 | |
|         if actual != expect:
 | |
|             logger.warning("OpenSSL inconsistency for ERR_LIB_%s (%d != %d)",
 | |
|                            libname, actual, expect)
 | |
|     return lib2errnum
 | |
| 
 | |
| 
 | |
| def parse_openssl_error_text(args):
 | |
|     """Parse error reasons from crypto/err/openssl.txt.
 | |
| 
 | |
|     Detected lines match "<LIBNAME>_R_<ERRNAME>:<ERRCODE>:<MESSAGE>",
 | |
|     e.g., "ASN1_R_ADDING_OBJECT:171:adding object". The <MESSAGE> part
 | |
|     is not stored as it will be recovered at runtime when needed.
 | |
|     """
 | |
|     # ignore backslash line continuation (placed before <MESSAGE> if present)
 | |
|     pat = re.compile(r"^((\w+?)_R_(\w+)):(\d+):")
 | |
|     seen = {}
 | |
|     for match in _file_search(args.errtxt, pat):
 | |
|         reason, libname, errname, num = match.groups()
 | |
|         if "_F_" in reason:  # ignore function codes
 | |
|             # FEAT(picnixz): in the future, we may want to also check
 | |
|             # the consistency of the OpenSSL files with an external tool.
 | |
|             # See https://github.com/python/cpython/issues/132745.
 | |
|             continue
 | |
|         yield reason, libname, errname, int(num)
 | |
| 
 | |
| 
 | |
| def parse_extra_reasons(args):
 | |
|     """Parse extra reasons from crypto/err/openssl.ec.
 | |
| 
 | |
|     Detected lines are matched against "R <LIBNAME>_R_<ERRNAME> <ERRCODE>",
 | |
|     e.g., "R SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010".
 | |
|     """
 | |
|     pat = re.compile(r"^R\s+((\w+)_R_(\w+))\s+(\d+)")
 | |
|     for match in _file_search(args.errcodes, pat):
 | |
|         reason, libname, errname, num = match.groups()
 | |
|         yield reason, libname, errname, int(num)
 | |
| 
 | |
| 
 | |
| def gen_library_codes(args):
 | |
|     """Generate table short libname to numeric code."""
 | |
|     yield "/* generated from args.lib2errnum */"
 | |
|     yield "static struct py_ssl_library_code library_codes[] = {"
 | |
|     for libname in sorted(args.lib2errnum):
 | |
|         yield f"#ifdef ERR_LIB_{libname}"
 | |
|         yield f'    {{"{libname}", ERR_LIB_{libname}}},'
 | |
|         yield "#endif"
 | |
|     yield "    {NULL, 0}  /* sentinel */"
 | |
|     yield "};"
 | |
| 
 | |
| 
 | |
| def gen_error_codes(args):
 | |
|     """Generate error code table for error reasons."""
 | |
|     yield "/* generated from args.reasons */"
 | |
|     yield "static struct py_ssl_error_code error_codes[] = {"
 | |
|     for reason, libname, errname, num in args.reasons:
 | |
|         yield f"  #ifdef {reason}"
 | |
|         yield f'    {{"{errname}", ERR_LIB_{libname}, {reason}}},'
 | |
|         yield "  #else"
 | |
|         yield f'    {{"{errname}", {args.lib2errnum[libname]}, {num}}},'
 | |
|         yield "  #endif"
 | |
|     yield "    {NULL, 0, 0}  /* sentinel */"
 | |
|     yield "};"
 | |
| 
 | |
| 
 | |
| def get_openssl_git_commit(args):
 | |
|     git_describe = subprocess.run(
 | |
|         ['git', 'describe', '--long', '--dirty'],
 | |
|         cwd=args.srcdir,
 | |
|         capture_output=True,
 | |
|         encoding='utf-8',
 | |
|         check=True,
 | |
|     )
 | |
|     return git_describe.stdout.strip()
 | |
| 
 | |
| 
 | |
| def main(args=None):
 | |
|     args = parser.parse_args(args)
 | |
|     if not os.path.isdir(args.srcdir):
 | |
|         error(f"OpenSSL directory not found: {args.srcdir}")
 | |
|     args.err_h = os.path.join(args.srcdir, "include", "openssl", "err.h")
 | |
|     if not os.path.isfile(args.err_h):
 | |
|         # Fall back to infile for OpenSSL 3.0.0 and later.
 | |
|         args.err_h += ".in"
 | |
|     args.errcodes = os.path.join(args.srcdir, "crypto", "err", "openssl.ec")
 | |
|     if not os.path.isfile(args.errcodes):
 | |
|         error(f"file {args.errcodes} not found in {args.srcdir}")
 | |
|     args.errtxt = os.path.join(args.srcdir, "crypto", "err", "openssl.txt")
 | |
|     if not os.path.isfile(args.errtxt):
 | |
|         error(f"file {args.errtxt} not found in {args.srcdir}")
 | |
| 
 | |
|     # [("ERR_LIB_X509", "X509", 11), ...]
 | |
|     args.lib2errnum = parse_err_h(args)
 | |
| 
 | |
|     # [('X509_R_AKID_MISMATCH', 'X509', 'AKID_MISMATCH', 110), ...]
 | |
|     reasons = []
 | |
|     reasons.extend(parse_openssl_error_text(args))
 | |
|     reasons.extend(parse_extra_reasons(args))
 | |
|     # sort by macro name and numeric error code
 | |
|     args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))
 | |
| 
 | |
|     commit = get_openssl_git_commit(args)
 | |
|     lines = [
 | |
|         "/* File generated by Tools/ssl/make_ssl_data.py */",
 | |
|         f"/* Generated on {datetime.datetime.now(datetime.UTC).isoformat()} */",
 | |
|         f"/* Generated from Git commit {commit} */",
 | |
|         "",
 | |
|     ]
 | |
|     lines.extend(gen_library_codes(args))
 | |
|     lines.append("")
 | |
|     lines.extend(gen_error_codes(args))
 | |
| 
 | |
|     if args.output is None:
 | |
|         for line in lines:
 | |
|             print(line)
 | |
|     else:
 | |
|         with open(args.output, 'w') as output:
 | |
|             for line in lines:
 | |
|                 print(line, file=output)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     main()
 | 
