ssl: take the OID table and the OpenSSL cipher-string parser from #8007 (#8646)

* ssl: read the OID table out of OpenSSL's object database

`txt2obj` and `nid2obj` have to answer with OpenSSL's own numbers, and the
table behind them was 39 entries transcribed by hand, so every object
outside that set was reported unknown.

Read OpenSSL's own files instead: `obj_mac.num` carries each object's NID
and `objects.txt` carries its OID, short name and description, and the
identifier `objects.pl` derives per object -- the `!Cname` override, else
the description, else the short name, each qualified by the enclosing
`!module` with `-` spelled `_` -- joins the two.  `OidEntry`, `OidTable`
and the `find_by_*` functions keep their shape, so `ssl.rs` only changes
where an object has no OID to report.

Three details the object database settles that a curated table did not
have to:

- an object written with one name carries it as both, so
  `nid2obj(48)` is `('emailAddress', 'emailAddress', ...)`;
- an object need not have an OID at all -- `DES-EDE3` and 139 other
  algorithm names have a NID and nothing to encode -- so the fourth
  field of the tuple is None for those, as it is on OpenSSL;
- one arc does not encode, since DER packs the first two into a byte,
  so the bare roots `iso` and `itu-t` are OID-less too, while two arcs
  such as X500's `2.5` are a real object.

Checked against `_ssl.nid2obj` on CPython built with OpenSSL, over every
NID it answers: 1387 of its 1390 present, with no disagreement on any of
them, up from 39.  The three absent are OpenSSL's unnamed placeholder
slots, which it reports as ('NULL', 'NULL', None).

The data files come from openssl/openssl `crypto/objects/`, via the
rustls integration rewrite in #8007.

Co-authored-by: Ivan Mironov <mironov.ivan@gmail.com>
Assisted-by: Claude

* ssl: parse OpenSSL cipher strings in set_ciphers

Replaces the pattern matcher `set_ciphers` used with the grammar
`man openssl-ciphers` documents, in a new `ssl/cipher.rs` taken from
#8007: `!`, `-`, `+`, `@STRENGTH`, `@SECLEVEL=n`, `+`-joined
conjunctions, the DEFAULT/ALL/COMPLEMENTOF* groups, protocol versions,
SUITEB*, and the aXXX/kXXX/eXXX prefixes.

`set_ciphers` raises "No cipher can be selected." for a string that
names nothing the provider offers, which is the one failure
SSL_CTX_set_cipher_list reports, and keeps the TLS 1.3 suites selected
in provider order because that setter does not reach them. A SUITEB*
string pins its own key exchange groups, which `prepare_kx_groups`
now reads.

Against CPython's OpenSSL over 78 cipher strings, the selections agree
except under `@SECLEVEL=n`, which OpenSSL applies at handshake time and
this applies to the selection.

`CryptoExt` gains `default_cipher_suites` so that `DEFAULT` and `ALL`
are distinguishable.

Co-authored-by: Ivan Mironov <mironov.ivan@gmail.com>
Assisted-by: Claude

* ssl: finish cipher reporting and provider handling

Use the provider-ordered cipher metadata table as the single source for parsing, reporting, OpenSSL names and descriptions. This makes get_ciphers reflect the context selection and validates curves and Suite B against the active provider.

Keep set_ciphers from changing TLS 1.3 suites, preserve the provider's order, fix empty conjunctions, and keep FIPS initialization within the provider's filtered cipher and key-exchange defaults.

Co-authored-by: Ivan Mironov <mironov.ivan@gmail.com>

Assisted-by: Claude

Assisted-by: CodeRabbit

Assisted-by: OpenAI Codex: GPT-5

* ssl: preserve OpenSSL OID lookup rules

Keep object-name lookup case-sensitive and canonicalize numeric OID arcs before consulting the generated OpenSSL object table.

Assisted-by: OpenAI Codex: GPT-5

* ssl: preserve cleartext after MemoryBIO shutdown

Read at most one TLS record while waiting for close_notify in BIO mode, leaving any post-TLS cleartext queued for the BIO owner.

Co-authored-by: Ivan Mironov <mironov.ivan@gmail.com>

Assisted-by: OpenAI Codex: GPT-5

* ssl: reject empty cipher conjunction operands

Keep empty operands visible to the parser so malformed OpenSSL cipher strings such as AES+ and ALL++AES are rejected instead of normalized.

Assisted-by: CodeRabbit

Assisted-by: OpenAI Codex: GPT-5

* ssl: make Suite B cipher ids unambiguous

Spell out the u16 conversion so Windows-only iterator implementations cannot make collection type inference ambiguous.

Assisted-by: OpenAI Codex: GPT-5

* ssl: move native certificate loading behind host_env

Keep platform trust-store loading and filesystem predicates on the host boundary while exposing backend-neutral certificate bytes to the rustls integration.

Assisted-by: OpenAI Codex: GPT-5

* ssl: invalidate server config after cipher changes

Rebuild cached server configuration after set_ciphers updates the selected suites or Suite B key-exchange groups.

Assisted-by: CodeRabbit

Assisted-by: OpenAI Codex: GPT-5

* ssl: load default certificate sources independently

Honor SSL_CERT_FILE and SSL_CERT_DIR independently. Keep directory certificates as lazy capath candidates, and fall back to native roots only when no environment certificate file was loaded.

Co-authored-by: Ivan Mironov <mironov.ivan@gmail.com>

Assisted-by: OpenAI Codex: GPT-5

---------

Co-authored-by: Ivan Mironov <mironov.ivan@gmail.com>
This commit is contained in:
Jeong, YunWonandIvan Mironov authored and GitHub committed 2026-09-04 22:49:06 +09:00
1 parent ba19382018
commit de6053099a
17 files changed
+5061 -681

No files matched your search

