mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Replace SSL backend to rustls (#6244)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -50,6 +50,7 @@ nanos
|
||||
nonoverlapping
|
||||
objclass
|
||||
peekable
|
||||
pemfile
|
||||
powc
|
||||
powf
|
||||
powi
|
||||
@@ -61,6 +62,7 @@ rposition
|
||||
rsplitn
|
||||
rustc
|
||||
rustfmt
|
||||
rustls
|
||||
rustyline
|
||||
seedable
|
||||
seekfrom
|
||||
|
||||
17
.github/workflows/ci.yaml
vendored
17
.github/workflows/ci.yaml
vendored
@@ -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
825
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
68
Lib/test/test_ssl.py
vendored
@@ -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]
|
||||
|
||||
17
README.md
17
README.md
@@ -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.
|
||||
|
||||
@@ -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!("=======");
|
||||
}
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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
229
stdlib/src/openssl/cert.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
7142
stdlib/src/ssl.rs
7142
stdlib/src/ssl.rs
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
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
464
stdlib/src/ssl/oid.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user