1 #! /usr/bin/env python3
2
3 """
4 This script should be called *manually* when we want to upgrade SSLError
5 `library` and `reason` mnemonics to a more recent OpenSSL version.
6
7 It takes two arguments:
8 - the path to the OpenSSL source tree (e.g. git checkout)
9 - the path to the header file to be generated Modules/_ssl_data_{version}.h
10 - error codes are version specific
11 """
12
13 import argparse
14 import datetime
15 import operator
16 import os
17 import re
18 import sys
19
20
21 parser = argparse.ArgumentParser(
22 description="Generate ssl_data.h from OpenSSL sources"
23 )
24 parser.add_argument("srcdir", help="OpenSSL source directory")
25 parser.add_argument(
26 "output", nargs="?", type=argparse.FileType("w"), default=sys.stdout
27 )
28
29
30 def _file_search(fname, pat):
31 with open(fname, encoding="utf-8") as f:
32 for line in f:
33 match = pat.search(line)
34 if match is not None:
35 yield match
36
37
38 def parse_err_h(args):
39 """Parse err codes, e.g. ERR_LIB_X509: 11"""
40 pat = re.compile(r"#\s*define\W+ERR_LIB_(\w+)\s+(\d+)")
41 lib2errnum = {}
42 for match in _file_search(args.err_h, pat):
43 libname, num = match.groups()
44 lib2errnum[libname] = int(num)
45
46 return lib2errnum
47
48
49 def parse_openssl_error_text(args):
50 """Parse error reasons, X509_R_AKID_MISMATCH"""
51 # ignore backslash line continuation for now
52 pat = re.compile(r"^((\w+?)_R_(\w+)):(\d+):")
53 for match in _file_search(args.errtxt, pat):
54 reason, libname, errname, num = match.groups()
55 if "_F_" in reason:
56 # ignore function codes
57 continue
58 num = int(num)
59 yield reason, libname, errname, num
60
61
62 def parse_extra_reasons(args):
63 """Parse extra reasons from openssl.ec"""
64 pat = re.compile(r"^R\s+((\w+)_R_(\w+))\s+(\d+)")
65 for match in _file_search(args.errcodes, pat):
66 reason, libname, errname, num = match.groups()
67 num = int(num)
68 yield reason, libname, errname, num
69
70
71 def gen_library_codes(args):
72 """Generate table short libname to numeric code"""
73 yield "static struct py_ssl_library_code library_codes[] = {"
74 for libname in sorted(args.lib2errnum):
75 yield f"#ifdef ERR_LIB_{libname}"
76 yield f' {{"{libname}", ERR_LIB_{libname}}},'
77 yield "#endif"
78 yield " { NULL }"
79 yield "};"
80 yield ""
81
82
83 def gen_error_codes(args):
84 """Generate error code table for error reasons"""
85 yield "static struct py_ssl_error_code error_codes[] = {"
86 for reason, libname, errname, num in args.reasons:
87 yield f" #ifdef {reason}"
88 yield f' {{"{errname}", ERR_LIB_{libname}, {reason}}},'
89 yield " #else"
90 yield f' {{"{errname}", {args.lib2errnum[libname]}, {num}}},'
91 yield " #endif"
92
93 yield " { NULL }"
94 yield "};"
95 yield ""
96
97
98 def main():
99 args = parser.parse_args()
100
101 args.err_h = os.path.join(args.srcdir, "include", "openssl", "err.h")
102 if not os.path.isfile(args.err_h):
103 # Fall back to infile for OpenSSL 3.0.0
104 args.err_h += ".in"
105 args.errcodes = os.path.join(args.srcdir, "crypto", "err", "openssl.ec")
106 args.errtxt = os.path.join(args.srcdir, "crypto", "err", "openssl.txt")
107
108 if not os.path.isfile(args.errtxt):
109 parser.error(f"File {args.errtxt} not found in srcdir\n.")
110
111 # {X509: 11, ...}
112 args.lib2errnum = parse_err_h(args)
113
114 # [('X509_R_AKID_MISMATCH', 'X509', 'AKID_MISMATCH', 110), ...]
115 reasons = []
116 reasons.extend(parse_openssl_error_text(args))
117 reasons.extend(parse_extra_reasons(args))
118 # sort by libname, numeric error code
119 args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))
120
121 lines = [
122 "/* File generated by Tools/ssl/make_ssl_data.py */"
123 f"/* Generated on {datetime.datetime.utcnow().isoformat()} */"
124 ]
125 lines.extend(gen_library_codes(args))
126 lines.append("")
127 lines.extend(gen_error_codes(args))
128
129 for line in lines:
130 args.output.write(line + "\n")
131
132
133 if __name__ == "__main__":
134 main()