aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/util/tls_helper.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 17:02:11 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commit3498bb1ca00ab7d051d296b8d482bea987a00fa4 (patch)
treee10260e5816317f4547847e2a230ea1a87fb2448 /crates/shirabe/src/util/tls_helper.rs
parent291b43d132749a61918dca23acef1b639c5333a7 (diff)
downloadphp-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.rs149
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(&ltrim(&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
+ }
+}