Replace SSL backend to rustls (#6244)

This commit is contained in:
Jeong, YunWon
2025-11-16 22:17:35 +09:00
committed by GitHub
parent 7c4c1eabf0
commit 1a783fc9ec
21 changed files with 12969 additions and 3345 deletions

View File

@@ -2,11 +2,14 @@ argtypes
asdl
asname
augassign
badcert
badsyntax
basetype
boolop
bxor
cached_tsver
cadata
cafile
cellarg
cellvar
cellvars
@@ -23,8 +26,8 @@ freevars
fromlist
heaptype
HIGHRES
Itertool
IMMUTABLETYPE
Itertool
kwonlyarg
kwonlyargs
lasti
@@ -47,6 +50,7 @@ stackdepth
stringlib
structseq
subparams
ticketer
tok_oldval
tvars
unaryop
@@ -56,6 +60,7 @@ VARKEYWORDS
varkwarg
wbits
weakreflist
webpki
withitem
withs
xstat

View File

@@ -50,6 +50,7 @@ nanos
nonoverlapping
objclass
peekable
pemfile
powc
powf
powi
@@ -61,6 +62,7 @@ rposition
rsplitn
rustc
rustfmt
rustls
rustyline
seedable
seekfrom

View File

@@ -16,7 +16,8 @@ concurrency:
cancel-in-progress: true
env:
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls
CARGO_ARGS_NO_SSL: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite
# Skip additional tests on Windows. They are checked on Linux and MacOS.
# test_glob: many failing tests
# test_io: many failing tests
@@ -169,7 +170,7 @@ jobs:
target: aarch64-apple-ios
if: runner.os == 'macOS'
- name: Check compilation for iOS
run: cargo check --target aarch64-apple-ios
run: cargo check --target aarch64-apple-ios ${{ env.CARGO_ARGS_NO_SSL }}
if: runner.os == 'macOS'
exotic_targets:
@@ -186,14 +187,14 @@ jobs:
- name: Install gcc-multilib and musl-tools
run: sudo apt-get update && sudo apt-get install gcc-multilib musl-tools
- name: Check compilation for x86 32bit
run: cargo check --target i686-unknown-linux-gnu
run: cargo check --target i686-unknown-linux-gnu ${{ env.CARGO_ARGS_NO_SSL }}
- uses: dtolnay/rust-toolchain@stable
with:
target: aarch64-linux-android
- name: Check compilation for android
run: cargo check --target aarch64-linux-android
run: cargo check --target aarch64-linux-android ${{ env.CARGO_ARGS_NO_SSL }}
- uses: dtolnay/rust-toolchain@stable
with:
@@ -202,28 +203,28 @@ jobs:
- name: Install gcc-aarch64-linux-gnu
run: sudo apt install gcc-aarch64-linux-gnu
- name: Check compilation for aarch64 linux gnu
run: cargo check --target aarch64-unknown-linux-gnu
run: cargo check --target aarch64-unknown-linux-gnu ${{ env.CARGO_ARGS_NO_SSL }}
- uses: dtolnay/rust-toolchain@stable
with:
target: i686-unknown-linux-musl
- name: Check compilation for musl
run: cargo check --target i686-unknown-linux-musl
run: cargo check --target i686-unknown-linux-musl ${{ env.CARGO_ARGS_NO_SSL }}
- uses: dtolnay/rust-toolchain@stable
with:
target: x86_64-unknown-freebsd
- name: Check compilation for freebsd
run: cargo check --target x86_64-unknown-freebsd
run: cargo check --target x86_64-unknown-freebsd ${{ env.CARGO_ARGS_NO_SSL }}
- uses: dtolnay/rust-toolchain@stable
with:
target: x86_64-unknown-freebsd
- name: Check compilation for freeBSD
run: cargo check --target x86_64-unknown-freebsd
run: cargo check --target x86_64-unknown-freebsd ${{ env.CARGO_ARGS_NO_SSL }}
# - name: Prepare repository for redox compilation
# run: bash scripts/redox/uncomment-cargo.sh

825
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ repository.workspace = true
license.workspace = true
[features]
default = ["threading", "stdlib", "stdio", "importlib"]
default = ["threading", "stdlib", "stdio", "importlib", "ssl-rustls"]
importlib = ["rustpython-vm/importlib"]
encodings = ["rustpython-vm/encodings"]
stdio = ["rustpython-vm/stdio"]
@@ -20,8 +20,10 @@ freeze-stdlib = ["stdlib", "rustpython-vm/freeze-stdlib", "rustpython-pylib?/fre
jit = ["rustpython-vm/jit"]
threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"]
sqlite = ["rustpython-stdlib/sqlite"]
ssl = ["rustpython-stdlib/ssl"]
ssl-vendor = ["ssl", "rustpython-stdlib/ssl-vendor"]
ssl = []
ssl-rustls = ["ssl", "rustpython-stdlib/ssl-rustls"]
ssl-openssl = ["ssl", "rustpython-stdlib/ssl-openssl"]
ssl-vendor = ["ssl-openssl", "rustpython-stdlib/ssl-vendor"]
tkinter = ["rustpython-stdlib/tkinter"]
[build-dependencies]

68
Lib/test/test_ssl.py vendored
View File

@@ -426,7 +426,6 @@ class BasicSocketTests(unittest.TestCase):
ssl.RAND_add(b"this is a random bytes object", 75.0)
ssl.RAND_add(bytearray(b"this is a random bytearray object"), 75.0)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_parse_cert(self):
# note that this uses an 'unofficial' function in _ssl.c,
# provided solely for this test, to exercise the certificate
@@ -506,7 +505,6 @@ class BasicSocketTests(unittest.TestCase):
self.assertEqual(p['subjectAltName'], san)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_parse_all_sans(self):
p = ssl._ssl._test_decode_cert(ALLSANFILE)
self.assertEqual(p['subjectAltName'],
@@ -927,7 +925,6 @@ class BasicSocketTests(unittest.TestCase):
)
self.assertIn(rc, errors)
@unittest.skip("TODO: RUSTPYTHON; hangs")
def test_read_write_zero(self):
# empty reads and writes now work, bpo-42854, bpo-31711
client_context, server_context, hostname = testing_context()
@@ -993,7 +990,6 @@ class ContextTests(unittest.TestCase):
len(intersection), 2, f"\ngot: {sorted(names)}\nexpected: {sorted(expected)}"
)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_options(self):
# Test default SSLContext options
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -1066,8 +1062,8 @@ class ContextTests(unittest.TestCase):
with self.assertRaises(AttributeError):
ctx.hostname_checks_common_name = True
@ignore_deprecation
@unittest.expectedFailure # TODO: RUSTPYTHON
@ignore_deprecation
def test_min_max_version(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# OpenSSL default is MINIMUM_SUPPORTED, however some vendors like
@@ -1185,7 +1181,6 @@ class ContextTests(unittest.TestCase):
with self.assertRaises(TypeError):
ctx.verify_flags = None
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_load_cert_chain(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# Combined key and cert in a single file
@@ -1294,7 +1289,6 @@ class ContextTests(unittest.TestCase):
self.assertIsNone(cm.exc_value)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_load_verify_locations(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_verify_locations(CERTFILE)
@@ -1314,7 +1308,6 @@ class ContextTests(unittest.TestCase):
# Issue #10989: crash if the second argument type is invalid
self.assertRaises(TypeError, ctx.load_verify_locations, None, True)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_load_verify_cadata(self):
# test cadata
with open(CAFILE_CACERT) as f:
@@ -1380,7 +1373,6 @@ class ContextTests(unittest.TestCase):
with self.assertRaises(ssl.SSLError):
ctx.load_verify_locations(cadata=cacert_der + b"A")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_load_dh_params(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
try:
@@ -1473,7 +1465,6 @@ class ContextTests(unittest.TestCase):
self.assertEqual(ctx.cert_store_stats(),
{'x509_ca': 1, 'crl': 0, 'x509': 2})
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_get_ca_certs(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
self.assertEqual(ctx.get_ca_certs(), [])
@@ -1732,7 +1723,6 @@ class SSLErrorTests(unittest.TestCase):
s = str(cm.exception)
self.assertTrue("NO_START_LINE" in s, s)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_subclass(self):
# Check that the appropriate SSLError subclass is raised
# (this only tests one of them)
@@ -1751,7 +1741,6 @@ class SSLErrorTests(unittest.TestCase):
self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_WANT_READ)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_bad_server_hostname(self):
ctx = ssl.create_default_context()
with self.assertRaises(ValueError):
@@ -1838,7 +1827,6 @@ class SSLObjectTests(unittest.TestCase):
with self.assertRaisesRegex(TypeError, "public constructor"):
ssl.SSLObject(bio, bio)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_unwrap(self):
client_ctx, server_ctx, hostname = testing_context()
c_in = ssl.MemoryBIO()
@@ -2193,7 +2181,6 @@ class SimpleBackgroundTests(unittest.TestCase):
% (count, func.__name__))
return ret
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_bio_handshake(self):
sock = socket.socket(socket.AF_INET)
self.addCleanup(sock.close)
@@ -2230,7 +2217,6 @@ class SimpleBackgroundTests(unittest.TestCase):
pass
self.assertRaises(ssl.SSLError, sslobj.write, b'foo')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_bio_read_write_data(self):
sock = socket.socket(socket.AF_INET)
self.addCleanup(sock.close)
@@ -2248,7 +2234,6 @@ class SimpleBackgroundTests(unittest.TestCase):
self.assertEqual(buf, b'foo\n')
self.ssl_io_loop(sock, incoming, outgoing, sslobj.unwrap)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_transport_eof(self):
client_context, server_context, hostname = testing_context()
with socket.socket(socket.AF_INET) as sock:
@@ -3565,7 +3550,6 @@ class ThreadedTests(unittest.TestCase):
f.close()
self.assertEqual(d1, d2)
@unittest.skip("TODO: RUSTPYTHON; hangs")
def test_asyncore_server(self):
"""Check the example asyncore integration."""
if support.verbose:
@@ -3595,7 +3579,6 @@ class ThreadedTests(unittest.TestCase):
if support.verbose:
sys.stdout.write(" client: connection closed.\n")
@unittest.skip("TODO: RUSTPYTHON; hangs")
def test_recv_send(self):
"""Test recv(), send() and friends."""
if support.verbose:
@@ -3732,7 +3715,6 @@ class ThreadedTests(unittest.TestCase):
s.close()
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_recv_zero(self):
server = ThreadedEchoServer(CERTFILE)
self.enterContext(server)
@@ -4040,6 +4022,7 @@ class ThreadedTests(unittest.TestCase):
s.connect((HOST, server.port))
self.assertIn("ECDH", s.cipher()[0])
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipUnless("tls-unique" in ssl.CHANNEL_BINDING_TYPES,
"'tls-unique' channel binding not available")
def test_tls_unique_channel_binding(self):
@@ -4212,7 +4195,6 @@ class ThreadedTests(unittest.TestCase):
sni_name=hostname)
self.assertIs(stats['client_alpn_protocol'], None)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_alpn_protocols(self):
server_protocols = ['foo', 'bar', 'milkshake']
protocol_tests = [
@@ -4263,7 +4245,6 @@ class ThreadedTests(unittest.TestCase):
cert = stats['peercert']
self.assertIn((('commonName', name),), cert['subject'])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_sni_callback(self):
calls = []
server_context, other_context, client_context = self.sni_contexts()
@@ -4514,7 +4495,6 @@ class ThreadedTests(unittest.TestCase):
'Session refers to a different SSLContext.')
@requires_tls_version('TLSv1_2')
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipUnless(ssl.HAS_PSK, 'TLS-PSK disabled on this OpenSSL build')
def test_psk(self):
psk = bytes.fromhex('deadbeef')
@@ -4583,7 +4563,6 @@ class ThreadedTests(unittest.TestCase):
s.connect((HOST, server.port))
@requires_tls_version('TLSv1_3')
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipUnless(ssl.HAS_PSK, 'TLS-PSK disabled on this OpenSSL build')
def test_psk_tls1_3(self):
psk = bytes.fromhex('deadbeef')
@@ -4616,6 +4595,43 @@ class ThreadedTests(unittest.TestCase):
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))
@unittest.skip("TODO: rustpython")
def test_thread_recv_while_main_thread_sends(self):
# GH-137583: Locking was added to calls to send() and recv() on SSL
# socket objects. This seemed fine at the surface level because those
# calls weren't re-entrant, but recv() calls would implicitly mimick
# holding a lock by blocking until it received data. This means that
# if a thread started to infinitely block until data was received, calls
# to send() would deadlock, because it would wait forever on the lock
# that the recv() call held.
data = b"1" * 1024
event = threading.Event()
def background(sock):
event.set()
received = sock.recv(len(data))
self.assertEqual(received, data)
client_context, server_context, hostname = testing_context()
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as sock:
sock.connect((HOST, server.port))
sock.settimeout(1)
sock.setblocking(1)
# Ensure that the server is ready to accept requests
sock.sendall(b"123")
self.assertEqual(sock.recv(3), b"123")
with threading_helper.catch_threading_exception() as cm:
thread = threading.Thread(target=background,
args=(sock,), daemon=True)
thread.start()
event.wait()
sock.sendall(data)
thread.join()
if cm.exc_value is not None:
raise cm.exc_value
@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3")
class TestPostHandshakeAuth(unittest.TestCase):
@@ -4736,6 +4752,7 @@ class TestPostHandshakeAuth(unittest.TestCase):
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'TRUE\n')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_pha_optional_nocert(self):
if support.verbose:
sys.stdout.write("\n")
@@ -4775,6 +4792,7 @@ class TestPostHandshakeAuth(unittest.TestCase):
s.write(b'PHA')
self.assertIn(b'extension not received', s.recv(1024))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_pha_no_pha_server(self):
# server doesn't have PHA enabled, cert is requested in handshake
client_context, server_context, hostname = testing_context()
@@ -4844,7 +4862,6 @@ class TestPostHandshakeAuth(unittest.TestCase):
# server cert has not been validated
self.assertEqual(s.getpeercert(), {})
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_internal_chain_client(self):
client_context, server_context, hostname = testing_context(
server_chain=False
@@ -4916,7 +4933,6 @@ class TestPostHandshakeAuth(unittest.TestCase):
self.assertEqual(ee, uvc[0])
self.assertNotEqual(ee, ca)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_internal_chain_server(self):
client_context, server_context, hostname = testing_context()
client_context.load_cert_chain(SIGNED_CERTFILE)
@@ -5040,7 +5056,6 @@ class TestSSLDebug(unittest.TestCase):
ctx = ssl._create_stdlib_context()
self.assertEqual(ctx.keylog_filename, os_helper.TESTFN)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_msg_callback(self):
client_context, server_context, hostname = testing_context()
@@ -5085,7 +5100,6 @@ class TestSSLDebug(unittest.TestCase):
msg
)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_msg_callback_deadlock_bpo43577(self):
client_context, server_context, hostname = testing_context()
server_context2 = testing_context()[1]

