-
Notifications
You must be signed in to change notification settings - Fork 1.5k
ssl: load hashed capath certificates lazily #8648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -634,6 +634,42 @@ pub(super) struct CertLoader<'a> { | |
| seen_certs: HashSet<Vec<u8>>, | ||
| } | ||
|
|
||
| fn is_capath_hash_name(name: &std::ffi::OsStr) -> bool { | ||
| let Some(name) = name.to_str() else { | ||
| return false; | ||
| }; | ||
| let Some((hash, suffix)) = name.split_once('.') else { | ||
| return false; | ||
| }; | ||
| hash.len() == 8 | ||
| && hash.bytes().all(|byte| byte.is_ascii_hexdigit()) | ||
| && !suffix.is_empty() | ||
| && suffix.bytes().all(|byte| byte.is_ascii_digit()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::is_capath_hash_name; | ||
| use std::ffi::OsStr; | ||
|
|
||
| #[test] | ||
| fn capath_hash_names_follow_openssl_shape() { | ||
| for valid in ["4e1295a3.0", "ABCDEF01.12"] { | ||
| assert!(is_capath_hash_name(OsStr::new(valid)), "{valid}"); | ||
| } | ||
| for invalid in [ | ||
| "4e1295a.0", | ||
| "4e1295a30.0", | ||
| "4e1295ag.0", | ||
| "4e1295a3", | ||
| "4e1295a3.", | ||
| "4e1295a3.pem", | ||
| ] { | ||
| assert!(!is_capath_hash_name(OsStr::new(invalid)), "{invalid}"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<'a> CertLoader<'a> { | ||
| /// Create a new CertLoader with references to the store and DER cache | ||
| pub(super) fn new(store: &'a mut RootCertStore, ca_certs_der: &'a mut Vec<Vec<u8>>) -> Self { | ||
|
|
@@ -656,26 +692,29 @@ impl<'a> CertLoader<'a> { | |
|
|
||
| /// Load certificates from a directory | ||
| /// | ||
| /// Reads all files in the directory and attempts to parse them as certificates. | ||
| /// Invalid files are silently skipped (matches OpenSSL capath behavior). | ||
| /// Reads OpenSSL-style hashed entries and attempts to parse them as certificates. | ||
| /// Invalid entries are silently skipped (matches OpenSSL capath behavior). | ||
| pub(super) fn load_from_dir(&mut self, dir_path: &str) -> Result<CertStats, std::io::Error> { | ||
| let entries = rustpython_host_env::fs::read_dir(dir_path)?; | ||
| let mut stats = CertStats::default(); | ||
| let mut paths = Vec::new(); | ||
|
|
||
| for entry in entries { | ||
| let entry = entry?; | ||
| let path = entry.path(); | ||
| if is_capath_hash_name(&entry.file_name()) && rustpython_host_env::fs::is_file(&path) { | ||
| paths.push(path); | ||
|
Comment on lines
+705
to
+706
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 36331 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target source ---'
sed -n '620,730p' crates/stdlib/src/ssl/cert.rs
printf '%s\n' '--- related definitions and tests ---'
rg -n -C 4 'load_from_dir|is_capath_hash_name|capath|uppercase|hash name|subject hash|X509_NAME|Trust' crates/stdlib/src/ssl/cert.rs crates/stdlib/src/ssl
printf '%s\n' '--- changed-file summary ---'
git diff --stat -- crates/stdlib/src/ssl/cert.rsRepository: RustPython/RustPython Length of output: 14911 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- certificate parsing and store insertion ---'
sed -n '1,330p' crates/stdlib/src/ssl/cert.rs
sed -n '730,860p' crates/stdlib/src/ssl/cert.rs
printf '%s\n' '--- directory-loader callers and capath setup ---'
rg -n -C 6 'load_from_dir|load_verify_locations|capath|CApath|CertLoader' crates/stdlib/src/ssl crates/stdlib
printf '%s\n' '--- capath-related fixtures and tests ---'
rg -n -C 5 'capath|4e1295a3|hash' Lib crates/stdlib extra_tests 2>/dev/null || trueRepository: RustPython/RustPython Length of output: 50378 🌐 Web query:
💡 Result: The OpenSSL X509_LOOKUP_hash_dir method is used for locating and loading certificates and Certificate Revocation Lists (CRLs) from a directory on demand [1][2]. The semantics for filename lookup and handling are as follows: Filename Semantics: The directory must contain files named using the format hash.N for certificates or hash.rN for CRLs [1][3]. The hash component is derived by applying the X509_NAME_hash_ex function (or X509_NAME_hash in older versions) to the subject name of the certificate or the issuer name of the CRL [1][2]. Sequence Numbers (N): The.N or.rN suffix represents a sequence number starting at zero [1][4]. This suffix allows for the storage of multiple certificates with the same subject name hash in the same directory [1][2]. OpenSSL iterates through these files sequentially starting from N=0 [1][3]. Lookup Behavior: 1. On-Demand Loading: Files are loaded into memory only when needed and subsequently cached [1][5]. 2. Sequential Scanning: OpenSSL assumes that the sequence numbers are consecutive [1][2]. It will attempt to load files in order (e.g.,.0,.1,.2...). 3. Termination of Lookup: Gaps in the sequence are not supported; the lookup process assumes there are no more objects with a given hash once it encounters a missing number in the sequence [1][6]. 4. CRLs: When checking for newer CRLs for a previously loaded hash, the method limits its check to files with a sequence number greater than the currently cached CRL [1][3]. To ensure correct operation, especially when multiple certificates share the same subject name hash, the files must be explicitly named with these sequential numbers [1][2]. The openssl-rehash(1) utility is the standard tool for generating these symlinks automatically for PEM files in a directory [1][3]. If distinct certificates exist with the same subject name, utilizing Subject Key Identifiers (SKID) and Authority Key Identifiers (AKID) is essential for proper identification and verification by OpenSSL, rather than relying solely on subject name matching [7]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- loader implementation ---'
sed -n '720,825p' crates/stdlib/src/ssl/cert.rs
printf '%s\n' '--- exact directory-loader callers ---'
rg -n -C 10 'load_from_dir' crates/stdlib/src/ssl
printf '%s\n' '--- exact capath API flow ---'
rg -n -C 8 'capath|CApath|verify_locations|load_verify' crates/stdlib/src/ssl
printf '%s\n' '--- certificate hash helpers ---'
rg -n -C 6 'hash.*subject|subject.*hash|X509_NAME_hash|name_hash|openssl' crates/stdlib/src/ssl/cert.rs crates/stdlib/src/sslRepository: RustPython/RustPython Length of output: 39183 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- all CertLoader references ---'
rg -n -C 8 'CertLoader|load_from_dir' .
printf '%s\n' '--- ssl module files and verification entry points ---'
fd -t f . crates/stdlib/src/ssl | sort
rg -n -C 8 'load_verify_locations|cafile|capath|verify_locations|RootCertStore|load_from_file|load_from_bytes' crates/stdlib/src/ssl --glob '*.rs'
printf '%s\n' '--- repository state ---'
git status --short
git diff --statRepository: RustPython/RustPython Length of output: 28487 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- capath cache and trust-store integration ---'
sed -n '800,900p' crates/stdlib/src/ssl.rs
sed -n '900,970p' crates/stdlib/src/ssl.rs
printf '%s\n' '--- capath configuration and use ---'
rg -n -C 10 'capath_state|get_capath|capath_cert|directories' crates/stdlib/src/ssl.rs
printf '%s\n' '--- current uppercase-name test context ---'
sed -n '637,672p' crates/stdlib/src/ssl/cert.rsRepository: RustPython/RustPython Length of output: 17160 🌐 Web query:
💡 Result: In OpenSSL's hashed directory lookup method, implemented in Citations:
Bind capath filenames to certificate subjects.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| paths.sort(); | ||
|
|
||
| // Skip directories and process all files | ||
| // OpenSSL capath uses hash-based naming like "4e1295a3.0" | ||
| 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) | ||
| if let Ok(file_stats) = self.load_from_bytes(&contents) { | ||
| stats.total_certs += file_stats.total_certs; | ||
| stats.ca_certs += file_stats.ca_certs; | ||
| } | ||
| for path in paths { | ||
| let Ok(contents) = rustpython_host_env::fs::read(path) else { | ||
| continue; | ||
| }; | ||
| if let Ok(file_stats) = self.load_from_bytes(&contents) { | ||
| stats.total_certs += file_stats.total_certs; | ||
| stats.ca_certs += file_stats.ca_certs; | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Track capath CAs only when a verifier uses them.
Line 3268 now uses capath roots for server-side client-certificate verification.
complete_handshakestill callstrack_used_ca_from_capathonly when!self.server_side, so a successful server mTLS handshake does not updateget_ca_certs()orcert_store_stats(). Conversely, a client withverify_mode == CERT_NONEcan match only an issuer DN and increment both counters without verification. Run tracking for both roles only when the configured verifier uses trust roots.🤖 Prompt for AI Agents