Skip to content

Commit cc0b76c

Browse files
committed
Add ssl_data
1 parent b0422cb commit cc0b76c

File tree

8 files changed

+5116
-31
lines changed

8 files changed

+5116
-31
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_ssl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1715,7 +1715,6 @@ def test_str(self):
17151715
self.assertEqual(str(e), "foo")
17161716
self.assertEqual(e.errno, 1)
17171717

1718-
@unittest.expectedFailure # TODO: RUSTPYTHON
17191718
def test_lib_reason(self):
17201719
# Test the library and reason attributes
17211720
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -4760,6 +4759,7 @@ def test_pha_optional_nocert(self):
47604759
s.write(b'HASCERT')
47614760
self.assertEqual(s.recv(1024), b'FALSE\n')
47624761

4762+
@unittest.expectedFailure # TODO: RUSTPYTHON
47634763
def test_pha_no_pha_client(self):
47644764
client_context, server_context, hostname = testing_context()
47654765
server_context.post_handshake_auth = True

scripts/make_ssl_data_rs.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Generate Rust SSL error mapping code from OpenSSL sources.
5+
6+
This is based on CPython's Tools/ssl/make_ssl_data.py but generates
7+
Rust code instead of C headers.
8+
9+
It takes two arguments:
10+
- the path to the OpenSSL source tree (e.g. git checkout)
11+
- the path to the Rust file to be generated (e.g. stdlib/src/ssl/ssl_data.rs)
12+
- error codes are version specific
13+
"""
14+
15+
import argparse
16+
import datetime
17+
import operator
18+
import os
19+
import re
20+
import sys
21+
22+
23+
parser = argparse.ArgumentParser(
24+
description="Generate ssl_data.rs from OpenSSL sources"
25+
)
26+
parser.add_argument("srcdir", help="OpenSSL source directory")
27+
parser.add_argument(
28+
"output", nargs="?", default=None
29+
)
30+
31+
32+
def _file_search(fname, pat):
33+
with open(fname, encoding="utf-8") as f:
34+
for line in f:
35+
match = pat.search(line)
36+
if match is not None:
37+
yield match
38+
39+
40+
def parse_err_h(args):
41+
"""Parse err codes, e.g. ERR_LIB_X509: 11"""
42+
pat = re.compile(r"#\s*define\W+ERR_LIB_(\w+)\s+(\d+)")
43+
lib2errnum = {}
44+
for match in _file_search(args.err_h, pat):
45+
libname, num = match.groups()
46+
lib2errnum[libname] = int(num)
47+
48+
return lib2errnum
49+
50+
51+
def parse_openssl_error_text(args):
52+
"""Parse error reasons, X509_R_AKID_MISMATCH"""
53+
# ignore backslash line continuation for now
54+
pat = re.compile(r"^((\w+?)_R_(\w+)):(\d+):")
55+
for match in _file_search(args.errtxt, pat):
56+
reason, libname, errname, num = match.groups()
57+
if "_F_" in reason:
58+
# ignore function codes
59+
continue
60+
num = int(num)
61+
yield reason, libname, errname, num
62+
63+
64+
def parse_extra_reasons(args):
65+
"""Parse extra reasons from openssl.ec"""
66+
pat = re.compile(r"^R\s+((\w+)_R_(\w+))\s+(\d+)")
67+
for match in _file_search(args.errcodes, pat):
68+
reason, libname, errname, num = match.groups()
69+
num = int(num)
70+
yield reason, libname, errname, num
71+
72+
73+
def gen_library_codes_rust(args):
74+
"""Generate Rust phf map for library codes"""
75+
yield "// Maps lib_code -> library name"
76+
yield "// Example: 20 -> \"SSL\""
77+
yield "pub static LIBRARY_CODES: phf::Map<u32, &'static str> = phf_map! {"
78+
79+
# Deduplicate: keep the last one if there are duplicates
80+
seen = {}
81+
for libname in sorted(args.lib2errnum):
82+
lib_num = args.lib2errnum[libname]
83+
seen[lib_num] = libname
84+
85+
for lib_num in sorted(seen.keys()):
86+
libname = seen[lib_num]
87+
yield f' {lib_num}u32 => "{libname}",'
88+
yield "};"
89+
yield ""
90+
91+
92+
def gen_error_codes_rust(args):
93+
"""Generate Rust phf map for error codes"""
94+
yield "// Maps encoded (lib, reason) -> error mnemonic"
95+
yield "// Example: encode_error_key(20, 134) -> \"CERTIFICATE_VERIFY_FAILED\""
96+
yield "// Key encoding: (lib << 32) | reason"
97+
yield "pub static ERROR_CODES: phf::Map<u64, &'static str> = phf_map! {"
98+
for reason, libname, errname, num in args.reasons:
99+
if libname not in args.lib2errnum:
100+
continue
101+
lib_num = args.lib2errnum[libname]
102+
# Encode (lib, reason) as single u64
103+
key = (lib_num << 32) | num
104+
yield f' {key}u64 => "{errname}",'
105+
yield "};"
106+
yield ""
107+
108+
109+
def main():
110+
args = parser.parse_args()
111+
112+
args.err_h = os.path.join(args.srcdir, "include", "openssl", "err.h")
113+
if not os.path.isfile(args.err_h):
114+
# Fall back to infile for OpenSSL 3.0.0
115+
args.err_h += ".in"
116+
args.errcodes = os.path.join(args.srcdir, "crypto", "err", "openssl.ec")
117+
args.errtxt = os.path.join(args.srcdir, "crypto", "err", "openssl.txt")
118+
119+
if not os.path.isfile(args.errtxt):
120+
parser.error(f"File {args.errtxt} not found in srcdir\n.")
121+
122+
# {X509: 11, ...}
123+
args.lib2errnum = parse_err_h(args)
124+
125+
# [('X509_R_AKID_MISMATCH', 'X509', 'AKID_MISMATCH', 110), ...]
126+
reasons = []
127+
reasons.extend(parse_openssl_error_text(args))
128+
reasons.extend(parse_extra_reasons(args))
129+
# sort by libname, numeric error code
130+
args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))
131+
132+
lines = [
133+
"// File generated by tools/make_ssl_data_rs.py",
134+
f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}",
135+
f"// Source: OpenSSL from {args.srcdir}",
136+
"",
137+
"use phf::phf_map;",
138+
"",
139+
]
140+
lines.extend(gen_library_codes_rust(args))
141+
lines.extend(gen_error_codes_rust(args))
142+
143+
# Add helper function
144+
lines.extend([
145+
"/// Helper function to create encoded key from (lib, reason) pair",
146+
"#[inline]",
147+
"pub fn encode_error_key(lib: i32, reason: i32) -> u64 {",
148+
" ((lib as u64) << 32) | (reason as u64 & 0xFFFFFFFF)",
149+
"}",
150+
"",
151+
])
152+
153+
if args.output is None:
154+
for line in lines:
155+
print(line)
156+
else:
157+
with open(args.output, 'w') as output:
158+
for line in lines:
159+
print(line, file=output)
160+
161+
print(f"Generated {args.output}")
162+
print(f"Found {len(args.lib2errnum)} library codes")
163+
print(f"Found {len(args.reasons)} error codes")
164+
165+
166+
if __name__ == "__main__":
167+
main()

stdlib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ num-integer = { workspace = true }
4040
num-traits = { workspace = true }
4141
num_enum = { workspace = true }
4242
parking_lot = { workspace = true }
43+
phf = { version = "0.11", features = ["macros"] }
4344

4445
memchr = { workspace = true }
4546
base64 = "0.22"

stdlib/src/ssl.rs

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
mod cert;
44

5+
// Conditional compilation for OpenSSL version-specific error codes
6+
cfg_if::cfg_if! {
7+
if #[cfg(ossl310)] {
8+
// OpenSSL 3.1.0+
9+
#[path = "ssl/ssl_data_31.rs"]
10+
mod ssl_data;
11+
} else if #[cfg(ossl300)] {
12+
// OpenSSL 3.0.0+
13+
#[path = "ssl/ssl_data_300.rs"]
14+
mod ssl_data;
15+
} else {
16+
// OpenSSL 1.1.1+ (fallback)
17+
#[path = "ssl/ssl_data_111.rs"]
18+
mod ssl_data;
19+
}
20+
}
21+
522
use crate::vm::{PyRef, VirtualMachine, builtins::PyModule};
623
use openssl_probe::ProbeResult;
724

@@ -48,7 +65,7 @@ mod _ssl {
4865
ArgBytesLike, ArgCallable, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath,
4966
OptionalArg, PyComparisonValue,
5067
},
51-
types::{Comparable, Constructor, PyComparisonOp, PyTypeFlags},
68+
types::{Comparable, Constructor, PyComparisonOp},
5269
utils::ToCString,
5370
},
5471
};
@@ -726,16 +743,16 @@ mod _ssl {
726743
let ctx_ptr = builder.as_ptr();
727744
match proto {
728745
SslVersion::Tls1 => {
729-
sys::SSL_CTX_set_min_proto_version(ctx_ptr, sys::TLS1_VERSION as i32);
730-
sys::SSL_CTX_set_max_proto_version(ctx_ptr, sys::TLS1_VERSION as i32);
746+
sys::SSL_CTX_set_min_proto_version(ctx_ptr, sys::TLS1_VERSION);
747+
sys::SSL_CTX_set_max_proto_version(ctx_ptr, sys::TLS1_VERSION);
731748
}
732749
SslVersion::Tls1_1 => {
733-
sys::SSL_CTX_set_min_proto_version(ctx_ptr, sys::TLS1_1_VERSION as i32);
734-
sys::SSL_CTX_set_max_proto_version(ctx_ptr, sys::TLS1_1_VERSION as i32);
750+
sys::SSL_CTX_set_min_proto_version(ctx_ptr, sys::TLS1_1_VERSION);
751+
sys::SSL_CTX_set_max_proto_version(ctx_ptr, sys::TLS1_1_VERSION);
735752
}
736753
SslVersion::Tls1_2 => {
737-
sys::SSL_CTX_set_min_proto_version(ctx_ptr, sys::TLS1_2_VERSION as i32);
738-
sys::SSL_CTX_set_max_proto_version(ctx_ptr, sys::TLS1_2_VERSION as i32);
754+
sys::SSL_CTX_set_min_proto_version(ctx_ptr, sys::TLS1_2_VERSION);
755+
sys::SSL_CTX_set_max_proto_version(ctx_ptr, sys::TLS1_2_VERSION);
739756
}
740757
_ => {
741758
// For Tls, TlsClient, TlsServer, use default (no restrictions)
@@ -987,12 +1004,12 @@ mod _ssl {
9871004
let proto_version = match value {
9881005
-2 => {
9891006
// PY_PROTO_MINIMUM_SUPPORTED -> use minimum available (TLS 1.2)
990-
sys::TLS1_2_VERSION as i32
1007+
sys::TLS1_2_VERSION
9911008
}
9921009
-1 => {
9931010
// PY_PROTO_MAXIMUM_SUPPORTED -> use maximum available
9941011
// For max on min_proto_version, we use the newest available
995-
sys::TLS1_3_VERSION as i32
1012+
sys::TLS1_3_VERSION
9961013
}
9971014
_ => value,
9981015
};
@@ -1025,7 +1042,7 @@ mod _ssl {
10251042
}
10261043
-2 => {
10271044
// PY_PROTO_MINIMUM_SUPPORTED -> use minimum available (TLS 1.2)
1028-
sys::TLS1_2_VERSION as i32
1045+
sys::TLS1_2_VERSION
10291046
}
10301047
_ => value,
10311048
};
@@ -3033,21 +3050,33 @@ mod _ssl {
30333050
let file = file
30343051
.rsplit_once(&['/', '\\'][..])
30353052
.map_or(file, |(_, basename)| basename);
3036-
// TODO: finish map
3037-
let default_errstr = e.reason().unwrap_or("unknown error");
3038-
let (errstr, is_cert_verify_error) = match default_errstr {
3039-
"certificate verify failed" => ("CERTIFICATE_VERIFY_FAILED", true),
3040-
"no shared cipher" => ("NO_SHARED_CIPHER", false),
3041-
"sslv3 alert handshake failure" => ("SSLV3_ALERT_HANDSHAKE_FAILURE", false),
3042-
"tlsv1 alert internal error" => ("TLSV1_ALERT_INTERNAL_ERROR", false),
3043-
"tlsv1 alert access denied" => ("TLSV1_ALERT_ACCESS_DENIED", false),
3044-
"tlsv1 alert unknown ca" => ("TLSV1_ALERT_UNKNOWN_CA", false),
3045-
"sslv3 alert certificate revoked" => ("SSLV3_ALERT_CERTIFICATE_REVOKED", false),
3046-
"sslv3 alert certificate expired" => ("SSLV3_ALERT_CERTIFICATE_EXPIRED", false),
3047-
"wrong version number" => ("WRONG_VERSION_NUMBER", false),
3048-
"wrong ssl version" => ("WRONG_SSL_VERSION", false),
3049-
_ => (default_errstr, false),
3050-
};
3053+
3054+
// Get error codes - same approach as CPython
3055+
// CPython: Modules/_ssl.c:474-496
3056+
let lib = sys::ERR_GET_LIB(e.code());
3057+
let reason = sys::ERR_GET_REASON(e.code());
3058+
3059+
// Look up error mnemonic from our static tables
3060+
// CPython uses dict lookup: err_codes_to_names[(lib, reason)]
3061+
let key = super::ssl_data::encode_error_key(lib, reason);
3062+
let errstr = super::ssl_data::ERROR_CODES
3063+
.get(&key)
3064+
.copied()
3065+
.or_else(|| {
3066+
// Fallback: use OpenSSL's error string
3067+
e.reason()
3068+
})
3069+
.unwrap_or("unknown error");
3070+
3071+
// Check if this is a certificate verification error
3072+
// CPython: Modules/_ssl.c:663-666, 683-686
3073+
// ERR_LIB_SSL = 20 (from _ssl_data_300.h)
3074+
// SSL_R_CERTIFICATE_VERIFY_FAILED = 134 (from _ssl_data_300.h)
3075+
let is_cert_verify_error = lib == 20 && reason == 134;
3076+
3077+
// Look up library name from our static table
3078+
// CPython uses: lib_codes_to_names[lib]
3079+
let lib_name = super::ssl_data::LIBRARY_CODES.get(&(lib as u32)).copied();
30513080

30523081
// Use SSLCertVerificationError for certificate verification failures
30533082
let cls = if is_cert_verify_error {
@@ -3057,9 +3086,9 @@ mod _ssl {
30573086
};
30583087

30593088
// Build message
3060-
let lib_obj = e.library();
3061-
let msg = if let Some(lib) = lib_obj {
3062-
format!("[{lib}] {errstr} ({file}:{line})")
3089+
// CPython: Modules/_ssl.c:539-549
3090+
let msg = if let Some(lib_str) = lib_name {
3091+
format!("[{lib_str}] {errstr} ({file}:{line})")
30633092
} else {
30643093
format!("{errstr} ({file}:{line})")
30653094
};
@@ -3079,8 +3108,8 @@ mod _ssl {
30793108
let _ = exc_obj.set_attr("reason", reason_value, vm);
30803109

30813110
// Set library attribute (None if not available)
3082-
let library_value: PyObjectRef = if let Some(lib) = lib_obj {
3083-
vm.ctx.new_str(lib).into()
3111+
let library_value: PyObjectRef = if let Some(lib_str) = lib_name {
3112+
vm.ctx.new_str(lib_str).into()
30843113
} else {
30853114
vm.ctx.none()
30863115
};

0 commit comments

Comments
 (0)