View File

@@ -66,17 +66,11 @@ Welcome to the magnificent Rust Python interpreter
>>>>>
```
If you'd like to make https requests, you can enable the `ssl` feature, which
also lets you install the `pip` package manager. Note that on Windows, you may
need to install OpenSSL, or you can enable the `ssl-vendor` feature instead,
which compiles OpenSSL for you but requires a C compiler, perl, and `make`.
OpenSSL version 3 is expected and tested in CI. Older versions may not work.
Once you've installed rustpython with SSL support, you can install pip by
You can install pip by
running:
```bash
cargo install --git https://github.com/RustPython/RustPython --features ssl
cargo install --git https://github.com/RustPython/RustPython
rustpython --install-pip
```
@@ -88,6 +82,13 @@ conda install rustpython -c conda-forge
rustpython
```
### SSL provider
For HTTPS requests, `ssl-rustls` feature is enabled by default. You can replace it with `ssl-openssl` feature if your environment requires OpenSSL.
Note that to use OpenSSL on Windows, you may need to install OpenSSL, or you can enable the `ssl-vendor` feature instead,
which compiles OpenSSL for you but requires a C compiler, perl, and `make`.
OpenSSL version 3 is expected and tested in CI. Older versions may not work.
### WASI
You can compile RustPython to a standalone WebAssembly WASI module so it can run anywhere.