+6
View File
@@ -71,8 +71,11 @@
// words - list of words to be always considered correct
// (compound words like pyarg, baseclass, microbenchmark are handled by allowCompoundWords)
"words": [
"AESGCM",
"aiterable",
"alnum",
"COMPLEMENTOFALL",
"COMPLEMENTOFDEFAULT",
"csock",
"coro",
"contig",
@@ -82,6 +85,7 @@
"deduped",
"deoptimized",
"deoptimize",
"EECDH",
"emscripten",
"excs",
"fdigits",
@@ -98,6 +102,7 @@
"kwonly",
"lossily",
"mcache",
"MLKEM",
"oparg",
"opargs",
"pointee",
@@ -109,6 +114,7 @@
"reraises",
"reraising",
"significand",
"SUITEB",
"summands",
"TESTFN",
"TZPATH",
Generated
+1 -1
View File
@@ -3463,6 +3463,7 @@ dependencies = [
"parking_lot",
"paste",
"rustix",
"rustls-native-certs",
"rustpython-wtf8",
"rustyline",
"schannel",
@@ -3628,7 +3629,6 @@ dependencies = [
"rand 0.10.2",
"rapidhash",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"rustls-platform-verifier",
"rustpython-common",
+4
View File
@@ -8,6 +8,9 @@ rust-version.workspace = true
repository.workspace = true
license.workspace = true
[features]
native-certs = ["dep:rustls-native-certs"]
[dependencies]
rustpython-wtf8 = { workspace = true }
@@ -35,6 +38,7 @@ mac_address = { workspace = true }
dns-lookup = { workspace = true }
gethostname = { workspace = true }
rustyline = { workspace = true }
rustls-native-certs = { workspace = true, optional = true }
socket2 = { workspace = true, features = ["all"] }
which = { workspace = true }
+12
View File
@@ -36,6 +36,18 @@ pub fn metadata(path: impl AsRef<Path>) -> io::Result<Metadata> {
fs::metadata(path)
}
pub fn exists(path: impl AsRef<Path>) -> bool {
metadata(path).is_ok()
}
pub fn is_file(path: impl AsRef<Path>) -> bool {
metadata(path).is_ok_and(|metadata| metadata.is_file())
}
pub fn is_dir(path: impl AsRef<Path>) -> bool {
metadata(path).is_ok_and(|metadata| metadata.is_dir())
}
pub fn symlink_metadata(path: impl AsRef<Path>) -> io::Result<Metadata> {
fs::symlink_metadata(path)
}
+2
View File
@@ -30,6 +30,8 @@ pub mod fileutils;
pub mod fs;
#[cfg(any(unix, windows))]
pub mod locale;
#[cfg(all(feature = "native-certs", not(target_arch = "wasm32")))]
pub mod native_certs;
pub mod readline;
#[cfg(windows)]
+22
View File
@@ -0,0 +1,22 @@
/// Certificates and errors returned by the platform's native trust-store
/// loader. Certificate bytes stay independent of any TLS backend type.
pub struct LoadResult {
pub certs: Vec<Vec<u8>>,
pub errors: Vec<String>,
}
pub fn load() -> LoadResult {
let result = rustls_native_certs::load_native_certs();
LoadResult {
certs: result
.certs
.into_iter()
.map(|cert| cert.as_ref().to_vec())
.collect(),
errors: result
.errors
.into_iter()
.map(|error| error.to_string())
.collect(),
}
}
+1 -2
View File
@@ -24,7 +24,7 @@ ssl-openssl-vendor = ["ssl-openssl", "openssl/vendored"]
tkinter = ["dep:tk-sys", "dep:tcl-sys", "dep:widestring"]
flame-it = ["flame"]
__ssl-rustls = ["ssl", "rustls", "rustls-native-certs", "rustls-pemfile", "rustls-platform-verifier", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "oid-registry", "pkcs8"]
__ssl-rustls = ["ssl", "rustpython-host_env/native-certs", "rustls", "rustls-pemfile", "rustls-platform-verifier", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "oid-registry", "pkcs8"]
[dependencies]
# rustpython crates
@@ -92,7 +92,6 @@ foreign-types-shared = { workspace = true, optional = true }
# Rustls dependencies (optional, for ssl-rustls feature)
rustls = { workspace = true, default-features = false, features = ["std", "tls12"], optional = true }
rustls-native-certs = { workspace = true, optional = true }
rustls-pemfile = { workspace = true, optional = true }
rustls-platform-verifier = { workspace = true, optional = true }
x509-cert = { workspace = true, features = ["pem", "builder"], optional = true }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+119 -234
View File
@@ -19,6 +19,9 @@ mod oid;
// Certificate operations module (parsing, validation, conversion)
mod cert;
/// OpenSSL cipher string parsing for `set_ciphers`
mod cipher;
// OpenSSL compatibility layer (abstracts rustls operations)
mod compat;
@@ -94,13 +97,14 @@ mod _ssl {
use super::cert;
// Import OID module
use super::cipher;
use super::oid;
// Import compat module (OpenSSL compatibility layer)
use super::compat::{
ClientConfigOptions, MultiCertResolver, ProtocolSettings, ServerConfigOptions, SslError,
create_client_config, create_server_config, curve_name_to_kx_group, extract_cipher_info,
get_cipher_encryption_desc, is_blocking_io_error, normalize_cipher_name, ssl_do_handshake,
create_client_config, create_server_config, curve_name_to_kx_group, is_blocking_io_error,
ssl_do_handshake,
};
use super::providers::CryptoExt;
@@ -332,10 +336,10 @@ mod _ssl {
#[pyattr]
const _OPENSSL_API_VERSION: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15);
// Default cipher list for rustls - using modern secure ciphers
#[pyattr]
const _DEFAULT_CIPHERS: &str =
"TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256";
#[pyattr(once)]
fn _DEFAULT_CIPHERS(_vm: &VirtualMachine) -> String {
cipher::default_cipher_string()
}
// Has features
#[pyattr]
@@ -659,114 +663,6 @@ mod _ssl {
Ok(alpn_list)
}
/// Parse OpenSSL cipher string to rustls SupportedCipherSuite list
///
/// Supports patterns like:
/// - "AES128" → filters for AES_128
/// - "AES256" → filters for AES_256
/// - "AES128:AES256" → both
/// - "ECDHE+AESGCM" → ECDHE AND AESGCM (both conditions must match)
/// - "ALL" or "DEFAULT" → all available
/// - "!MD5" → exclusion (ignored, rustls doesn't support weak ciphers anyway)
fn parse_cipher_string(cipher_str: &str) -> Result<Vec<rustls::SupportedCipherSuite>, String> {
if cipher_str.is_empty() {
return Err("No cipher can be selected".to_string());
}
let all_suites = CryptoExt::get_ext().all_ciphers_or_default();
let mut selected = Vec::new();
for part in cipher_str.split(':') {
let part = part.trim();
// Skip exclusions (rustls doesn't support these)
if part.starts_with('!') {
continue;
}
// Skip priority markers starting with +
if part.starts_with('+') {
continue;
}
// Match pattern
match part {
"ALL" | "DEFAULT" | "HIGH" => {
// Add all available cipher suites
selected.extend_from_slice(all_suites);
}
_ => {
// Check if this is a compound pattern with + (AND condition)
// e.g., "ECDHE+AESGCM" means ECDHE AND AESGCM
let patterns: Vec<&str> = part.split('+').collect();
let mut found_any = false;
for suite in all_suites {
let name = format!("{:?}", suite.suite());
// Check if all patterns match (AND condition)
let matches = patterns.iter().all(|&pattern| {
// Handle common OpenSSL pattern variations
if pattern.contains("AES128") {
name.contains("AES_128")
} else if pattern.contains("AES256") {
name.contains("AES_256")
} else if pattern == "AESGCM" {
// AESGCM: AES with GCM mode
name.contains("AES") && name.contains("GCM")
} else if pattern == "AESCCM" {
// AESCCM: AES with CCM mode
name.contains("AES") && name.contains("CCM")
} else if pattern == "CHACHA20" {
name.contains("CHACHA20")
} else if pattern == "ECDHE" {
name.contains("ECDHE")
} else if pattern == "DHE" {
// DHE but not ECDHE
name.contains("DHE") && !name.contains("ECDHE")
} else if pattern == "ECDH" {
// ECDH but not ECDHE
name.contains("ECDH") && !name.contains("ECDHE")
} else if pattern == "DH" {
// DH but not DHE or ECDH
name.contains("DH")
&& !name.contains("DHE")
&& !name.contains("ECDH")
} else if pattern == "RSA" {
name.contains("RSA")
} else if pattern == "AES" {
name.contains("AES")
} else if pattern == "ECDSA" {
name.contains("ECDSA")
} else {
// Direct substring match for other patterns
name.contains(pattern)
}
});
if matches {
selected.push(*suite);
found_any = true;
}
}
if !found_any {
// No matching cipher suite found - warn but continue
}
}
}
}
// Remove duplicates
selected.dedup_by_key(|s| s.suite());
if selected.is_empty() {
Err("No cipher can be selected".to_string())
} else {
Ok(selected)
}
}
// SSLContext - manages TLS configuration
#[pyattr]
#[pyclass(name = "_SSLContext", module = "ssl", traverse)]
@@ -851,6 +747,9 @@ mod _ssl {
/// Selected cipher suites (None = use all rustls defaults)
#[pytraverse(skip)]
selected_ciphers: PyRwLock<Option<Vec<rustls::SupportedCipherSuite>>>,
/// Key exchange groups a SUITEB* cipher string pinned (RFC 6460)
#[pytraverse(skip)]
suite_b_kx_groups: PyRwLock<Option<Vec<&'static dyn SupportedKxGroup>>>,
}
#[derive(FromArgs)]
@@ -1393,41 +1292,46 @@ mod _ssl {
/// Helper: Try to load certificates from Python's os.environ variables
///
/// Returns true if certificates were successfully loaded.
/// Returns true if certificates were successfully loaded from
/// `SSL_CERT_FILE`. `SSL_CERT_DIR` is an independent source and its
/// certificates remain hidden from store statistics until used.
///
/// We use Python's os.environ instead of Rust's std::env
/// because Python code can modify os.environ at runtime (e.g.,
/// `os.environ['SSL_CERT_FILE'] = '/path'`), but rustls-native-certs uses
/// std::env which only sees the process environment at startup.
/// `os.environ['SSL_CERT_FILE'] = '/path'`), but the native certificate
/// loader uses std::env which only sees the process environment.
fn try_load_from_python_environ(
&self,
loader: &mut cert::CertLoader<'_>,
store: &mut rustls::RootCertStore,
vm: &VirtualMachine,
) -> PyResult<bool> {
use std::path::Path;
let os_module = vm.import("os", 0)?;
let environ = os_module.get_attr("environ", vm)?;
// Try SSL_CERT_FILE first
let mut loaded_certs = Vec::new();
let mut loaded_file = false;
if let Ok(cert_file) = Self::get_env_path(&environ, "SSL_CERT_FILE", vm)
&& Path::new(&cert_file).exists()
&& let Ok(stats) = loader.load_from_file(&cert_file)
&& rustpython_host_env::fs::exists(&cert_file)
{
self.update_cert_stats(stats);
return Ok(true);
let mut loader = cert::CertLoader::new(store, &mut loaded_certs);
if let Ok(stats) = loader.load_from_file(&cert_file) {
self.update_cert_stats(stats);
loaded_file = true;
}
}
// Try SSL_CERT_DIR (only if SSL_CERT_FILE didn't work)
let file_cert_count = loaded_certs.len();
if let Ok(cert_dir) = Self::get_env_path(&environ, "SSL_CERT_DIR", vm)
&& Path::new(&cert_dir).is_dir()
&& let Ok(stats) = loader.load_from_dir(&cert_dir)
&& rustpython_host_env::fs::is_dir(&cert_dir)
{
self.update_cert_stats(stats);
return Ok(true);
let mut loader = cert::CertLoader::new(store, &mut loaded_certs);
if loader.load_from_dir(&cert_dir).is_ok() {
*self.capath_certs_der.write() = loaded_certs.split_off(file_cert_count);
}
}
Ok(false)
Ok(loaded_file)
}
/// Helper: Load system certificates using rustls-native-certs
@@ -1468,12 +1372,12 @@ mod _ssl {
#[cfg(not(windows))]
{
let result = rustls_native_certs::load_native_certs();
let result = rustpython_host_env::native_certs::load();
// Load successfully found certificates
for cert in result.certs {
let is_ca = cert::is_ca_certificate(cert.as_ref());
if store.add(cert).is_ok() {
let is_ca = cert::is_ca_certificate(&cert);
if store.add(cert.into()).is_ok() {
*self.x509_cert_count.write() += 1;
if is_ca {
*self.ca_cert_count.write() += 1;
@@ -1508,18 +1412,14 @@ mod _ssl {
// see: test_load_default_certs_env_windows
let _ = self.load_system_certificates(&mut store, vm);
let mut lazy_ca_certs = Vec::new();
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
let _ = self.try_load_from_python_environ(&mut loader, vm)?;
let _ = self.try_load_from_python_environ(&mut store, vm)?;
}
#[cfg(not(windows))]
{
// Non-Windows: Try env vars first; only fallback to system certs if not set
// see: test_load_default_certs_env
let mut lazy_ca_certs = Vec::new();
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
let loaded = self.try_load_from_python_environ(&mut loader, vm)?;
let loaded = self.try_load_from_python_environ(&mut store, vm)?;
if !loaded {
let _ = self.load_system_certificates(&mut store, vm);
@@ -1564,76 +1464,63 @@ mod _ssl {
#[pymethod]
fn set_ciphers(&self, ciphers: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<()> {
let cipher_str = ciphers.as_str();
// `SSL_CTX_set_cipher_list` reports one failure for a string it
// cannot read and for a readable one that selects nothing, and the
// TLS 1.3 suites are not among what it can select -- they have
// their own setter -- so a string naming only those selects
// nothing either.
let (mut selected_ciphers, suite_b_kx_groups) =
cipher::CipherList::parse_to_rustls(ciphers.as_str())
.ok()
.filter(|(suites, _)| suites.iter().any(|s| s.tls13().is_none()))
.ok_or_else(|| {
vm.new_os_subtype_error(
PySSLError::class(&vm.ctx).to_owned(),
None,
"No cipher can be selected.".to_owned(),
)
.upcast()
})?;
// Parse cipher string and store selected ciphers
let selected_ciphers = parse_cipher_string(cipher_str).map_err(|e| {
vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), None, e)
.upcast()
})?;
// TLS 1.3 has a separate OpenSSL setter. Discard whatever this
// cipher string happened to match there, then restore exactly the
// provider defaults in their preference order.
selected_ciphers = cipher::restore_default_tls13(
selected_ciphers,
CryptoExt::get_ext().default_ciphers_or_provider(),
);
// Store in context
*self.selected_ciphers.write() = Some(selected_ciphers);
*self.suite_b_kx_groups.write() = suite_b_kx_groups;
*self.server_config.write() = None;
Ok(())
}
#[pymethod]
fn get_ciphers(&self, vm: &VirtualMachine) -> PyListRef {
// Dynamically generate cipher list from rustls ALL_CIPHER_SUITES
// This automatically includes all cipher suites supported by the current rustls version
// What the last `set_ciphers` selected, or the provider defaults
// when no cipher string was given.
let selected = self.selected_ciphers.read().clone();
let suites = selected
.unwrap_or_else(|| CryptoExt::get_ext().default_ciphers_or_provider().to_vec());
let cipher_list = CryptoExt::get_ext()
.all_ciphers_or_default()
let cipher_list = suites
.iter()
.map(|suite| {
// Extract cipher information using unified helper
let cipher_info = extract_cipher_info(suite);
// Convert to OpenSSL-style name
// e.g., "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" -> "ECDHE-RSA-AES128-GCM-SHA256"
let openssl_name = normalize_cipher_name(&cipher_info.name);
// Determine key exchange and auth methods
let (kx, auth) = if cipher_info.protocol == "TLSv1.3" {
// TLS 1.3 doesn't distinguish - all use modern algos
("any", "any")
} else if cipher_info.name.contains("ECDHE") {
// TLS 1.2 with ECDHE
let auth = if cipher_info.name.contains("ECDSA") {
"ECDSA"
} else if cipher_info.name.contains("RSA") {
"RSA"
} else {
"any"
};
("ECDH", auth)
} else {
("any", "any")
};
// Build description string
// Format: "{name} {protocol} Kx={kx} Au={auth} Enc={enc} Mac={mac}"
let enc = get_cipher_encryption_desc(&openssl_name);
let description = format!(
"{} {} Kx={} Au={} Enc={} Mac=AEAD",
openssl_name, cipher_info.protocol, kx, auth, enc
);
// Create cipher dict
let cipher = cipher::describe(suite);
let dict = vm.ctx.new_dict();
dict.set_item("name", vm.ctx.new_str(openssl_name).into(), vm)
.unwrap();
dict.set_item("protocol", vm.ctx.new_str(cipher_info.protocol).into(), vm)
.unwrap();
dict.set_item("id", vm.ctx.new_int(0).into(), vm).unwrap(); // Placeholder ID
dict.set_item("strength_bits", vm.ctx.new_int(cipher_info.bits).into(), vm)
.unwrap();
dict.set_item("alg_bits", vm.ctx.new_int(cipher_info.bits).into(), vm)
.unwrap();
dict.set_item("description", vm.ctx.new_str(description).into(), vm)
.unwrap();
let items: [(&str, PyObjectRef); 6] = [
("id", vm.ctx.new_int(cipher.id).into()),
("name", vm.ctx.new_str(cipher.name).into()),
("protocol", vm.ctx.new_str(cipher.protocol).into()),
("strength_bits", vm.ctx.new_int(cipher.bits).into()),
("alg_bits", vm.ctx.new_int(cipher.bits).into()),
("description", vm.ctx.new_str(cipher.description).into()),
];
for (key, value) in items {
dict.set_item(key, value, vm).unwrap();
}
dict.into()
})
.collect::<Vec<_>>();
@@ -1814,7 +1701,7 @@ mod _ssl {
};
// Check if file exists
if !std::path::Path::new(&path_str).exists() {
if !rustpython_host_env::fs::exists(&path_str) {
// Create FileNotFoundError with errno=ENOENT (2)
let exc = vm.new_os_subtype_error(
vm.ctx.exceptions.file_not_found_error.to_owned(),
@@ -1869,23 +1756,7 @@ mod _ssl {
return Err(vm.new_type_error("ECDH curve name must be str or bytes"));
};
// Validate curve name (common curves for compatibility)
// rustls supports: X25519, secp256r1 (prime256v1), secp384r1
let valid_curves = [
"prime256v1",
"secp256r1",
"prime384v1",
"secp384r1",
"prime521v1",
"secp521r1",
"X25519",
"x25519",
"x448", // For future compatibility
];
if !valid_curves.contains(&curve_name.as_str()) {
return Err(vm.new_value_error(format!("unknown curve name '{curve_name}'")));
}
curve_name_to_kx_group(&curve_name).map_err(|error| vm.new_value_error(error))?;
// Store the curve name to be used during handshake
// This will limit the key exchange groups offered/accepted
@@ -2030,7 +1901,10 @@ mod _ssl {
sock_send_method: vm.ctx.none(),
sock_recv_method: vm.ctx.none(),
tls_record_header_buf: vm.ctx.none(),
tls_record_header_buf: vm
.ctx
.new_bytearray(Vec::with_capacity(TLS_RECORD_HEADER_SIZE))
.into(),
context: PyRwLock::new(zelf),
server_side,
server_hostname: PyRwLock::new(hostname),
@@ -2382,6 +2256,7 @@ mod _ssl {
accept_count: AtomicUsize::new(0),
session_hits: AtomicUsize::new(0),
selected_ciphers: PyRwLock::new(None),
suite_b_kx_groups: PyRwLock::new(None),
})
}
}
@@ -3270,6 +3145,7 @@ mod _ssl {
) -> PyResult<Option<Vec<&'static dyn SupportedKxGroup>>> {
let ctx = self.context.read();
let ecdh_curve = ctx.ecdh_curve.read().clone();
let suite_b_kx_groups = ctx.suite_b_kx_groups.read().clone();
drop(ctx);
if let Some(ref curve_name) = ecdh_curve {
@@ -3278,7 +3154,9 @@ mod _ssl {
Err(e) => Err(vm.new_value_error(format!("Failed to set ECDH curve: {e}"))),
}
} else {
Ok(None)
// A SUITEB* cipher string pins its own groups, and nothing
// else asked for a curve.
Ok(suite_b_kx_groups)
}
}
@@ -4011,14 +3889,14 @@ mod _ssl {
};
// Extract cipher information outside the lock
let cipher_info = extract_cipher_info(&suite);
let cipher = cipher::describe(&suite);
// Note: returns a 3-tuple (name, protocol_version, bits)
// The 'description' field is part of get_ciphers() output, not cipher()
Some((
cipher_info.name,
cipher_info.protocol.to_string(),
cipher_info.bits,
cipher.name.to_owned(),
cipher.protocol.to_owned(),
cipher.bits.into(),
))
}
@@ -4509,11 +4387,11 @@ mod _ssl {
return Ok(self.try_read_close_notify_socket(conn, vm));
}
// BIO mode: read from incoming BIO
match self.sock_recv(SSL3_RT_MAX_PACKET_SIZE, vm) {
Ok(bytes_obj) => {
let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?;
let data = bytes.borrow_buf();
// BIO mode: stop at the TLS record boundary so cleartext queued
// after close_notify remains available to the BIO's owner.
match self.sock_recv_at_most_one_tls_record(vm) {
Ok(bytes) => {
let data = bytes.as_bytes();
if data.is_empty() {
if let Some(ref bio) = self.incoming_bio {
@@ -4528,7 +4406,7 @@ mod _ssl {
return Ok(true);
}
let data_slice: &[u8] = data.as_ref();
let data_slice: &[u8] = data;
let mut cursor = std::io::Cursor::new(data_slice);
let _ = conn.read_tls(&mut cursor);
let _ = conn.process_new_packets();
@@ -4600,14 +4478,13 @@ mod _ssl {
let suite = conn.negotiated_cipher_suite()?;
// Extract cipher information using unified helper
let cipher_info = extract_cipher_info(&suite);
let cipher = cipher::describe(&suite);
// Return as list with single tuple (name, version, bits)
let tuple = vm.ctx.new_tuple(vec![
vm.ctx.new_str(cipher_info.name).into(),
vm.ctx.new_str(cipher_info.protocol).into(),
vm.ctx.new_int(cipher_info.bits).into(),
vm.ctx.new_str(cipher.name).into(),
vm.ctx.new_str(cipher.protocol).into(),
vm.ctx.new_int(cipher.bits).into(),
]);
Some(vm.ctx.new_list(vec![tuple.into()]))
}
@@ -4959,7 +4836,11 @@ mod _ssl {
vm.ctx.new_int(entry.nid),
vm.ctx.new_str(entry.short_name),
vm.ctx.new_str(entry.long_name),
vm.ctx.new_str(entry.oid_string()),
// OpenSSL names some objects without giving them an OID, and
// `nid2obj` reports the fourth field as None for those.
entry
.oid_string()
.map_or_else(|| vm.ctx.none(), |oid| vm.ctx.new_str(oid).into()),
))
.into())
}
@@ -4975,7 +4856,11 @@ mod _ssl {
vm.ctx.new_int(entry.nid),
vm.ctx.new_str(entry.short_name),
vm.ctx.new_str(entry.long_name),
vm.ctx.new_str(entry.oid_string()),
// OpenSSL names some objects without giving them an OID, and
// `nid2obj` reports the fourth field as None for those.
entry
.oid_string()
.map_or_else(|| vm.ctx.none(), |oid| vm.ctx.new_str(oid).into()),
))
.into())
}
+1 -1
View File
@@ -668,7 +668,7 @@ impl<'a> CertLoader<'a> {
// Skip directories and process all files
// OpenSSL capath uses hash-based naming like "4e1295a3.0"
if path.is_file()
if rustpython_host_env::fs::is_file(&path)
&& let Ok(contents) = rustpython_host_env::fs::read(&path)
{
// Ignore errors for individual files (some may not be certs)
File diff suppressed because it is too large. Load diff
+3 -139
View File
@@ -1990,106 +1990,6 @@ impl ResolvesServerCert for MultiCertResolver {
// Helper Functions for OpenSSL Compatibility:
/// Normalize cipher suite name for OpenSSL compatibility
///
/// Converts rustls cipher names to OpenSSL format:
/// - TLS_AES_256_GCM_SHA384 → AES256-GCM-SHA384
/// - Replaces "AES-256" with "AES256" and "AES-128" with "AES128"
pub(super) fn normalize_cipher_name(rustls_name: &str) -> String {
rustls_name
.strip_prefix("TLS_")
.unwrap_or(rustls_name)
.replace("_WITH_", "_")
.replace('_', "-")
.replace("AES-256", "AES256")
.replace("AES-128", "AES128")
}
/// Get cipher key size in bits from cipher suite name
///
/// Returns:
/// - 256 for AES-256 and ChaCha20
/// - 128 for AES-128
/// - 0 for unknown ciphers
pub(super) fn get_cipher_key_bits(cipher_name: &str) -> i32 {
if cipher_name.contains("256") || cipher_name.contains("CHACHA20") {
256
} else if cipher_name.contains("128") {
128
} else {
0
}
}
/// Get encryption algorithm description from cipher name
///
/// Returns human-readable encryption description for OpenSSL compatibility
pub(super) fn get_cipher_encryption_desc(cipher_name: &str) -> &'static str {
if cipher_name.contains("AES256") {
"AESGCM(256)"
} else if cipher_name.contains("AES128") {
"AESGCM(128)"
} else if cipher_name.contains("CHACHA20") {
"CHACHA20-POLY1305(256)"
} else {
"Unknown"
}
}
/// Normalize rustls cipher suite name to IANA standard format
///
/// Converts rustls Debug format names to IANA standard:
/// - "TLS13_AES_256_GCM_SHA384" -> "TLS_AES_256_GCM_SHA384"
/// - Other names remain unchanged
pub(super) fn normalize_rustls_cipher_name(rustls_name: &str) -> String {
if rustls_name.starts_with("TLS13_") {
rustls_name.replace("TLS13_", "TLS_")
} else {
rustls_name.to_string()
}
}
/// Convert rustls protocol version to string representation
///
/// Returns the TLS version string
/// - TLSv1.2, TLSv1.3, or "Unknown"
pub(super) fn get_protocol_version_str(version: &rustls::SupportedProtocolVersion) -> &'static str {
match version.version {
rustls::ProtocolVersion::TLSv1_2 => "TLSv1.2",
rustls::ProtocolVersion::TLSv1_3 => "TLSv1.3",
_ => "Unknown",
}
}
/// Cipher suite information
///
/// Contains all relevant cipher information extracted from a rustls CipherSuite
pub(super) struct CipherInfo {
/// IANA standard cipher name (e.g., "TLS_AES_256_GCM_SHA384")
pub name: String,
/// TLS protocol version (e.g., "TLSv1.2", "TLSv1.3")
pub protocol: &'static str,
/// Key size in bits (e.g., 128, 256)
pub bits: i32,
}
/// Extract cipher information from a rustls CipherSuite
///
/// This consolidates the common cipher extraction logic used across
/// get_ciphers(), cipher(), and shared_ciphers() methods.
pub(super) fn extract_cipher_info(suite: &rustls::SupportedCipherSuite) -> CipherInfo {
let rustls_name = format!("{:?}", suite.suite());
let name = normalize_rustls_cipher_name(&rustls_name);
let protocol = get_protocol_version_str(suite.version());
let bits = get_cipher_key_bits(&name);
CipherInfo {
name,
protocol,
bits,
}
}
/// Convert curve name to rustls key exchange group
///
/// Maps OpenSSL curve names (e.g., "prime256v1", "secp384r1") to rustls KxGroups.
@@ -2097,43 +1997,7 @@ pub(super) fn extract_cipher_info(suite: &rustls::SupportedCipherSuite) -> Ciphe
pub(super) fn curve_name_to_kx_group(
curve: &str,
) -> Result<Vec<&'static dyn SupportedKxGroup>, String> {
// Get the default crypto provider's key exchange groups
let all_groups = CryptoExt::get_ext().all_kx_or_default();
match curve {
// P-256 (also known as secp256r1 or prime256v1)
"prime256v1" | "secp256r1" => {
// Find SECP256R1 in the provider's groups
all_groups
.iter()
.find(|g| g.name() == rustls::NamedGroup::secp256r1)
.map(|g| vec![*g])
.ok_or_else(|| "secp256r1 not supported by crypto provider".to_owned())
}
// P-384 (also known as secp384r1 or prime384v1)
"secp384r1" | "prime384v1" => all_groups
.iter()
.find(|g| g.name() == rustls::NamedGroup::secp384r1)
.map(|g| vec![*g])
.ok_or_else(|| "secp384r1 not supported by crypto provider".to_owned()),
// X25519
"X25519" | "x25519" => all_groups
.iter()
.find(|g| g.name() == rustls::NamedGroup::X25519)
.map(|g| vec![*g])
.ok_or_else(|| "X25519 not supported by crypto provider".to_owned()),
// P-521 (also known as secp521r1 or prime521v1)
"prime521v1" | "secp521r1" => all_groups
.iter()
.find(|g| g.name() == rustls::NamedGroup::secp521r1)
.map(|g| vec![*g])
.ok_or_else(|| "secp521r1 not supported by crypto provider".to_owned()),
// X448
"X448" | "x448" => all_groups
.iter()
.find(|g| g.name() == rustls::NamedGroup::X448)
.map(|g| vec![*g])
.ok_or_else(|| "X448 not supported by crypto provider".to_owned()),
_ => Err(format!("unknown curve name '{curve}'")),
}
super::cipher::kx_group_by_openssl_name(curve)
.map(|group| vec![group])
.ok_or_else(|| format!("unknown curve name '{curve}'"))
}
+251 -301
View File
@@ -2,11 +2,17 @@
//! 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.
//! `txt2obj` and `nid2obj` have to answer with OpenSSL's own numbers, so the
//! table behind them is read out of OpenSSL's own object database rather than
//! transcribed by hand: `obj_mac.num` carries every object's NID, and
//! `objects.txt` carries the numeric OID, short name and description for the
//! same objects. `objects.pl` is what OpenSSL generates its C tables from,
//! and the identifier it derives per object is what joins the two files here:
//! the `!Cname` override if one precedes the object, else its description,
//! else its short name -- each qualified by the enclosing `!module` and with
//! `-` spelled `_`.
use oid_registry::asn1_rs::Oid;
use alloc::rc::Rc;
use std::collections::HashMap;
/// OID entry with openssl-compatible metadata
@@ -18,56 +24,16 @@ pub(super) struct OidEntry {
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(super) 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),
/// Dotted-decimal form, rendered once while the table is built. `None`
/// for the objects OpenSSL names but assigns no OID -- cipher and digest
/// algorithm names like `DES-EDE3` -- whose `nid2obj` reports it as such.
oid: Option<Box<str>>,
}
impl OidEntry {
/// Create entry from oid-registry static constant
pub(super) 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(super) 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(super) fn oid_string(&self) -> String {
match &self.oid {
OidRef::Static(oid) => oid.to_id_string(),
OidRef::String(s) => s.to_string(),
}
/// Get OID as string (e.g., "2.5.4.3"), if this object has one
pub(super) fn oid_string(&self) -> Option<&str> {
self.oid.as_deref()
}
}
@@ -79,8 +45,8 @@ pub(super) struct OidTable {
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>,
/// Long name -> index mapping
long_name_to_idx: HashMap<&'static str, usize>,
/// OID string -> index mapping
oid_str_to_idx: HashMap<String, usize>,
}
@@ -93,11 +59,15 @@ impl OidTable {
let mut long_name_to_idx = HashMap::with_capacity(entries.len());
let mut oid_str_to_idx = HashMap::with_capacity(entries.len());
// OpenSSL's own database repeats a name, and an OID, across objects;
// its lookups answer with the first such object, so keep the first.
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);
nid_to_idx.entry(entry.nid).or_insert(idx);
short_name_to_idx.entry(entry.short_name).or_insert(idx);
long_name_to_idx.entry(entry.long_name).or_insert(idx);
if let Some(oid) = entry.oid_string() {
oid_str_to_idx.entry(oid.to_owned()).or_insert(idx);
}
}
Self {
@@ -115,261 +85,185 @@ impl OidTable {
pub(super) fn find_by_oid_string(&self, oid_str: &str) -> Option<&OidEntry> {
self.oid_str_to_idx
.get(oid_str)
.get(canonical_oid(oid_str)?.as_str())
.map(|&idx| &self.entries[idx])
}
pub(super) fn find_by_name(&self, name: &str) -> Option<&OidEntry> {
// Try short name first (exact match)
// OpenSSL object names are case-sensitive. Try the short name first,
// as OBJ_txt2obj does when a short and long name collide.
self.short_name_to_idx
.get(name)
.or_else(|| {
// Try long name (case-insensitive)
self.long_name_to_idx.get(&name.to_lowercase())
})
.or_else(|| self.long_name_to_idx.get(name))
.map(|&idx| &self.entries[idx])
}
}
/// Parse the numeric form accepted by OpenSSL and render it canonically for
/// lookup. The first arc may not have leading zeroes, while later arcs may.
fn canonical_oid(oid: &str) -> Option<String> {
let oid = oid.trim_end_matches(' ');
let oid = oid.strip_suffix('.').unwrap_or(oid);
let mut parts = oid.split('.');
let first = parts.next()?;
if !matches!(first, "0" | "1" | "2") {
return None;
}
let mut canonical = first.to_owned();
let mut count = 1;
for part in parts {
if part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let part = part.trim_start_matches('0');
canonical.push('.');
canonical.push_str(if part.is_empty() { "0" } else { part });
count += 1;
}
(count >= 2).then_some(canonical)
}
/// Global OID table
static OID_TABLE: rustpython_common::lock::LazyLock<OidTable> =
rustpython_common::lock::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)
/// OpenSSL's NID assignments: one `identifier<whitespace>nid` line per object.
static OBJ_MAC_NUM: &str = include_str!("../../rustls-data/obj_mac.num");
/// OpenSSL's object database: the numeric OID, short name and description of
/// each object, with `!Alias` / `!Cname` / `!module` / `!global` directives
/// between them.
static OBJECTS_TXT: &str = include_str!("../../rustls-data/objects.txt");
/// The name `objects.pl` derives for an object or alias: qualified by the
/// enclosing `!module`, with `-` spelled `_`.
fn qualify(name: &str, module: Option<&str>) -> Rc<str> {
let name = match module {
Some(module) => format!("{module}_{name}"),
None => name.to_owned(),
};
name.replace('-', "_").into()
}
/// 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)
};
/// Expand an OID written as a mix of arc numbers and earlier names into the
/// numbers alone. Resolving against `aliases` as we go is what keeps the
/// expansion non-recursive: a name can only refer to something already read.
fn resolve_oid<'a>(
parts: impl Iterator<Item = &'a str>,
aliases: &HashMap<Rc<str>, Rc<[u64]>>,
) -> Rc<[u64]> {
let mut oid = Vec::with_capacity(16);
for part in parts {
match aliases.get(part.replace('-', "_").as_str()) {
Some(prefix) => oid.extend_from_slice(prefix),
None => oid.push(part.parse().expect("OID arc is not a number")),
}
}
oid.into()
}
/// Build the complete OID table
fn dotted(oid: &[u64]) -> Box<str> {
oid.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(".")
.into_boxed_str()
}
/// Build the complete OID table out of OpenSSL's object database
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"),
]
let nids: HashMap<&str, i32> = OBJ_MAC_NUM
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.filter_map(|line| {
let mut field = line.split_whitespace();
let name = field.next()?;
let nid = field.next()?.parse().ok()?;
Some((name, nid))
})
.collect();
let mut entries = Vec::with_capacity(nids.len());
let mut aliases: HashMap<Rc<str>, Rc<[u64]>> = HashMap::new();
let mut cname: Option<&str> = None;
let mut module: Option<&str> = None;
for line in OBJECTS_TXT
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
{
if let Some(directive) = line.strip_prefix('!') {
let mut field = directive.split_whitespace();
match field.next() {
Some("Alias") => {
let alias = field.next().expect("!Alias without a name");
let oid = resolve_oid(field, &aliases);
aliases.insert(qualify(alias, module), oid);
}
Some("Cname") => cname = Some(field.next().expect("!Cname without a name")),
Some("module") => module = Some(field.next().expect("!module without a name")),
Some("global") => module = None,
other => panic!("unknown objects.txt directive {other:?}"),
}
continue;
}
let mut field = line.split(':').map(str::trim);
let oid = resolve_oid(
field
.next()
.expect("object without an OID")
.split_whitespace(),
&aliases,
);
// An object written with only one name carries it as both: OpenSSL's
// `OBJ_nid2sn(NID_pkcs9_emailAddress)` answers "emailAddress" for a
// line whose short-name column is blank.
let (short_name, long_name) = match (field.next().unwrap_or(""), field.next().unwrap_or(""))
{
("", "") => continue,
("", name) | (name, "") => (name, name),
(short_name, long_name) => (short_name, long_name),
};
// Every name this object can be reached by, most specific first. They
// become aliases for later objects to build on, and the first of them
// that `obj_mac.num` knows carries this object's NID.
let mut identifiers: Vec<Rc<str>> = Vec::with_capacity(3);
for name in [cname.take(), Some(long_name), Some(short_name)]
.into_iter()
.flatten()
.filter(|name| !name.is_empty())
{
let name = qualify(name, module);
if !identifiers.contains(&name) {
aliases.entry(name.clone()).or_insert_with(|| oid.clone());
identifiers.push(name);
}
}
let Some(nid) = identifiers
.iter()
.find_map(|name| nids.get(&**name).copied())
else {
continue;
};
entries.push(OidEntry {
nid,
short_name,
long_name,
// A single arc does not encode: DER packs the first two into one
// byte, so OpenSSL gives the bare roots -- `iso`, `itu-t` -- a NID
// and no OID, exactly as it does for the algorithm names above.
oid: (oid.len() >= 2).then(|| dotted(&oid)),
});
}
assert!(cname.is_none(), "!Cname with no object after it");
entries
}
// Public API Functions
@@ -398,7 +292,7 @@ mod tests {
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");
assert_eq!(entry.oid_string(), Some("2.5.4.3"));
}
#[test]
@@ -412,7 +306,7 @@ mod tests {
fn 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");
assert_eq!(entry.oid_string(), Some("2.5.4.3"));
}
#[test]
@@ -423,23 +317,30 @@ mod tests {
}
#[test]
fn find_by_name_case_insensitive() {
let entry = find_by_name("COMMONNAME").unwrap();
fn find_by_name_is_case_sensitive() {
assert!(find_by_name("commonName").is_some());
assert!(find_by_name("COMMONNAME").is_none());
}
#[test]
fn find_by_oid_string_normalizes_arcs() {
let entry = find_by_oid_string("2.005.004.003.").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.oid_string(), Some("2.5.4.3"));
}
#[test]
fn 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");
assert_eq!(entry.oid_string(), Some("2.5.29.17"));
}
#[test]
fn 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");
assert_eq!(entry.oid_string(), Some("1.3.6.1.5.5.7.3.1"));
}
#[test]
@@ -455,11 +356,60 @@ mod tests {
#[test]
fn oid_count() {
let table = &*OID_TABLE;
// We should have 50+ OIDs defined
// The table is OpenSSL's whole object database, not a curated subset.
assert!(
table.entries.len() >= 50,
"Expected at least 50 OIDs, got {}",
table.entries.len() >= 1000,
"Expected at least 1000 OIDs, got {}",
table.entries.len()
);
}
#[test]
fn nids_agree_with_openssl() {
// Spot-check objects whose NIDs Python code and test suites depend on.
// Every row is what `_ssl.nid2obj(nid)` answers on CPython built
// against OpenSSL, covering both name shapes: a distinct short and
// long name, and a single name standing for both.
for (nid, short_name, long_name, oid) in [
(13, "CN", "commonName", Some("2.5.4.3")),
(
48,
"emailAddress",
"emailAddress",
Some("1.2.840.113549.1.9.1"),
),
(
85,
"subjectAltName",
"X509v3 Subject Alternative Name",
Some("2.5.29.17"),
),
(105, "serialNumber", "serialNumber", Some("2.5.4.5")),
(
129,
"serverAuth",
"TLS Web Server Authentication",
Some("1.3.6.1.5.5.7.3.1"),
),
(660, "street", "streetAddress", Some("2.5.4.9")),
// Two arcs is still an object, and OpenSSL names some objects
// with no OID at all.
(11, "X500", "directory services (X.500)", Some("2.5")),
(181, "ISO", "iso", None),
(33, "DES-EDE3", "des-ede3", None),
(114, "MD5-SHA1", "md5-sha1", None),
(405, "ansi-X9-62", "ANSI X9.62", Some("1.2.840.10045")),
(
983,
"md_gost12_512",
"GOST R 34.11-2012 with 512 bit hash",
Some("1.2.643.7.1.1.2.3"),
),
] {
let entry = find_by_nid(nid).unwrap_or_else(|| panic!("no entry for NID {nid}"));
assert_eq!(entry.short_name, short_name, "NID {nid}");
assert_eq!(entry.long_name, long_name, "NID {nid}");
assert_eq!(entry.oid_string(), oid, "NID {nid}");
}
}
}
+14 -1
View File
@@ -26,6 +26,9 @@ static CRYPTO_EXT: OnceLock<CryptoExt> = OnceLock::new();
#[derive(Clone, Copy)]
pub struct CryptoExt {
pub all_cipher_suites: Option<&'static [SupportedCipherSuite]>,
/// The suites `DEFAULT` selects, which an OpenSSL cipher string
/// distinguishes from the `ALL` that `all_cipher_suites` holds.
pub default_cipher_suites: Option<&'static [SupportedCipherSuite]>,
pub all_kx_groups: Option<&'static [&'static dyn SupportedKxGroup]>,
#[allow(clippy::type_complexity)]
pub any_supported_key: Option<fn(&PrivateKeyDer<'_>) -> Result<Arc<dyn SigningKey>, Error>>,
@@ -53,7 +56,17 @@ impl CryptoExt {
/// Panics if a [`CryptoProvider`] hasn't been set.
#[must_use]
pub fn all_ciphers_or_default(&self) -> &'static [SupportedCipherSuite] {
self.all_cipher_suites.unwrap_or_else(|| {
self.all_cipher_suites
.unwrap_or_else(|| self.default_ciphers_or_provider())
}
/// Returns the default [`SupportedCipherSuite`]s, or the provider's own.
///
/// # Panics
/// Panics if a [`CryptoProvider`] hasn't been set.
#[must_use]
pub fn default_ciphers_or_provider(&self) -> &'static [SupportedCipherSuite] {
self.default_cipher_suites.unwrap_or_else(|| {
CryptoProvider::get_default()
.expect("A CryptoProvider has been set if CryptoExt is set")
.cipher_suites
+2
View File
@@ -30,6 +30,7 @@ fn main() {
"ring" => {
let ext = CryptoExt {
all_cipher_suites: Some(ring::ALL_CIPHER_SUITES),
default_cipher_suites: Some(ring::DEFAULT_CIPHER_SUITES),
all_kx_groups: Some(ring::ALL_KX_GROUPS),
any_supported_key: Some(ring::sign::any_supported_type),
ticketer: ring::Ticketer::new,
@@ -40,6 +41,7 @@ fn main() {
"graviola" => {
let ext = CryptoExt {
all_cipher_suites: Some(rustls_graviola::suites::ALL_CIPHER_SUITES),
default_cipher_suites: None,
all_kx_groups: Some(rustls_graviola::kx::ALL_KX_GROUPS),
any_supported_key: None,
ticketer: rustls_graviola::Ticketer::new,
+11 -2
View File
@@ -33,9 +33,18 @@ fn install_default_tls_provider(_vm: &mut crate::VirtualMachine) {
use rustls::crypto::aws_lc_rs;
use rustpython_stdlib::ssl::providers::CryptoExt;
#[cfg(feature = "ssl-rustls-aws-lc-fips")]
let (all_cipher_suites, all_kx_groups) = (None, None);
#[cfg(not(feature = "ssl-rustls-aws-lc-fips"))]
let (all_cipher_suites, all_kx_groups) = (
Some(aws_lc_rs::ALL_CIPHER_SUITES),
Some(aws_lc_rs::ALL_KX_GROUPS),
);
let ext = CryptoExt {
all_cipher_suites: Some(aws_lc_rs::ALL_CIPHER_SUITES),
all_kx_groups: Some(aws_lc_rs::ALL_KX_GROUPS),
all_cipher_suites,
default_cipher_suites: Some(aws_lc_rs::DEFAULT_CIPHER_SUITES),
all_kx_groups,
any_supported_key: Some(aws_lc_rs::sign::any_supported_type),
ticketer: aws_lc_rs::Ticketer::new,
};