diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 17:02:11 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-26 00:20:05 +0900 |
| commit | 3498bb1ca00ab7d051d296b8d482bea987a00fa4 (patch) | |
| tree | e10260e5816317f4547847e2a230ea1a87fb2448 /crates/shirabe/src/util/tls_helper.rs | |
| parent | 291b43d132749a61918dca23acef1b639c5333a7 (diff) | |
| download | php-shirabe-3498bb1ca00ab7d051d296b8d482bea987a00fa4.tar.gz php-shirabe-3498bb1ca00ab7d051d296b8d482bea987a00fa4.tar.zst php-shirabe-3498bb1ca00ab7d051d296b8d482bea987a00fa4.zip | |
test: port 24 command/repository/package/util tests; add TlsHelper
Port command (9), util gitlab/forgejo/tls (6), package (6), repository (3)
tests. Implement TlsHelper. Fix porting bugs: config_command extra merge,
RootAliasPackage setters, ValidatingArrayLoader isset, repository_factory name
generation, forgejo exception code, version_parser error chaining.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/util/tls_helper.rs')
| -rw-r--r-- | crates/shirabe/src/util/tls_helper.rs | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/crates/shirabe/src/util/tls_helper.rs b/crates/shirabe/src/util/tls_helper.rs new file mode 100644 index 0000000..6422be3 --- /dev/null +++ b/crates/shirabe/src/util/tls_helper.rs @@ -0,0 +1,149 @@ +//! ref: composer/src/Composer/Util/TlsHelper.php + +use shirabe_external_packages::composer::ca_bundle::ca_bundle::CaBundle; +use shirabe_external_packages::composer::pcre::Preg; +use shirabe_php_shim::{ + PhpMixed, ltrim, preg_quote, str_replace, strtolower, substr, substr_count, +}; + +/// Extracted certificate names. Mirrors PHP's `array{cn: string, san: string[]}`. +#[derive(Debug, Clone)] +pub struct CertificateNames { + pub cn: String, + pub san: Vec<String>, +} + +/// Match hostname against a certificate. +/// +/// @deprecated Use composer/ca-bundle and composer/composer 2.2 if you still need PHP 5 +/// compatibility, this class will be removed in Composer 3.0 +#[derive(Debug)] +pub struct TlsHelper; + +impl TlsHelper { + /// Match hostname against a certificate. Sets `cn` to the common name of the + /// certificate iff a match is found. + pub fn check_certificate_host( + certificate: &PhpMixed, + hostname: &str, + cn: &mut Option<String>, + ) -> bool { + let names = Self::get_certificate_names(certificate); + + let Some(names) = names else { + return false; + }; + + let mut combined_names = names.san.clone(); + combined_names.push(names.cn.clone()); + let hostname = strtolower(hostname); + + for cert_name in &combined_names { + let matcher = Self::cert_name_matcher(cert_name); + + if let Some(matcher) = matcher + && matcher(&hostname) + { + *cn = Some(names.cn.clone()); + + return true; + } + } + + false + } + + /// Extract DNS names out of an X.509 certificate. + pub fn get_certificate_names(certificate: &PhpMixed) -> Option<CertificateNames> { + let info: Option<&PhpMixed> = if certificate.as_array().is_some() { + Some(certificate) + } else if CaBundle::is_openssl_parse_safe() { + // TODO(phase-c): openssl_x509_parse on a PEM string certificate. + todo!("openssl_x509_parse for non-array certificates") + } else { + None + }; + + let info = info?.as_array()?; + + let common_name = info + .get("subject") + .and_then(|s| s.as_array()) + .and_then(|s| s.get("commonName")) + .and_then(|c| c.as_string()); + + let common_name = strtolower(common_name?); + let mut subject_alt_names: Vec<String> = Vec::new(); + + if let Some(san) = info + .get("extensions") + .and_then(|e| e.as_array()) + .and_then(|e| e.get("subjectAltName")) + .and_then(|s| s.as_string()) + { + let split = Preg::split("{\\s*,\\s*}", san); + subject_alt_names = split + .into_iter() + .filter_map(|name| { + if name.starts_with("DNS:") { + Some(strtolower(<rim(&substr(&name, 4, None), None))) + } else { + None + } + }) + .collect(); + } + + Some(CertificateNames { + cn: common_name, + san: subject_alt_names, + }) + } + + /// Get the certificate pin. + pub fn get_certificate_fingerprint(_certificate: &str) -> String { + todo!("openssl public key extraction and sha1 fingerprint") + } + + /// Test if it is safe to use the PHP function openssl_x509_parse(). + pub fn is_openssl_parse_safe() -> bool { + CaBundle::is_openssl_parse_safe() + } + + /// Convert certificate name into matching function. + fn cert_name_matcher(cert_name: &str) -> Option<Box<dyn Fn(&str) -> bool>> { + let wildcards = substr_count(cert_name, "*"); + + if wildcards == 0 { + // Literal match. + let cert_name = cert_name.to_string(); + return Some(Box::new(move |hostname: &str| hostname == cert_name)); + } + + if wildcards == 1 { + let components: Vec<&str> = cert_name.split('.').collect(); + + if components.len() < 3 { + // Must have 3+ components + return None; + } + + let first_component = components[0]; + + // Wildcard must be the last character. + if !first_component.ends_with('*') { + return None; + } + + let mut wildcard_regex = preg_quote(cert_name, None); + wildcard_regex = str_replace("\\*", "[a-z0-9-]+", &wildcard_regex); + let wildcard_regex = format!("{{^{}$}}", wildcard_regex); + + return Some(Box::new(move |hostname: &str| { + Preg::is_match(&wildcard_regex, hostname) + })); + } + + None + } +} |