View File

@@ -520,7 +520,7 @@ impl ExecutingFrame<'_> {
trace!(" {:#?}", self);
trace!(
" Executing op code: {}",
instruction.display(arg, &self.code.code).to_string()
instruction.display(arg, &self.code.code)
);
trace!("=======");
}

View File

@@ -59,6 +59,14 @@ pub use rustpython_vm as vm;
pub use settings::{InstallPipMode, RunMode, parse_opts};
pub use shell::run_shell;
#[cfg(all(
feature = "ssl",
not(any(feature = "ssl-rustls", feature = "ssl-openssl"))
))]
compile_error!(
"Feature \"ssl\" is now enabled by either \"ssl-rustls\" or \"ssl-openssl\" to be enabled. Do not manually pass \"ssl\" feature. To enable ssl-openssl, use --no-default-features to disable ssl-rustls"
);
/// The main cli of the `rustpython` interpreter. This function will return `std::process::ExitCode`
/// based on the return code of the python code ran through the cli.
pub fn run(init: impl FnOnce(&mut VirtualMachine) + 'static) -> ExitCode {
@@ -141,7 +149,7 @@ __import__("io").TextIOWrapper(
}
fn install_pip(installer: InstallPipMode, scope: Scope, vm: &VirtualMachine) -> PyResult<()> {
if cfg!(not(feature = "ssl")) {
if !cfg!(feature = "ssl") {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.system_error.to_owned(),
"install-pip requires rustpython be build with '--features=ssl'".to_owned(),

View File

@@ -15,8 +15,12 @@ default = ["compiler"]
compiler = ["rustpython-vm/compiler"]
threading = ["rustpython-common/threading", "rustpython-vm/threading"]
sqlite = ["dep:libsqlite3-sys"]
ssl = ["openssl", "openssl-sys", "foreign-types-shared", "openssl-probe"]
ssl-vendor = ["ssl", "openssl/vendored"]
# SSL backends - default to rustls
ssl = []
ssl-rustls = ["ssl", "rustls", "rustls-native-certs", "rustls-pemfile", "rustls-platform-verifier", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "aws-lc-rs", "oid-registry", "pkcs8"]
ssl-rustls-fips = ["ssl-rustls", "aws-lc-rs/fips"]
ssl-openssl = ["ssl", "openssl", "openssl-sys", "foreign-types-shared", "openssl-probe"]
ssl-vendor = ["ssl-openssl", "openssl/vendored"]
tkinter = ["dep:tk-sys", "dep:tcl-sys"]
[dependencies]
@@ -86,6 +90,7 @@ bzip2 = "0.6"
# tkinter
tk-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0", optional = true }
tcl-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0", optional = true }
chrono.workspace = true
# uuid
[target.'cfg(not(any(target_os = "ios", target_os = "android", target_os = "windows", target_arch = "wasm32", target_os = "redox")))'.dependencies]
@@ -107,11 +112,27 @@ rustix = { workspace = true }
gethostname = "1.0.2"
socket2 = { version = "0.6.0", features = ["all"] }
dns-lookup = "3.0"
# OpenSSL dependencies (optional, for ssl-openssl feature)
openssl = { version = "0.10.72", optional = true }
openssl-sys = { version = "0.9.110", optional = true }
openssl-probe = { version = "0.1.5", optional = true }
foreign-types-shared = { version = "0.1.1", optional = true }
# Rustls dependencies (optional, for ssl-rustls feature)
rustls = { version = "0.23.35", default-features = false, features = ["std", "tls12", "aws_lc_rs"], optional = true }
rustls-native-certs = { version = "0.8", optional = true }
rustls-pemfile = { version = "2.2", optional = true }
rustls-platform-verifier = { version = "0.6", optional = true }
x509-cert = { version = "0.2.5", features = ["pem", "builder"], optional = true }
x509-parser = { version = "0.16", optional = true }
der = { version = "0.7", features = ["alloc", "oid"], optional = true }
pem-rfc7468 = { version = "0.7", optional = true }
webpki-roots = { version = "0.26", optional = true }
aws-lc-rs = { version = "1.14.1", optional = true }
oid-registry = { version = "0.7", features = ["x509", "pkcs1", "nist_algs"], optional = true }
pkcs8 = { version = "0.10", features = ["encryption", "pkcs5", "pem"], optional = true }
[target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies]
libsqlite3-sys = { version = "0.28", features = ["bundled"], optional = true }
lzma-sys = "0.1"

View File

@@ -23,25 +23,28 @@ fn main() {
println!("cargo::rustc-check-cfg=cfg({cfg})");
}
#[allow(clippy::unusual_byte_groupings)]
if let Ok(v) = std::env::var("DEP_OPENSSL_VERSION_NUMBER") {
println!("cargo:rustc-env=OPENSSL_API_VERSION={v}");
// cfg setup from openssl crate's build script
let version = u64::from_str_radix(&v, 16).unwrap();
for (ver, cfg) in ossl_vers {
if version >= ver {
println!("cargo:rustc-cfg={cfg}");
#[cfg(feature = "ssl-openssl")]
{
#[allow(clippy::unusual_byte_groupings)]
if let Ok(v) = std::env::var("DEP_OPENSSL_VERSION_NUMBER") {
println!("cargo:rustc-env=OPENSSL_API_VERSION={v}");
// cfg setup from openssl crate's build script
let version = u64::from_str_radix(&v, 16).unwrap();
for (ver, cfg) in ossl_vers {
if version >= ver {
println!("cargo:rustc-cfg={cfg}");
}
}
}
}
if let Ok(v) = std::env::var("DEP_OPENSSL_CONF") {
for conf in v.split(',') {
println!("cargo:rustc-cfg=osslconf=\"{conf}\"");
if let Ok(v) = std::env::var("DEP_OPENSSL_CONF") {
for conf in v.split(',') {
println!("cargo:rustc-cfg=osslconf=\"{conf}\"");
}
}
// it's possible for openssl-sys to link against the system openssl under certain conditions,
// so let the ssl module know to only perform a probe if we're actually vendored
if std::env::var("DEP_OPENSSL_VENDORED").is_ok_and(|s| s == "1") {
println!("cargo::rustc-cfg=openssl_vendored")
}
}
// it's possible for openssl-sys to link against the system openssl under certain conditions,
// so let the ssl module know to only perform a probe if we're actually vendored
if std::env::var("DEP_OPENSSL_VENDORED").is_ok_and(|s| s == "1") {
println!("cargo::rustc-cfg=openssl_vendored")
}
}

View File

@@ -75,8 +75,14 @@ mod select;
not(any(target_os = "android", target_arch = "wasm32"))
))]
mod sqlite;
#[cfg(all(not(target_arch = "wasm32"), feature = "ssl"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "ssl-openssl"))]
mod openssl;
#[cfg(all(not(target_arch = "wasm32"), feature = "ssl-rustls"))]
mod ssl;
#[cfg(all(feature = "ssl-openssl", feature = "ssl-rustls"))]
compile_error!("features \"ssl-openssl\" and \"ssl-rustls\" are mutually exclusive");
#[cfg(all(unix, not(target_os = "redox"), not(target_os = "ios")))]
mod termios;
#[cfg(not(any(
@@ -167,10 +173,14 @@ pub fn get_module_inits() -> impl Iterator<Item = (Cow<'static, str>, StdlibInit
{
"_sqlite3" => sqlite::make_module,
}
#[cfg(feature = "ssl")]
#[cfg(all(not(target_arch = "wasm32"), feature = "ssl-rustls"))]
{
"_ssl" => ssl::make_module,
}
#[cfg(all(not(target_arch = "wasm32"), feature = "ssl-openssl"))]
{
"_ssl" => openssl::make_module,
}
#[cfg(windows)]
{
"_overlapped" => overlapped::make_module,

3705
stdlib/src/openssl.rs Normal file

File diff suppressed because it is too large Load Diff

229
stdlib/src/openssl/cert.rs Normal file
View File

@@ -0,0 +1,229 @@
pub(super) use ssl_cert::{PySSLCertificate, cert_to_certificate, cert_to_py, obj2txt};
// Certificate type for SSL module
#[pymodule(sub)]
pub(crate) mod ssl_cert {
use crate::{
common::ascii,
vm::{
PyObjectRef, PyPayload, PyResult, VirtualMachine,
convert::{ToPyException, ToPyObject},
function::{FsPath, OptionalArg},
},
};
use foreign_types_shared::ForeignTypeRef;
use openssl::{
asn1::Asn1ObjectRef,
x509::{self, X509, X509Ref},
};
use openssl_sys as sys;
use std::fmt;
// Import constants and error converter from _ssl module
use crate::openssl::_ssl::{ENCODING_DER, ENCODING_PEM, convert_openssl_error};
pub(crate) fn obj2txt(obj: &Asn1ObjectRef, no_name: bool) -> Option<String> {
let no_name = i32::from(no_name);
let ptr = obj.as_ptr();
let b = unsafe {
let buflen = sys::OBJ_obj2txt(std::ptr::null_mut(), 0, ptr, no_name);
assert!(buflen >= 0);
if buflen == 0 {
return None;
}
let buflen = buflen as usize;
let mut buf = Vec::<u8>::with_capacity(buflen + 1);
let ret = sys::OBJ_obj2txt(
buf.as_mut_ptr() as *mut libc::c_char,
buf.capacity() as _,
ptr,
no_name,
);
assert!(ret >= 0);
// SAFETY: OBJ_obj2txt initialized the buffer successfully
buf.set_len(buflen);
buf
};
let s = String::from_utf8(b)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
Some(s)
}
#[pyattr]
#[pyclass(module = "ssl", name = "Certificate")]
#[derive(PyPayload)]
pub(crate) struct PySSLCertificate {
cert: X509,
}
impl fmt::Debug for PySSLCertificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad("Certificate")
}
}
#[pyclass]
impl PySSLCertificate {
#[pymethod]
fn public_bytes(
&self,
format: OptionalArg<i32>,
vm: &VirtualMachine,
) -> PyResult<PyObjectRef> {
let format = format.unwrap_or(ENCODING_PEM);
match format {
ENCODING_DER => {
// DER encoding
let der = self
.cert
.to_der()
.map_err(|e| convert_openssl_error(vm, e))?;
Ok(vm.ctx.new_bytes(der).into())
}
ENCODING_PEM => {
// PEM encoding
let pem = self
.cert
.to_pem()
.map_err(|e| convert_openssl_error(vm, e))?;
Ok(vm.ctx.new_bytes(pem).into())
}
_ => Err(vm.new_value_error("Unsupported format")),
}
}
#[pymethod]
fn get_info(&self, vm: &VirtualMachine) -> PyResult {
cert_to_dict(vm, &self.cert)
}
}
fn name_to_py(vm: &VirtualMachine, name: &x509::X509NameRef) -> PyResult {
let list = name
.entries()
.map(|entry| {
let txt = obj2txt(entry.object(), false).to_pyobject(vm);
let asn1_str = entry.data();
let data_bytes = asn1_str.as_slice();
let data = match std::str::from_utf8(data_bytes) {
Ok(s) => vm.ctx.new_str(s.to_owned()),
Err(_) => vm
.ctx
.new_str(String::from_utf8_lossy(data_bytes).into_owned()),
};
Ok(vm.new_tuple(((txt, data),)).into())
})
.collect::<Result<_, _>>()?;
Ok(vm.ctx.new_tuple(list).into())
}
// Helper to convert X509 to dict (for getpeercert with binary=False)
fn cert_to_dict(vm: &VirtualMachine, cert: &X509Ref) -> PyResult {
let dict = vm.ctx.new_dict();
dict.set_item("subject", name_to_py(vm, cert.subject_name())?, vm)?;
dict.set_item("issuer", name_to_py(vm, cert.issuer_name())?, vm)?;
// X.509 version: OpenSSL uses 0-based (0=v1, 1=v2, 2=v3) but Python uses 1-based (1=v1, 2=v2, 3=v3)
dict.set_item("version", vm.new_pyobj(cert.version() + 1), vm)?;
let serial_num = cert
.serial_number()
.to_bn()
.and_then(|bn| bn.to_hex_str())
.map_err(|e| convert_openssl_error(vm, e))?;
dict.set_item(
"serialNumber",
vm.ctx.new_str(serial_num.to_owned()).into(),
vm,
)?;
dict.set_item(
"notBefore",
vm.ctx.new_str(cert.not_before().to_string()).into(),
vm,
)?;
dict.set_item(
"notAfter",
vm.ctx.new_str(cert.not_after().to_string()).into(),
vm,
)?;
if let Some(names) = cert.subject_alt_names() {
let san: Vec<PyObjectRef> = names
.iter()
.map(|gen_name| {
if let Some(email) = gen_name.email() {
vm.new_tuple((ascii!("email"), email)).into()
} else if let Some(dnsname) = gen_name.dnsname() {
vm.new_tuple((ascii!("DNS"), dnsname)).into()
} else if let Some(ip) = gen_name.ipaddress() {
// Parse IP address properly (IPv4 or IPv6)
let ip_str = if ip.len() == 4 {
// IPv4
format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
} else if ip.len() == 16 {
// IPv6 - format with all zeros visible (not compressed)
let ip_addr = std::net::Ipv6Addr::from(ip[0..16]);
let s = ip_addr.segments();
format!(
"{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}",
s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]
)
} else {
// Fallback for unexpected length
String::from_utf8_lossy(ip).into_owned()
};
vm.new_tuple((ascii!("IP Address"), ip_str)).into()
} else if let Some(uri) = gen_name.uri() {
vm.new_tuple((ascii!("URI"), uri)).into()
} else {
// Handle DirName, Registered ID, and othername
// Check if this is a directory name
if let Some(dirname) = gen_name.directory_name()
&& let Ok(py_name) = name_to_py(vm, dirname)
{
return vm.new_tuple((ascii!("DirName"), py_name)).into();
}
// TODO: Handle Registered ID (GEN_RID)
// CPython implementation uses i2t_ASN1_OBJECT to convert OID
// This requires accessing GENERAL_NAME union which is complex in Rust
// For now, we return <unsupported> for unhandled types
// For othername and other unsupported types
vm.new_tuple((ascii!("othername"), ascii!("<unsupported>")))
.into()
}
})
.collect();
dict.set_item("subjectAltName", vm.ctx.new_tuple(san).into(), vm)?;
};
Ok(dict.into())
}
// Helper to create Certificate object from X509
pub(crate) fn cert_to_certificate(vm: &VirtualMachine, cert: X509) -> PyResult {
Ok(PySSLCertificate { cert }.into_ref(&vm.ctx).into())
}
// For getpeercert() - returns bytes or dict depending on binary flag
pub(crate) fn cert_to_py(vm: &VirtualMachine, cert: &X509Ref, binary: bool) -> PyResult {
if binary {
let b = cert.to_der().map_err(|e| convert_openssl_error(vm, e))?;
Ok(vm.ctx.new_bytes(b).into())
} else {
cert_to_dict(vm, cert)
}
}
#[pyfunction]
pub(crate) fn _test_decode_cert(path: FsPath, vm: &VirtualMachine) -> PyResult {
let path = path.to_path_buf(vm)?;
let pem = std::fs::read(path).map_err(|e| e.to_pyexception(vm))?;
let x509 = X509::from_pem(&pem).map_err(|e| convert_openssl_error(vm, e))?;
cert_to_py(vm, &x509, false)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1786
stdlib/src/ssl/compat.rs Normal file

File diff suppressed because it is too large Load Diff

464
stdlib/src/ssl/oid.rs Normal file
View File

@@ -0,0 +1,464 @@
// spell-checker: disable
//! OID (Object Identifier) management for SSL/TLS
//!
//! This module provides OID lookup functionality compatible with CPython's ssl module.
//! It uses oid-registry crate for well-known OIDs while maintaining NID (Numerical Identifier)
//! mappings for CPython compatibility.
use oid_registry::asn1_rs::Oid;
use std::collections::HashMap;
/// OID entry with openssl-compatible metadata
#[derive(Debug, Clone)]
pub struct OidEntry {
/// NID (OpenSSL Numerical Identifier) - must match CPython/OpenSSL values
pub nid: i32,
/// Short name (e.g., "CN", "serverAuth")
pub short_name: &'static str,
/// Long name/description (e.g., "commonName", "TLS Web Server Authentication")
pub long_name: &'static str,
/// OID reference (static or dynamic)
pub oid: OidRef,
}
/// OID reference - either from oid-registry or runtime-created
#[derive(Debug, Clone)]
pub enum OidRef {
/// Static OID from oid-registry crate (stored as value)
Static(Oid<'static>),
/// OID string (for OIDs not in oid-registry) - parsed on demand
String(&'static str),
}
impl OidEntry {
/// Create entry from oid-registry static constant
pub fn from_static(
nid: i32,
short_name: &'static str,
long_name: &'static str,
oid: &Oid<'static>,
) -> Self {
Self {
nid,
short_name,
long_name,
oid: OidRef::Static(oid.clone()),
}
}
/// Create entry from OID string (for OIDs not in oid-registry)
pub const fn from_string(
nid: i32,
short_name: &'static str,
long_name: &'static str,
oid_str: &'static str,
) -> Self {
Self {
nid,
short_name,
long_name,
oid: OidRef::String(oid_str),
}
}
/// Get OID as string (e.g., "2.5.4.3")
pub fn oid_string(&self) -> String {
match &self.oid {
OidRef::Static(oid) => oid.to_id_string(),
OidRef::String(s) => s.to_string(),
}
}
}
/// OID table with multiple indices for fast lookup
pub struct OidTable {
/// All entries
entries: Vec<OidEntry>,
/// NID -> index mapping
nid_to_idx: HashMap<i32, usize>,
/// Short name -> index mapping
short_name_to_idx: HashMap<&'static str, usize>,
/// Long name -> index mapping (case-insensitive)
long_name_to_idx: HashMap<String, usize>,
/// OID string -> index mapping
oid_str_to_idx: HashMap<String, usize>,
}
impl OidTable {
fn build() -> Self {
let entries = build_oid_entries();
let mut nid_to_idx = HashMap::with_capacity(entries.len());
let mut short_name_to_idx = HashMap::with_capacity(entries.len());
let mut long_name_to_idx = HashMap::with_capacity(entries.len());
let mut oid_str_to_idx = HashMap::with_capacity(entries.len());
for (idx, entry) in entries.iter().enumerate() {
nid_to_idx.insert(entry.nid, idx);
short_name_to_idx.insert(entry.short_name, idx);
long_name_to_idx.insert(entry.long_name.to_lowercase(), idx);
oid_str_to_idx.insert(entry.oid_string(), idx);
}
Self {
entries,
nid_to_idx,
short_name_to_idx,
long_name_to_idx,
oid_str_to_idx,
}
}
pub fn find_by_nid(&self, nid: i32) -> Option<&OidEntry> {
self.nid_to_idx.get(&nid).map(|&idx| &self.entries[idx])
}
pub fn find_by_oid_string(&self, oid_str: &str) -> Option<&OidEntry> {
self.oid_str_to_idx
.get(oid_str)
.map(|&idx| &self.entries[idx])
}
pub fn find_by_name(&self, name: &str) -> Option<&OidEntry> {
// Try short name first (exact match)
self.short_name_to_idx
.get(name)
.or_else(|| {
// Try long name (case-insensitive)
self.long_name_to_idx.get(&name.to_lowercase())
})
.map(|&idx| &self.entries[idx])
}
}
/// Global OID table
static OID_TABLE: std::sync::LazyLock<OidTable> = std::sync::LazyLock::new(OidTable::build);
/// Macro to define OID entry using oid-registry constant
macro_rules! oid_static {
($nid:expr, $short:expr, $long:expr, $oid_const:path) => {
OidEntry::from_static($nid, $short, $long, &$oid_const)
};
}
/// Macro to define OID entry from string
macro_rules! oid_string {
($nid:expr, $short:expr, $long:expr, $oid_str:expr) => {
OidEntry::from_string($nid, $short, $long, $oid_str)
};
}
/// Build the complete OID table
fn build_oid_entries() -> Vec<OidEntry> {
vec![
// Priority 1: X.509 DN Attributes (OpenSSL NID values)
// These NIDs MUST match OpenSSL for CPython compatibility
oid_static!(13, "CN", "commonName", oid_registry::OID_X509_COMMON_NAME),
oid_static!(14, "C", "countryName", oid_registry::OID_X509_COUNTRY_NAME),
oid_static!(
15,
"L",
"localityName",
oid_registry::OID_X509_LOCALITY_NAME
),
oid_static!(
16,
"ST",
"stateOrProvinceName",
oid_registry::OID_X509_STATE_OR_PROVINCE_NAME
),
oid_static!(
17,
"O",
"organizationName",
oid_registry::OID_X509_ORGANIZATION_NAME
),
oid_static!(
18,
"OU",
"organizationalUnitName",
oid_registry::OID_X509_ORGANIZATIONAL_UNIT
),
oid_static!(41, "name", "name", oid_registry::OID_X509_NAME),
oid_static!(42, "GN", "givenName", oid_registry::OID_X509_GIVEN_NAME),
oid_static!(43, "initials", "initials", oid_registry::OID_X509_INITIALS),
oid_static!(
4,
"serialNumber",
"serialNumber",
oid_registry::OID_X509_SERIALNUMBER
),
oid_static!(100, "surname", "surname", oid_registry::OID_X509_SURNAME),
// emailAddress is special - it's in PKCS#9, not X.509
oid_static!(
48,
"emailAddress",
"emailAddress",
oid_registry::OID_PKCS9_EMAIL_ADDRESS
),
// Priority 2: X.509 Extensions (Critical ones)
oid_static!(
82,
"subjectKeyIdentifier",
"X509v3 Subject Key Identifier",
oid_registry::OID_X509_EXT_SUBJECT_KEY_IDENTIFIER
),
oid_static!(
83,
"keyUsage",
"X509v3 Key Usage",
oid_registry::OID_X509_EXT_KEY_USAGE
),
oid_static!(
85,
"subjectAltName",
"X509v3 Subject Alternative Name",
oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME
),
oid_static!(
86,
"issuerAltName",
"X509v3 Issuer Alternative Name",
oid_registry::OID_X509_EXT_ISSUER_ALT_NAME
),
oid_static!(
87,
"basicConstraints",
"X509v3 Basic Constraints",
oid_registry::OID_X509_EXT_BASIC_CONSTRAINTS
),
oid_static!(
88,
"crlNumber",
"X509v3 CRL Number",
oid_registry::OID_X509_EXT_CRL_NUMBER
),
oid_static!(
90,
"authorityKeyIdentifier",
"X509v3 Authority Key Identifier",
oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER
),
oid_static!(
126,
"extendedKeyUsage",
"X509v3 Extended Key Usage",
oid_registry::OID_X509_EXT_EXTENDED_KEY_USAGE
),
oid_static!(
103,
"crlDistributionPoints",
"X509v3 CRL Distribution Points",
oid_registry::OID_X509_EXT_CRL_DISTRIBUTION_POINTS
),
oid_static!(
89,
"certificatePolicies",
"X509v3 Certificate Policies",
oid_registry::OID_X509_EXT_CERTIFICATE_POLICIES
),
oid_static!(
177,
"authorityInfoAccess",
"Authority Information Access",
oid_registry::OID_PKIX_AUTHORITY_INFO_ACCESS
),
oid_static!(
105,
"nameConstraints",
"X509v3 Name Constraints",
oid_registry::OID_X509_EXT_NAME_CONSTRAINTS
),
// Priority 3: Extended Key Usage OIDs (not in oid-registry)
// These are defined in RFC 5280 but not in oid-registry, so we use strings
oid_string!(
129,
"serverAuth",
"TLS Web Server Authentication",
"1.3.6.1.5.5.7.3.1"
),
oid_string!(
130,
"clientAuth",
"TLS Web Client Authentication",
"1.3.6.1.5.5.7.3.2"
),
oid_string!(131, "codeSigning", "Code Signing", "1.3.6.1.5.5.7.3.3"),
oid_string!(
132,
"emailProtection",
"E-mail Protection",
"1.3.6.1.5.5.7.3.4"
),
oid_string!(133, "timeStamping", "Time Stamping", "1.3.6.1.5.5.7.3.8"),
oid_string!(180, "OCSPSigning", "OCSP Signing", "1.3.6.1.5.5.7.3.9"),
// Priority 4: Signature Algorithms
oid_static!(
6,
"rsaEncryption",
"rsaEncryption",
oid_registry::OID_PKCS1_RSAENCRYPTION
),
oid_static!(
65,
"sha1WithRSAEncryption",
"sha1WithRSAEncryption",
oid_registry::OID_PKCS1_SHA1WITHRSA
),
oid_static!(
668,
"sha256WithRSAEncryption",
"sha256WithRSAEncryption",
oid_registry::OID_PKCS1_SHA256WITHRSA
),
oid_static!(
669,
"sha384WithRSAEncryption",
"sha384WithRSAEncryption",
oid_registry::OID_PKCS1_SHA384WITHRSA
),
oid_static!(
670,
"sha512WithRSAEncryption",
"sha512WithRSAEncryption",
oid_registry::OID_PKCS1_SHA512WITHRSA
),
oid_static!(
408,
"id-ecPublicKey",
"id-ecPublicKey",
oid_registry::OID_KEY_TYPE_EC_PUBLIC_KEY
),
oid_static!(
794,
"ecdsa-with-SHA256",
"ecdsa-with-SHA256",
oid_registry::OID_SIG_ECDSA_WITH_SHA256
),
oid_static!(
795,
"ecdsa-with-SHA384",
"ecdsa-with-SHA384",
oid_registry::OID_SIG_ECDSA_WITH_SHA384
),
oid_static!(
796,
"ecdsa-with-SHA512",
"ecdsa-with-SHA512",
oid_registry::OID_SIG_ECDSA_WITH_SHA512
),
// Priority 5: Hash Algorithms
oid_string!(64, "sha1", "sha1", "1.3.14.3.2.26"),
oid_static!(672, "sha256", "sha256", oid_registry::OID_NIST_HASH_SHA256),
oid_static!(673, "sha384", "sha384", oid_registry::OID_NIST_HASH_SHA384),
oid_static!(674, "sha512", "sha512", oid_registry::OID_NIST_HASH_SHA512),
oid_string!(675, "sha224", "sha224", "2.16.840.1.101.3.4.2.4"),
// Priority 6: Elliptic Curve OIDs
oid_static!(714, "secp256r1", "secp256r1", oid_registry::OID_EC_P256),
oid_string!(715, "secp384r1", "secp384r1", "1.3.132.0.34"),
oid_string!(716, "secp521r1", "secp521r1", "1.3.132.0.35"),
oid_string!(1172, "X25519", "X25519", "1.3.101.110"),
oid_string!(1173, "Ed25519", "Ed25519", "1.3.101.112"),
// Additional useful OIDs
oid_string!(
183,
"subjectInfoAccess",
"Subject Information Access",
"1.3.6.1.5.5.7.1.11"
),
oid_string!(920, "OCSP", "OCSP", "1.3.6.1.5.5.7.48.1"),
oid_string!(921, "caIssuers", "CA Issuers", "1.3.6.1.5.5.7.48.2"),
]
}
// Public API Functions
/// Find OID entry by NID
pub fn find_by_nid(nid: i32) -> Option<&'static OidEntry> {
OID_TABLE.find_by_nid(nid)
}
/// Find OID entry by OID string (e.g., "2.5.4.3")
pub fn find_by_oid_string(oid_str: &str) -> Option<&'static OidEntry> {
OID_TABLE.find_by_oid_string(oid_str)
}
/// Find OID entry by name (short or long name)
pub fn find_by_name(name: &str) -> Option<&'static OidEntry> {
OID_TABLE.find_by_name(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_by_nid() {
let entry = find_by_nid(13).unwrap();
assert_eq!(entry.short_name, "CN");
assert_eq!(entry.long_name, "commonName");
assert_eq!(entry.oid_string(), "2.5.4.3");
}
#[test]
fn test_find_by_oid_string() {
let entry = find_by_oid_string("2.5.4.3").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.short_name, "CN");
}
#[test]
fn test_find_by_name_short() {
let entry = find_by_name("CN").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.oid_string(), "2.5.4.3");
}
#[test]
fn test_find_by_name_long() {
let entry = find_by_name("commonName").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.short_name, "CN");
}
#[test]
fn test_find_by_name_case_insensitive() {
let entry = find_by_name("COMMONNAME").unwrap();
assert_eq!(entry.nid, 13);
}
#[test]
fn test_subject_alt_name() {
let entry = find_by_nid(85).unwrap();
assert_eq!(entry.short_name, "subjectAltName");
assert_eq!(entry.oid_string(), "2.5.29.17");
}
#[test]
fn test_server_auth_eku() {
let entry = find_by_nid(129).unwrap();
assert_eq!(entry.short_name, "serverAuth");
assert_eq!(entry.oid_string(), "1.3.6.1.5.5.7.3.1");
}
#[test]
fn test_no_duplicate_nids() {
let table = &*OID_TABLE;
assert_eq!(
table.entries.len(),
table.nid_to_idx.len(),
"Duplicate NIDs detected!"
);
}
#[test]
fn test_oid_count() {
let table = &*OID_TABLE;
// We should have 50+ OIDs defined
assert!(
table.entries.len() >= 50,
"Expected at least 50 OIDs, got {}",
table.entries.len()
);
}
}