diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-16 00:04:33 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-16 10:00:40 +0900 |
| commit | c5f9f02edab222fbf37a593dd35c04b4976273e2 (patch) | |
| tree | 2badebb41deb4954cc18dfbcd15575645c27a49d /crates/shirabe/src/util/tls_helper.rs | |
| parent | f349711071dc66696fc120af122620de1b9234ee (diff) | |
| download | php-shirabe-c5f9f02edab222fbf37a593dd35c04b4976273e2.tar.gz php-shirabe-c5f9f02edab222fbf37a593dd35c04b4976273e2.tar.zst php-shirabe-c5f9f02edab222fbf37a593dd35c04b4976273e2.zip | |
feat(port): port TlsHelper.php
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/util/tls_helper.rs')
| -rw-r--r-- | crates/shirabe/src/util/tls_helper.rs | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/crates/shirabe/src/util/tls_helper.rs b/crates/shirabe/src/util/tls_helper.rs index 908b85a..ecacbcd 100644 --- a/crates/shirabe/src/util/tls_helper.rs +++ b/crates/shirabe/src/util/tls_helper.rs @@ -1 +1,187 @@ //! ref: composer/src/Composer/Util/TlsHelper.php + +use shirabe_external_packages::composer::ca_bundle::ca_bundle::CaBundle; +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_php_shim::{ + base64_decode, openssl_get_publickey, openssl_pkey_get_details, openssl_x509_parse, + preg_quote, substr_count, PhpMixed, RuntimeException, +}; + +/// @deprecated Use composer/ca-bundle and composer/composer 2.2 if you still need PHP 5 compatibility +pub struct TlsHelper; + +impl TlsHelper { + pub fn check_certificate_host( + certificate: &PhpMixed, + hostname: &str, + cn: &mut Option<String>, + ) -> bool { + let names = match Self::get_certificate_names(certificate) { + Some(n) => n, + None => return false, + }; + + let mut combined_names = names.san.clone(); + combined_names.push(names.cn.clone()); + let hostname = hostname.to_lowercase(); + + for cert_name in &combined_names { + if let Some(matcher) = Self::cert_name_matcher(cert_name) { + if matcher(&hostname) { + *cn = Some(names.cn.clone()); + return true; + } + } + } + + false + } + + pub fn get_certificate_names(certificate: &PhpMixed) -> Option<CertificateNames> { + let info = match certificate { + PhpMixed::Array(arr) => arr.clone(), + _ => { + if CaBundle::is_openssl_parse_safe() { + if let PhpMixed::String(cert_str) = certificate { + openssl_x509_parse(cert_str, false)? + } else { + return None; + } + } else { + return None; + } + } + }; + + let common_name = info.get("subject") + .and_then(|v| v.as_array()) + .and_then(|subj| subj.get("commonName")) + .and_then(|cn| cn.as_string()) + .map(|s| s.to_lowercase())?; + + let mut subject_alt_names = vec![]; + if let Some(san_value) = info.get("extensions") + .and_then(|v| v.as_array()) + .and_then(|ext| ext.get("subjectAltName")) + .and_then(|v| v.as_string()) + { + let parts = Preg::split(r"{\s*,\s*}", san_value).unwrap_or_default(); + for name in parts { + if name.starts_with("DNS:") { + let dns = name[4..].trim_start().to_lowercase(); + subject_alt_names.push(dns); + } + } + } + + Some(CertificateNames { + cn: common_name, + san: subject_alt_names, + }) + } + + /// Get the certificate pin. + /// + /// By Kevin McArthur of StormTide Digital Studios Inc. + /// @KevinSMcArthur / https://github.com/StormTide + /// + /// See https://tools.ietf.org/html/draft-ietf-websec-key-pinning-02 + /// + /// This method was adapted from Sslurp. + /// https://github.com/EvanDotPro/Sslurp + /// + /// (c) Evan Coury <me@evancoury.com> + /// + /// For the full copyright and license information, please see below: + /// + /// Copyright (c) 2013, Evan Coury + /// All rights reserved. + /// + /// Redistribution and use in source and binary forms, with or without modification, + /// are permitted provided that the following conditions are met: + /// + /// * Redistributions of source code must retain the above copyright notice, + /// this list of conditions and the following disclaimer. + /// + /// * Redistributions in binary form must reproduce the above copyright notice, + /// this list of conditions and the following disclaimer in the documentation + /// and/or other materials provided with the distribution. + /// + /// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + /// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + /// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + /// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + /// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + /// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + /// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + /// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + /// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + /// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + pub fn get_certificate_fingerprint(certificate: &str) -> anyhow::Result<String> { + let pubkey = openssl_get_publickey(certificate).ok_or_else(|| RuntimeException { + message: "Failed to retrieve the public key from certificate".to_string(), + code: 0, + })?; + let pubkeydetails = openssl_pkey_get_details(pubkey).ok_or_else(|| RuntimeException { + message: "Failed to retrieve public key details".to_string(), + code: 0, + })?; + let pubkeypem = pubkeydetails.get("key") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + + let start = "-----BEGIN PUBLIC KEY-----"; + let end = "-----END PUBLIC KEY-----"; + let start_pos = pubkeypem.find(start).unwrap_or(0) + start.len(); + let end_pos = pubkeypem.rfind(end).unwrap_or(pubkeypem.len()); + let pemtrim = &pubkeypem[start_pos..end_pos]; + + let der = base64_decode(pemtrim).unwrap_or_default(); + + Ok(shirabe_php_shim::hash("sha1", &String::from_utf8_lossy(&der))) + } + + pub fn is_openssl_parse_safe() -> bool { + CaBundle::is_openssl_parse_safe() + } + + fn cert_name_matcher(cert_name: &str) -> Option<Box<dyn Fn(&str) -> bool>> { + let wildcards = substr_count(cert_name, "*"); + + if wildcards == 0 { + let name = cert_name.to_string(); + return Some(Box::new(move |hostname: &str| hostname == name)); + } + + if wildcards == 1 { + let components: Vec<&str> = cert_name.split('.').collect(); + + if components.len() < 3 { + return None; + } + + let first_component = components[0]; + + if !first_component.ends_with('*') { + return None; + } + + let mut wildcard_regex = preg_quote(cert_name, None); + wildcard_regex = wildcard_regex.replace("\\*", "[a-z0-9-]+"); + let wildcard_regex = format!("{{^{}$}}", wildcard_regex); + + return Some(Box::new(move |hostname: &str| { + Preg::is_match(&wildcard_regex, hostname).unwrap_or(false) + })); + } + + None + } +} + +#[derive(Debug)] +pub struct CertificateNames { + pub cn: String, + pub san: Vec<String>, +} |
