diff --git a/ylong_http_client/src/async_impl/client.rs b/ylong_http_client/src/async_impl/client.rs index 5afb3dd7a5c241a2b86ce538aa9e8c06143cb37a..2711f6be4c1e4ff57ce5c5340a194ee346664de5 100644 --- a/ylong_http_client/src/async_impl/client.rs +++ b/ylong_http_client/src/async_impl/client.rs @@ -18,6 +18,8 @@ use super::timeout::TimeoutFuture; use super::{conn, Body, Connector, HttpConnector, Request, Response}; use crate::error::HttpClientError; use crate::runtime::timeout; +#[cfg(feature = "__c_openssl")] +use crate::util::c_openssl::verify::PubKeyPins; use crate::util::config::{ ClientConfig, ConnectorConfig, HttpConfig, HttpVersion, Proxy, Redirect, Timeout, }; @@ -514,6 +516,28 @@ impl ClientBuilder { self } + /// Adds user pinned Public Key. + /// + /// Used to avoid man-in-the-middle attacks. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::async_impl::ClientBuilder; + /// use ylong_http_client::PubKeyPins; + /// + /// let pinned_key = PubKeyPins::builder() + /// .add("https://example.com:443", + /// "sha256//YhKJKSzoTt2b5FP18fvpHo7fJYqQCjAa3HWY3tvRMwE=;sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=") + /// .build() + /// .unwrap(); + /// let builder = ClientBuilder::new().add_public_key_pins(pinned_key); + /// ``` + pub fn add_public_key_pins(mut self, pin: PubKeyPins) -> Self { + self.tls = self.tls.pinning_public_key(pin); + self + } + /// Loads trusted root certificates from a file. The file should contain a /// sequence of PEM-formatted CA certificates. /// @@ -891,6 +915,34 @@ HJMRZVCQpSMzvHlofHSNgzWV1MX5h1CP4SGZdBDTfA== assert!(client.is_err()); } + /// UT test cases for `ClientBuilder::build`. + /// + /// # Brief + /// 1. Creates a ClientBuilder by calling `Client::Builder`. + /// 2. Checks if the result is as expected. + #[cfg(feature = "__tls")] + #[test] + fn ut_client_build_tls_pubkey_pinning() { + use crate::PubKeyPins; + + let client = Client::builder() + .tls_built_in_root_certs(true) // not use root certs + .danger_accept_invalid_certs(true) // not verify certs + .max_tls_version(TlsVersion::TLS_1_2) + .min_tls_version(TlsVersion::TLS_1_2) + .add_public_key_pins( + PubKeyPins::builder() + .add( + "https://7.249.243.101:6789", + "sha256//VHQAbNl67nmkZJNESeTKvTxb5bQmd1maWnMKG/tjcAY=", + ) + .build() + .unwrap(), + ) + .build(); + assert!(client.is_ok()) + } + /// UT test cases for `ClientBuilder::default`. /// /// # Brief @@ -1399,10 +1451,7 @@ HJMRZVCQpSMzvHlofHSNgzWV1MX5h1CP4SGZdBDTfA== #[test] fn ut_client_recv_when_server_shutdown() { let mut handles = vec![]; - start_tcp_server!( - Handles: handles, - Shutdown: std::net::Shutdown::Both, - ); + start_tcp_server!(Handles: handles, Shutdown: std::net::Shutdown::Both,); let handle = handles.pop().expect("No more handles !"); let request = build_client_request!( diff --git a/ylong_http_client/src/async_impl/connector/mod.rs b/ylong_http_client/src/async_impl/connector/mod.rs index 1d620a072acfd73fdc00774fc0ee4fc4f3a66b44..93bcf5d47753d52dba004e7867cda90ccb5a1c6d 100644 --- a/ylong_http_client/src/async_impl/connector/mod.rs +++ b/ylong_http_client/src/async_impl/connector/mod.rs @@ -161,9 +161,10 @@ mod tls { tcp = tunnel(tcp, host, port, auth).await?; }; + let pinned_key = config.pinning_host_match(addr.as_str()); let mut stream = config .ssl_new(&host_name) - .and_then(|ssl| AsyncSslStream::new(ssl.into_inner(), tcp)) + .and_then(|ssl| AsyncSslStream::new(ssl.into_inner(), tcp, pinned_key)) .map_err(|e| Error::new(ErrorKind::Other, e))?; Pin::new(&mut stream) diff --git a/ylong_http_client/src/async_impl/ssl_stream/c_ssl_stream.rs b/ylong_http_client/src/async_impl/ssl_stream/c_ssl_stream.rs index 54adbf45b1749739a12852bd16d4839921ef57b9..8ee6dca6318a8ea1c2947b394cc611598175eb79 100644 --- a/ylong_http_client/src/async_impl/ssl_stream/c_ssl_stream.rs +++ b/ylong_http_client/src/async_impl/ssl_stream/c_ssl_stream.rs @@ -53,7 +53,11 @@ where S: AsyncRead + AsyncWrite, { /// Like [`SslStream::new`](ssl::SslStream::new). - pub(crate) fn new(ssl: Ssl, stream: S) -> Result { + pub(crate) fn new( + ssl: Ssl, + stream: S, + pinned_pubkey: Option, + ) -> Result { // This corresponds to `SSL_set_bio`. ssl::SslStream::new_base( ssl, @@ -61,6 +65,7 @@ where stream, context: ptr::null_mut(), }, + pinned_pubkey, ) .map(AsyncSslStream) } diff --git a/ylong_http_client/src/util/c_openssl/adapter.rs b/ylong_http_client/src/util/c_openssl/adapter.rs index 0a80ebc66dadd97c379c956519e16950e6fea28c..ad77324b31a12dcb75de6eecd0bae516f3172d83 100644 --- a/ylong_http_client/src/util/c_openssl/adapter.rs +++ b/ylong_http_client/src/util/c_openssl/adapter.rs @@ -20,6 +20,7 @@ use crate::util::c_openssl::error::ErrorStack; use crate::util::c_openssl::ssl::{ Ssl, SslContext, SslContextBuilder, SslFiletype, SslMethod, SslVersion, }; +use crate::util::c_openssl::verify::PubKeyPins; use crate::util::c_openssl::x509::{X509Store, X509}; use crate::util::config::tls::DefaultCertVerifier; use crate::util::AlpnProtocolList; @@ -44,6 +45,7 @@ pub struct TlsConfigBuilder { use_sni: bool, verify_hostname: bool, certs_list: Vec, + pins: Option, #[cfg(feature = "c_openssl_3_0")] paths_list: Vec, } @@ -65,6 +67,7 @@ impl TlsConfigBuilder { use_sni: true, verify_hostname: true, certs_list: vec![], + pins: None, #[cfg(feature = "c_openssl_3_0")] paths_list: vec![], } @@ -88,10 +91,10 @@ impl TlsConfigBuilder { } /// Sets the maximum supported protocol version. A value of `None` will - /// enable protocol versions down the the highest version supported by + /// enable protocol versions down the highest version supported by /// `OpenSSL`. /// - /// Requires `OpenSSL 1.1.0` or or `LibreSSL 2.6.1` or newer. + /// Requires `OpenSSL 1.1.0` or `LibreSSL 2.6.1` or newer. /// /// # Examples /// @@ -371,6 +374,11 @@ impl TlsConfigBuilder { self } + pub(crate) fn pinning_public_key(mut self, pin: PubKeyPins) -> Self { + self.pins = Some(pin); + self + } + /// Controls the use of TLS server name indication. /// /// Defaults to `true` -- sets sni. @@ -427,6 +435,7 @@ impl TlsConfigBuilder { cert_verifier: self.cert_verifier, use_sni: self.use_sni, verify_hostname: self.verify_hostname, + pins: self.pins, }) } } @@ -454,6 +463,7 @@ pub struct TlsConfig { cert_verifier: Option>, use_sni: bool, verify_hostname: bool, + pins: Option, } impl TlsConfig { @@ -486,11 +496,18 @@ impl TlsConfig { } Ok(TlsSsl(ssl)) } + + pub(crate) fn pinning_host_match(&self, domain: &str) -> Option { + match &self.pins { + None => None, + Some(pins) => pins.get_pin(domain), + } + } } impl Default for TlsConfig { fn default() -> Self { - // It must can be successful. + // It certainly can be successful. TlsConfig::builder() .build() .expect("TlsConfig build error!") diff --git a/ylong_http_client/src/util/c_openssl/error.rs b/ylong_http_client/src/util/c_openssl/error.rs index 9410d264d6405fe59f7a6823339cfc90b264c9e4..3cafcd803144eb7974f65d7172afbb4ebf3c3847 100644 --- a/ylong_http_client/src/util/c_openssl/error.rs +++ b/ylong_http_client/src/util/c_openssl/error.rs @@ -266,3 +266,102 @@ pub(crate) const fn error_get_reason(code: c_ulong) -> c_int { return ((2 as c_ulong * (error_system_error(code) as c_ulong)) | ((code & 0x7FFFFF) * (!error_system_error(code) as c_ulong))) as c_int; } + +pub(crate) struct VerifyError { + kind: VerifyKind, + cause: Reason, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum VerifyKind { + PubKeyPinning, +} + +pub(crate) enum Reason { + Msg(&'static str), +} + +impl VerifyError { + pub(crate) fn from_msg(kind: VerifyKind, msg: &'static str) -> Self { + Self { + kind, + cause: Reason::Msg(msg), + } + } +} + +impl VerifyKind { + pub fn as_str(&self) -> &'static str { + match self { + Self::PubKeyPinning => "Public Key Pinning Error", + } + } +} + +impl fmt::Debug for VerifyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut builder = f.debug_struct("VerifyError"); + builder.field("ErrorKind", &self.kind); + builder.field("Cause", &self.cause); + builder.finish() + } +} + +impl fmt::Display for VerifyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.kind.as_str())?; + write!(f, ": {}", self.cause)?; + Ok(()) + } +} + +impl fmt::Debug for Reason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Msg(msg) => write!(f, "{}", msg), + } + } +} + +impl fmt::Display for Reason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Msg(msg) => write!(f, "{}", msg), + } + } +} + +impl Error for VerifyError {} + +#[cfg(test)] +mod ut_c_openssl_error { + use crate::util::c_openssl::error::{VerifyError, VerifyKind}; + + /// UT test cases for `VerifyKind::as_str`. + /// + /// # Brief + /// 1. Transfer ErrorKind to str a by calling `VerifyKind::as_str`. + /// 2. Checks if the results are correct. + #[test] + fn ut_verify_err_as_str() { + assert_eq!( + VerifyKind::PubKeyPinning.as_str(), + "Public Key Pinning Error" + ); + } + + /// UT test cases for `VerifyKind::from` function. + /// + /// # Brief + /// 1. Calls `VerifyKind::from`. + /// 2. Checks if the results are correct. + #[test] + fn ut_verify_err_from() { + let error = VerifyError::from_msg(VerifyKind::PubKeyPinning, "error"); + assert_eq!( + format!("{:?}", error), + "VerifyError { ErrorKind: PubKeyPinning, Cause: error }" + ); + assert_eq!(format!("{error}"), "Public Key Pinning Error: error"); + } +} diff --git a/ylong_http_client/src/util/c_openssl/ffi/ssl.rs b/ylong_http_client/src/util/c_openssl/ffi/ssl.rs index f8a837ffb0171525e6c6730db4c77871ca59dbc3..3af302050d38a020f7e56abcdb9919b743933caf 100644 --- a/ylong_http_client/src/util/c_openssl/ffi/ssl.rs +++ b/ylong_http_client/src/util/c_openssl/ffi/ssl.rs @@ -14,7 +14,7 @@ use libc::{c_char, c_int, c_long, c_uchar, c_uint, c_void}; use super::bio::BIO; -use super::x509::{X509_STORE, X509_STORE_CTX, X509_VERIFY_PARAM}; +use super::x509::{C_X509, X509_STORE, X509_STORE_CTX, X509_VERIFY_PARAM}; /// This is the global context structure which is created by a server or client /// once per program life-time and which holds mainly default values for the @@ -149,6 +149,8 @@ extern "C" { /// by the peer, if any. pub(crate) fn SSL_get_verify_result(ssl: *const SSL) -> c_long; + pub(crate) fn SSL_get1_peer_certificate(ssl: *const SSL) -> *mut C_X509; + pub(crate) fn SSL_set_bio(ssl: *mut SSL, rbio: *mut BIO, wbio: *mut BIO); pub(crate) fn SSL_get_rbio(ssl: *const SSL) -> *mut BIO; diff --git a/ylong_http_client/src/util/c_openssl/ffi/x509.rs b/ylong_http_client/src/util/c_openssl/ffi/x509.rs index b74764be9dc9a5760547d098bef5b823c8b786e0..8623a60c40de52e5870ade2f504bd6d83b8145ed 100644 --- a/ylong_http_client/src/util/c_openssl/ffi/x509.rs +++ b/ylong_http_client/src/util/c_openssl/ffi/x509.rs @@ -19,6 +19,28 @@ extern "C" { pub(crate) fn EVP_PKEY_free(ctx: *mut EVP_PKEY); } +pub(crate) enum EVP_MD_CTX {} + +pub(crate) enum EVP_MD {} + +extern "C" { + pub(crate) fn EVP_MD_CTX_new() -> *mut EVP_MD_CTX; + + pub(crate) fn EVP_sha256() -> *mut EVP_MD; + + pub(crate) fn EVP_DigestInit(ctx: *mut EVP_MD_CTX, md: *mut EVP_MD) -> c_int; + + pub(crate) fn EVP_MD_CTX_free(ctx: *mut EVP_MD_CTX); + + pub(crate) fn EVP_DigestUpdate(ctx: *mut EVP_MD_CTX, buf: *const c_uchar, cnt: c_int) -> c_int; + + pub(crate) fn EVP_DigestFinal_ex( + ctx: *mut EVP_MD_CTX, + buf: *const c_uchar, + start: *const c_uint, + ); +} + pub(crate) enum C_X509 {} // for `C_X509` @@ -113,3 +135,12 @@ extern "C" { } pub(crate) enum STACK_X509 {} +pub(crate) enum X509_PUBKEY {} + +extern "C" { + pub(crate) fn X509_get_X509_PUBKEY(x509: *mut C_X509) -> *mut X509_PUBKEY; + + pub(crate) fn X509_PUBKEY_free(x509: *mut X509_PUBKEY); + + pub(crate) fn i2d_X509_PUBKEY(pubkey: *const X509_PUBKEY, buf: *mut *const c_uchar) -> c_int; +} diff --git a/ylong_http_client/src/util/c_openssl/mod.rs b/ylong_http_client/src/util/c_openssl/mod.rs index 6167c7b65f80394a75f70443db6d16616b5d875f..3188e4794796c8e0b2d96fefe9e987073697cde4 100644 --- a/ylong_http_client/src/util/c_openssl/mod.rs +++ b/ylong_http_client/src/util/c_openssl/mod.rs @@ -26,6 +26,7 @@ pub(crate) mod stack; pub(crate) mod x509; pub mod adapter; +pub(crate) mod verify; use core::ptr; use std::sync::Once; @@ -33,6 +34,7 @@ use std::sync::Once; pub use adapter::{Cert, Certificate, TlsConfig, TlsConfigBuilder, TlsFileType, TlsVersion}; use error::ErrorStack; use libc::c_int; +pub use verify::{PubKeyPins, PubKeyPinsBuilder}; pub(crate) use crate::util::c_openssl::ffi::callback::*; use crate::util::c_openssl::ffi::OPENSSL_init_ssl; diff --git a/ylong_http_client/src/util/c_openssl/ssl/error.rs b/ylong_http_client/src/util/c_openssl/ssl/error.rs index 9d716943426867c195a6d817a799b7fd6f7f6ce1..61ae36ff0b3f448addeb693cf6ed0c1ea771da06 100644 --- a/ylong_http_client/src/util/c_openssl/ssl/error.rs +++ b/ylong_http_client/src/util/c_openssl/ssl/error.rs @@ -19,6 +19,7 @@ use libc::c_int; use super::MidHandshakeSslStream; use crate::c_openssl::error::ErrorStack; +use crate::util::c_openssl::error::VerifyError; #[derive(Debug)] pub(crate) struct SslError { @@ -30,6 +31,7 @@ pub(crate) struct SslError { pub(crate) enum InternalError { Io(io::Error), Ssl(ErrorStack), + User(VerifyError), } impl SslError { @@ -57,6 +59,7 @@ impl Error for SslError { match self.internal { Some(InternalError::Io(ref e)) => Some(e), Some(InternalError::Ssl(ref e)) => Some(e), + Some(InternalError::User(ref e)) => Some(e), None => None, } } @@ -180,7 +183,8 @@ mod ut_ssl_error { use std::error::Error; use std::io; - use crate::util::c_openssl::error::ErrorStack; + use crate::util::c_openssl::error::VerifyKind::PubKeyPinning; + use crate::util::c_openssl::error::{ErrorStack, VerifyError}; use crate::util::c_openssl::ssl::{InternalError, SslError, SslErrorCode}; /// UT test cases for `SslErrorCode::from_int`. @@ -244,6 +248,14 @@ mod ut_ssl_error { internal: None, }; assert!(ssl_error.source().is_none()); + let ssl_error = SslError { + code: SslErrorCode::ZERO_RETURN, + internal: Some(InternalError::User(VerifyError::from_msg( + PubKeyPinning, + "error", + ))), + }; + assert!(ssl_error.source().is_some()); } /// UT test cases for `SslError::fmt`. diff --git a/ylong_http_client/src/util/c_openssl/ssl/ssl_base.rs b/ylong_http_client/src/util/c_openssl/ssl/ssl_base.rs index 9eb56165304985d5899ac40c5d9bc16efdbe4c3e..1f65d12706a979c03214e4ee1b95a7470b4777c2 100644 --- a/ylong_http_client/src/util/c_openssl/ssl/ssl_base.rs +++ b/ylong_http_client/src/util/c_openssl/ssl/ssl_base.rs @@ -64,7 +64,7 @@ impl Ssl { use super::MidHandshakeSslStream; use crate::c_openssl::ffi::ssl::SSL_connect; - let mut stream = SslStream::new_base(self, stream)?; + let mut stream = SslStream::new_base(self, stream, None)?; let ret = unsafe { SSL_connect(stream.ssl.as_ptr()) }; if ret > 0 { Ok(stream) diff --git a/ylong_http_client/src/util/c_openssl/ssl/stream.rs b/ylong_http_client/src/util/c_openssl/ssl/stream.rs index c058ef7a389c69399e8c3aca609b5a8ddf33eba1..43f4b1badc4150c468cb61b5faa3641101ff1bce 100644 --- a/ylong_http_client/src/util/c_openssl/ssl/stream.rs +++ b/ylong_http_client/src/util/c_openssl/ssl/stream.rs @@ -16,6 +16,7 @@ use core::marker::PhantomData; use core::mem::ManuallyDrop; use std::io::{self, Read, Write}; use std::panic::resume_unwind; +use std::ptr; use libc::c_int; @@ -24,12 +25,19 @@ use crate::c_openssl::bio::{self, get_error, get_panic, get_stream_mut, get_stre use crate::c_openssl::error::ErrorStack; use crate::c_openssl::ffi::ssl::{SSL_connect, SSL_set_bio, SSL_shutdown}; use crate::c_openssl::foreign::Foreign; +use crate::util::base64::encode; use crate::util::c_openssl::bio::BioMethod; +use crate::util::c_openssl::error::VerifyError; +use crate::util::c_openssl::error::VerifyKind::PubKeyPinning; +use crate::util::c_openssl::ffi::ssl::{SSL_get1_peer_certificate, SSL}; +use crate::util::c_openssl::ffi::x509::{i2d_X509_PUBKEY, X509_free, X509_get_X509_PUBKEY}; +use crate::util::c_openssl::verify::sha256_digest; /// A TLS session over a stream. pub struct SslStream { pub(crate) ssl: ManuallyDrop, method: ManuallyDrop, + pinned_pubkey: Option, p: PhantomData, } @@ -130,7 +138,11 @@ impl SslStream { } } - pub(crate) fn new_base(ssl: Ssl, stream: S) -> Result { + pub(crate) fn new_base( + ssl: Ssl, + stream: S, + pinned_pubkey: Option, + ) -> Result { unsafe { let (bio, method) = bio::new(stream)?; SSL_set_bio(ssl.as_ptr(), bio, bio); @@ -138,6 +150,7 @@ impl SslStream { Ok(SslStream { ssl: ManuallyDrop::new(ssl), method: ManuallyDrop::new(method), + pinned_pubkey, p: PhantomData, }) } @@ -146,6 +159,12 @@ impl SslStream { pub(crate) fn connect(&mut self) -> Result<(), SslError> { let ret = unsafe { SSL_connect(self.ssl.as_ptr()) }; if ret > 0 { + match &self.pinned_pubkey { + None => {} + Some(key) => { + verify_server_cert(self.ssl.as_ptr(), key.as_str())?; + } + } Ok(()) } else { Err(self.get_error(ret)) @@ -232,3 +251,93 @@ pub(crate) enum ShutdownResult { Sent, Received, } + +// TODO The SSLError thrown here is meaningless and has no information. +fn verify_server_cert(ssl: *const SSL, pinned_key: &str) -> Result<(), SslError> { + let certificate = unsafe { SSL_get1_peer_certificate(ssl) }; + if certificate.is_null() { + return Err(SslError { + code: SslErrorCode::SSL, + internal: Some(InternalError::User(VerifyError::from_msg( + PubKeyPinning, + "Failed to get the peer certificate.", + ))), + }); + } + + let size_1 = unsafe { i2d_X509_PUBKEY(X509_get_X509_PUBKEY(certificate), ptr::null_mut()) }; + if size_1 < 1 { + unsafe { X509_free(certificate) }; + return Err(SslError { + code: SslErrorCode::SSL, + internal: Some(InternalError::User(VerifyError::from_msg( + PubKeyPinning, + "Failed to get the length of the peer public key.", + ))), + }); + } + let key = vec![0u8; size_1 as usize]; + let size_2 = unsafe { i2d_X509_PUBKEY(X509_get_X509_PUBKEY(certificate), &mut key.as_ptr()) }; + + if size_1 != size_2 || size_2 <= 0 { + unsafe { X509_free(certificate) }; + return Err(SslError { + code: SslErrorCode::SSL, + internal: Some(InternalError::User(VerifyError::from_msg( + PubKeyPinning, + "Failed to read the peer public key.", + ))), + }); + } + + // sha256 length. + let mut digest = [0u8; 32]; + unsafe { sha256_digest(key.as_slice(), size_2, &mut digest)? } + let base64_digest = encode(&digest); + + let mut user_bytes = pinned_key.as_bytes(); + + let mut begin; + let mut end; + let prefix = b"sha256//"; + let suffix = b";sha256//"; + while !user_bytes.is_empty() { + begin = match user_bytes + .windows(prefix.len()) + .position(|window| window == prefix) + { + None => { + break; + } + Some(index) => index + 8, + }; + end = match user_bytes + .windows(suffix.len()) + .position(|window| window == suffix) + { + None => user_bytes.len(), + Some(index) => index, + }; + + let bytes = &user_bytes[begin..end]; + if bytes.eq(base64_digest.as_slice()) { + unsafe { X509_free(certificate) }; + return Ok(()); + } + + if end != user_bytes.len() { + user_bytes = &user_bytes[end + 1..]; + } else { + user_bytes = &user_bytes[end..]; + } + } + + unsafe { X509_free(certificate) }; + Err(SslError { + code: SslErrorCode::SSL, + internal: Some(InternalError::User(VerifyError::from_msg( + PubKeyPinning, + "Pinned public key verification failed.", + ))), + }) +} diff --git a/ylong_http_client/src/util/c_openssl/verify/mod.rs b/ylong_http_client/src/util/c_openssl/verify/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..1271ffc7f0fdb97724410caa147ec3714a294a1c --- /dev/null +++ b/ylong_http_client/src/util/c_openssl/verify/mod.rs @@ -0,0 +1,21 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Server's Public Key pinning. +//! The trusted public key digest about the server passed by the user, encoded +//! in base64 after using the sha256 digest. + +mod pinning; + +pub(crate) use pinning::sha256_digest; +pub use pinning::{PubKeyPins, PubKeyPinsBuilder}; diff --git a/ylong_http_client/src/util/c_openssl/verify/pinning.rs b/ylong_http_client/src/util/c_openssl/verify/pinning.rs new file mode 100644 index 0000000000000000000000000000000000000000..7c6851b6dc5e46f0bc4a19a8b9825f02bbded84e --- /dev/null +++ b/ylong_http_client/src/util/c_openssl/verify/pinning.rs @@ -0,0 +1,306 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use libc::c_int; +use ylong_http::request::uri::Uri; + +use crate::util::c_openssl::error::VerifyError; +use crate::util::c_openssl::error::VerifyKind::PubKeyPinning; +use crate::util::c_openssl::ffi::x509::{ + EVP_DigestFinal_ex, EVP_DigestInit, EVP_DigestUpdate, EVP_MD_CTX_free, EVP_MD_CTX_new, + EVP_sha256, +}; +use crate::util::c_openssl::ssl::{InternalError, SslError, SslErrorCode}; +use crate::ErrorKind::Build; +use crate::HttpClientError; + +/// A structure that serves Certificate and Public Key Pinning. +/// The map key is server authority(host:port), value is Base64(sha256(Server's +/// Public Key)). +/// +/// # Examples +/// +/// ``` +/// use ylong_http_client::PubKeyPins; +/// +/// let pins = PubKeyPins::builder() +/// .add( +/// "https://example.com", +/// "sha256//VHQAbNl67nmkZJNESeYKvTxb5bTmd1maWnMKG/tjcAY=", +/// ) +/// .build() +/// .unwrap(); +/// ``` +#[derive(Clone)] +pub struct PubKeyPins { + pub(crate) pub_keys: HashMap, +} + +/// A builder which is used to construct `PubKeyPins`. +/// +/// # Examples +/// +/// ``` +/// use ylong_http_client::PubKeyPinsBuilder; +/// +/// let builder = PubKeyPinsBuilder::new(); +/// ``` +pub struct PubKeyPinsBuilder { + pub_keys: Result, HttpClientError>, +} + +impl PubKeyPinsBuilder { + /// Creates a new `PubKeyPinsBuilder`. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::PubKeyPinsBuilder; + /// + /// let builder = PubKeyPinsBuilder::new(); + /// ``` + pub fn new() -> Self { + Self { + pub_keys: Ok(HashMap::new()), + } + } + + /// Sets a tuple of (server, public key digest) for `PubKeyPins`. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::PubKeyPinsBuilder; + /// + /// let pins = PubKeyPinsBuilder::new() + /// .add( + /// "https://example.com", + /// "sha256//VHQAbNl67nmkZJNESeYKvTxb5bTmd1maWnMKG/tjcAY=", + /// ) + /// .build() + /// .unwrap(); + /// ``` + pub fn add(mut self, uri: &str, digest: &str) -> Self { + self.pub_keys = self.pub_keys.and_then(move |mut keys| { + let parsed = Uri::try_from(uri).map_err(|e| HttpClientError::from_error(Build, e))?; + let auth = match (parsed.host(), parsed.port()) { + (None, _) => { + return err_from_msg!(Build, "uri has no host"); + } + (Some(host), Some(port)) => { + format!("{}:{}", host.as_str(), port.as_str()) + } + (Some(host), None) => { + format!("{}:443", host.as_str()) + } + }; + let pub_key = String::from(digest); + let _ = keys.insert(auth, pub_key); + Ok(keys) + }); + self + } + + /// Builds a `PubKeyPins`. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::PubKeyPinsBuilder; + /// + /// let pins = PubKeyPinsBuilder::new() + /// .add( + /// "https://example.com", + /// "sha256//VHQAbNl67nmkZJNESeYKvTxb5bTmd1maWnMKG/tjcAY=", + /// ) + /// .build() + /// .unwrap(); + /// ``` + pub fn build(self) -> Result { + Ok(PubKeyPins { + pub_keys: self.pub_keys?, + }) + } +} + +impl PubKeyPins { + /// Creates a new builder for `PubKeyPins`. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::PubKeyPins; + /// + /// let builder = PubKeyPins::builder(); + /// ``` + pub fn builder() -> PubKeyPinsBuilder { + PubKeyPinsBuilder::new() + } + pub(crate) fn get_pin(&self, domain: &str) -> Option { + self.pub_keys.get(&String::from(domain)).cloned() + } +} + +/// The Default implement of `PubKeyPinsBuilder`. +impl Default for PubKeyPinsBuilder { + /// Creates a new builder for `PubKeyPins`. + /// + /// # Examples + /// + /// ``` + /// use ylong_http_client::PubKeyPinsBuilder; + /// + /// let builder = PubKeyPinsBuilder::default(); + /// ``` + fn default() -> Self { + Self::new() + } +} + +// TODO The SSLError thrown here is meaningless and has no information. +pub(crate) unsafe fn sha256_digest( + pub_key: &[u8], + len: c_int, + digest: &mut [u8], +) -> Result<(), SslError> { + let md_ctx = EVP_MD_CTX_new(); + if md_ctx.is_null() { + return Err(SslError { + code: SslErrorCode::SSL, + internal: Some(InternalError::User(VerifyError::from_msg( + PubKeyPinning, + "Failed to allocates a digest context.", + ))), + }); + } + let init = EVP_DigestInit(md_ctx, EVP_sha256()); + if init == 0 { + EVP_MD_CTX_free(md_ctx); + return Err(SslError { + code: SslErrorCode::SSL, + internal: Some(InternalError::User(VerifyError::from_msg( + PubKeyPinning, + "Failed to set up digest context.", + ))), + }); + } + EVP_DigestUpdate(md_ctx, pub_key.as_ptr(), len); + + let start = 0; + EVP_DigestFinal_ex(md_ctx, digest.as_mut_ptr(), &start); + + EVP_MD_CTX_free(md_ctx); + + Ok(()) +} + +#[cfg(test)] +mod ut_verify_pinning { + use std::collections::HashMap; + + use libc::c_int; + + use crate::util::c_openssl::verify::sha256_digest; + use crate::{PubKeyPins, PubKeyPinsBuilder}; + + /// UT test cases for `PubKeyPins::clone`. + /// + /// # Brief + /// 1. Creates a `PubKeyPins`. + /// 2. Calls `PubKeyPins::clone` . + /// 3. Checks if the assert result is correct. + #[test] + fn ut_pubkey_pins_clone() { + let mut map = HashMap::new(); + let _value = map.insert( + "ylong_http.com:443".to_string(), + "sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=".to_string(), + ); + let pins = PubKeyPins { pub_keys: map }; + let pins_clone = pins.clone(); + assert_eq!(pins.pub_keys, pins_clone.pub_keys); + } + + /// UT test cases for `PubKeyPinsBuilder::add`. + /// + /// # Brief + /// 1. Creates a `PubKeyPinsBuilder`. + /// 2. Calls `PubKeyPins::add` . + /// 3. Checks if the assert result is correct. + #[test] + fn ut_pubkey_pins_builder_add() { + let pins = PubKeyPins::builder() + .add( + "/data/storage", + "sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=", + ) + .build() + .err(); + assert_eq!( + format!("{:?}", pins.unwrap()), + "HttpClientError { ErrorKind: Build, Cause: uri has no host }" + ); + let pins = PubKeyPinsBuilder::default() + .add( + "https://ylong_http.com", + "sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=", + ) + .build() + .unwrap(); + assert_eq!( + pins.get_pin("ylong_http.com:443"), + Some("sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=".to_string()) + ); + } + + /// UT test cases for `sha256_digest. + /// + /// # Brief + /// 1. Calls `sha256_digest` . + /// 2. Checks if the assert result is correct. + #[test] + fn ut_pubkey_sha256_digest() { + let pubkey = + bytes_from_hex("d0e8b8f11c98f369016eb2ed3c541e1f01382f9d5b3104c9ffd06b6175a46271") + .unwrap(); + + let key_words = Vec::from("Hello, SHA-256!"); + + let mut hash = [0u8; 32]; + assert!(unsafe { + sha256_digest(key_words.as_slice(), key_words.len() as c_int, &mut hash) + } + .is_ok()); + + assert_eq!(hash.as_slice(), pubkey.as_slice()); + } + + fn bytes_from_hex(str: &str) -> Option> { + if str.len() % 2 != 0 { + return None; + } + let mut vec = Vec::new(); + let mut remained = str; + while !remained.is_empty() { + let (left, right) = remained.split_at(2); + match u8::from_str_radix(left, 16) { + Ok(num) => vec.push(num), + Err(_) => return None, + } + remained = right; + } + Some(vec) + } +} diff --git a/ylong_http_client/src/util/c_openssl/x509.rs b/ylong_http_client/src/util/c_openssl/x509.rs index 16a9d5b0a0125c2f425b89dfb83e28ee6dc9b8e9..0846efce89a34b77e3ea45876f84d88c9808af39 100644 --- a/ylong_http_client/src/util/c_openssl/x509.rs +++ b/ylong_http_client/src/util/c_openssl/x509.rs @@ -25,12 +25,12 @@ use super::ffi::pem::PEM_read_bio_X509; #[cfg(feature = "c_openssl_3_0")] use super::ffi::x509::X509_STORE_load_path; use super::ffi::x509::{ - d2i_X509, EVP_PKEY_free, X509_NAME_free, X509_NAME_oneline, X509_STORE_CTX_free, - X509_STORE_CTX_get0_cert, X509_STORE_add_cert, X509_STORE_free, X509_STORE_new, - X509_VERIFY_PARAM_free, X509_VERIFY_PARAM_set1_host, X509_VERIFY_PARAM_set1_ip, + d2i_X509, EVP_PKEY_free, X509_NAME_free, X509_NAME_oneline, X509_PUBKEY_free, + X509_STORE_CTX_free, X509_STORE_CTX_get0_cert, X509_STORE_add_cert, X509_STORE_free, + X509_STORE_new, X509_VERIFY_PARAM_free, X509_VERIFY_PARAM_set1_host, X509_VERIFY_PARAM_set1_ip, X509_VERIFY_PARAM_set_hostflags, X509_get_issuer_name, X509_get_pubkey, X509_get_subject_name, X509_get_version, X509_up_ref, X509_verify, X509_verify_cert_error_string, EVP_PKEY, - STACK_X509, X509_NAME, X509_STORE, X509_STORE_CTX, X509_VERIFY_PARAM, + STACK_X509, X509_NAME, X509_PUBKEY, X509_STORE, X509_STORE_CTX, X509_VERIFY_PARAM, }; use super::foreign::{Foreign, ForeignRef}; use super::stack::Stackof; @@ -288,6 +288,13 @@ impl X509StoreContextRef { } } +foreign_type!( + type CStruct = X509_PUBKEY; + fn drop = X509_PUBKEY_free; + pub(crate) struct X509PubKey; + pub(crate) struct X509PubKeyRef; +); + #[cfg(test)] mod ut_x509 { diff --git a/ylong_http_client/src/util/mod.rs b/ylong_http_client/src/util/mod.rs index 6a7b22c40a5e8ee47c9956eef6cd179cfa2f7598..4c7aade51c060f8ebc1c8818caf5c4d847dd456b 100644 --- a/ylong_http_client/src/util/mod.rs +++ b/ylong_http_client/src/util/mod.rs @@ -36,7 +36,10 @@ pub(crate) mod dispatcher; pub(crate) mod test_utils; #[cfg(feature = "__c_openssl")] -pub use c_openssl::{Cert, Certificate, TlsConfig, TlsConfigBuilder, TlsFileType, TlsVersion}; +pub use c_openssl::{ + Cert, Certificate, PubKeyPins, PubKeyPinsBuilder, TlsConfig, TlsConfigBuilder, TlsFileType, + TlsVersion, +}; #[cfg(feature = "__tls")] pub use config::{AlpnProtocol, AlpnProtocolList, CertVerifier, ServerCerts}; pub use config::{Proxy, ProxyBuilder, Redirect, Retry, SpeedLimit, Timeout}; diff --git a/ylong_http_client/tests/common/async_utils.rs b/ylong_http_client/tests/common/async_utils.rs index 29c2fc58380678104da00dad3de611755e552d66..dc2fbbd5745a847118a675566c633f6f60ad6c82 100644 --- a/ylong_http_client/tests/common/async_utils.rs +++ b/ylong_http_client/tests/common/async_utils.rs @@ -16,7 +16,7 @@ macro_rules! async_client_test_case { ( HTTPS; ServeFnName: $server_fn_name: ident, - Tls: $tls_config: expr, + RootCA: $ca_file: expr, RuntimeThreads: $thread_num: expr, $(ClientNum: $client_num: expr,)? $(Request: { @@ -76,7 +76,7 @@ macro_rules! async_client_test_case { let mut shut_downs = vec![]; async_client_assert!( HTTPS; - Tls: $tls_config, + RootCA: $ca_file, Runtime: runtime, ServerNum: server_num, Handles: handles_vec, @@ -198,7 +198,7 @@ macro_rules! async_client_test_case { macro_rules! async_client_assert { ( HTTPS; - Tls: $tls_config: expr, + RootCA: $ca_file: expr, Runtime: $runtime: expr, ServerNum: $server_num: expr, Handles: $handle_vec: expr, @@ -221,7 +221,7 @@ macro_rules! async_client_assert { },)* ) => {{ let client = ylong_http_client::async_impl::Client::builder() - .tls_ca_file($tls_config) + .tls_ca_file($ca_file) .danger_accept_invalid_hostnames(true) .build() .unwrap(); diff --git a/ylong_http_client/tests/sdv_async_https_c_ssl.rs b/ylong_http_client/tests/sdv_async_https_c_ssl.rs index 5b39191297629f7e61771f51f1b7847489857037..698734c51314cd045c40c5a96037fee052932c6a 100644 --- a/ylong_http_client/tests/sdv_async_https_c_ssl.rs +++ b/ylong_http_client/tests/sdv_async_https_c_ssl.rs @@ -36,7 +36,7 @@ fn sdv_async_client_send_request() { async_client_test_case!( HTTPS; ServeFnName: ylong_server_fn, - Tls: path.to_str().unwrap(), + RootCA: path.to_str().unwrap(), RuntimeThreads: 1, Request: { Method: "GET", @@ -56,7 +56,7 @@ fn sdv_async_client_send_request() { async_client_test_case!( HTTPS; ServeFnName: ylong_server_fn, - Tls: path.to_str().unwrap(), + RootCA: path.to_str().unwrap(), RuntimeThreads: 1, Request: { Method: "HEAD", @@ -75,7 +75,7 @@ fn sdv_async_client_send_request() { async_client_test_case!( HTTPS; ServeFnName: ylong_server_fn, - Tls: path.to_str().unwrap(), + RootCA: path.to_str().unwrap(), RuntimeThreads: 1, Request: { Method: "POST", @@ -95,7 +95,7 @@ fn sdv_async_client_send_request() { async_client_test_case!( HTTPS; ServeFnName: ylong_server_fn, - Tls: path.to_str().unwrap(), + RootCA: path.to_str().unwrap(), RuntimeThreads: 1, Request: { Method: "HEAD", @@ -113,7 +113,7 @@ fn sdv_async_client_send_request() { async_client_test_case!( HTTPS; ServeFnName: ylong_server_fn, - Tls: path.to_str().unwrap(), + RootCA: path.to_str().unwrap(), RuntimeThreads: 1, Request: { Method: "PUT", @@ -139,7 +139,7 @@ fn sdv_client_send_request_repeatedly() { async_client_test_case!( HTTPS; ServeFnName: ylong_server_fn, - Tls: path.to_str().unwrap(), + RootCA: path.to_str().unwrap(), RuntimeThreads: 2, Request: { Method: "GET", @@ -177,7 +177,7 @@ fn sdv_client_making_multiple_connections() { async_client_test_case!( HTTPS; ServeFnName: ylong_server_fn, - Tls: path.to_str().unwrap(), + RootCA: path.to_str().unwrap(), RuntimeThreads: 2, ClientNum: 5, Request: { diff --git a/ylong_http_client/tests/sdv_async_https_pinning.rs b/ylong_http_client/tests/sdv_async_https_pinning.rs new file mode 100644 index 0000000000000000000000000000000000000000..dcf8547cf347eb3db8680d8410bfb02ee65315df --- /dev/null +++ b/ylong_http_client/tests/sdv_async_https_pinning.rs @@ -0,0 +1,291 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(all( + feature = "async", + feature = "http1_1", + feature = "__c_openssl", + feature = "tokio_base" +))] + +#[macro_use] +mod common; + +use std::path::PathBuf; + +use ylong_http_client::PubKeyPins; + +use crate::common::init_test_work_runtime; + +/// SDV test cases for `async::Client`. +/// +/// # Brief +/// 1. Starts a hyper https server with the tokio coroutine. +/// 2. Creates an async::Client that with public key pinning. +/// 3. The client sends a request message. +/// 4. Verifies the received request on the server. +/// 5. The server sends a response message. +/// 6. Verifies the received response on the client. +/// 7. Shuts down the server. +#[test] +fn sdv_client_public_key_pinning() { + define_service_handle!(HTTPS;); + set_server_fn!( + ASYNC; + ylong_server_fn, + Request: { + Method: "GET", + Header: "Content-Length", "5", + Body: "hello", + }, + Response: { + Status: 200, + Version: "HTTP/1.1", + Header: "Content-Length", "3", + Body: "hi!", + }, + ); + let runtime = init_test_work_runtime(1); + let mut handles_vec = vec![]; + let dir = env!("CARGO_MANIFEST_DIR"); + let mut path = PathBuf::from(dir); + path.push("tests/file/root-ca.pem"); + + { + start_server!( + HTTPS; + ServerNum: 1, + Runtime: runtime, + Handles: handles_vec, + ServeFnName: ylong_server_fn, + ); + let handle = handles_vec.pop().expect("No more handles !"); + + let pins = PubKeyPins::builder() + .add( + format!("https://127.0.0.1:{}", handle.port).as_str(), + "sha256//VHQAbNl67nmkZJNESeTKvTxb5bQmd1maWnMKG/tjcAY=", + ) + .build() + .unwrap(); + + let client = ylong_http_client::async_impl::Client::builder() + .tls_ca_file(path.to_str().unwrap()) + .add_public_key_pins(pins) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + + let shutdown_handle = runtime.spawn(async move { + async_client_assertions!( + ServerHandle: handle, + ClientRef: client, + Request: { + Method: "GET", + Host: "127.0.0.1", + Header: "Content-Length", "5", + Body: "hello", + }, + Response: { + Status: 200, + Version: "HTTP/1.1", + Header: "Content-Length", "3", + Body: "hi!", + }, + ); + }); + runtime + .block_on(shutdown_handle) + .expect("Runtime block on server shutdown failed"); + } + + { + start_server!( + HTTPS; + ServerNum: 1, + Runtime: runtime, + Handles: handles_vec, + ServeFnName: ylong_server_fn, + ); + let handle = handles_vec.pop().expect("No more handles !"); + + // Two wrong public keys and a correct public key in the middle. + let pins = PubKeyPins::builder() + .add( + format!("https://127.0.0.1:{}", handle.port).as_str(), + "sha256//YhKJKSzoTt2b5FP18fvpHo7fJYqQCjAa3HWY3tvRMwE=;sha256//VHQAbNl67nmkZJNESeTKvTxb5bQmd1maWnMKG/tjcAY=;sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=", + ) + .build() + .unwrap(); + + let client = ylong_http_client::async_impl::Client::builder() + .tls_ca_file(path.to_str().unwrap()) + .add_public_key_pins(pins) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + + let shutdown_handle = runtime.spawn(async move { + async_client_assertions!( + ServerHandle: handle, + ClientRef: client, + Request: { + Method: "GET", + Host: "127.0.0.1", + Header: "Content-Length", "5", + Body: "hello", + }, + Response: { + Status: 200, + Version: "HTTP/1.1", + Header: "Content-Length", "3", + Body: "hi!", + }, + ); + }); + runtime + .block_on(shutdown_handle) + .expect("Runtime block on server shutdown failed"); + } + + { + start_server!( + HTTPS; + ServerNum: 1, + Runtime: runtime, + Handles: handles_vec, + ServeFnName: ylong_server_fn, + ); + let handle = handles_vec.pop().expect("No more handles !"); + + // The public key of an irrelevant domain. + let pins = PubKeyPins::builder() + .add( + "https://ylong_http.test:6789", + "sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=", + ) + .build() + .unwrap(); + + let client = ylong_http_client::async_impl::Client::builder() + .tls_ca_file(path.to_str().unwrap()) + .add_public_key_pins(pins) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + + let shutdown_handle = runtime.spawn(async move { + async_client_assertions!( + ServerHandle: handle, + ClientRef: client, + Request: { + Method: "GET", + Host: "127.0.0.1", + Header: "Content-Length", "5", + Body: "hello", + }, + Response: { + Status: 200, + Version: "HTTP/1.1", + Header: "Content-Length", "3", + Body: "hi!", + }, + ); + }); + runtime + .block_on(shutdown_handle) + .expect("Runtime block on server shutdown failed"); + } +} + +/// SDV test cases for `async::Client`. +/// +/// # Brief +/// 1. Starts a hyper https server with the tokio coroutine. +/// 2. Creates an async::Client with an error public key pinning. +/// 3. The client sends a request message. +/// 4. Verifies the received request on the server. +/// 5. The server sends a response message. +/// 6. Verifies the received response on the client. +/// 7. Shuts down the server. +#[test] +fn sdv_client_public_key_pinning_error() { + define_service_handle!(HTTPS;); + set_server_fn!( + ASYNC; + ylong_server_fn, + Request: { + Method: "GET", + Header: "Content-Length", "5", + Body: "hello", + }, + Response: { + Status: 200, + Version: "HTTP/1.1", + Header: "Content-Length", "3", + Body: "hi!", + }, + ); + + let runtime = init_test_work_runtime(1); + + let mut handles_vec = vec![]; + start_server!( + HTTPS; + ServerNum: 1, + Runtime: runtime, + Handles: handles_vec, + ServeFnName: ylong_server_fn, + ); + let handle = handles_vec.pop().expect("No more handles !"); + + let pins = PubKeyPins::builder() + .add( + format!("https://127.0.0.1:{}", handle.port).as_str(), + "sha256//YhKJKSzoTt2b5FP18fvpHo7fJYqQCjAa3HWY3tvRMwE=", + ) + .build() + .unwrap(); + + let dir = env!("CARGO_MANIFEST_DIR"); + let mut path = PathBuf::from(dir); + path.push("tests/file/root-ca.pem"); + + let client = ylong_http_client::async_impl::Client::builder() + .tls_ca_file(path.to_str().unwrap()) + .add_public_key_pins(pins) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + + let shutdown_handle = runtime.spawn(async move { + let request = ylong_http_client::async_impl::Request::builder() + .method("GET") + .url(format!("{}:{}", "127.0.0.1", handle.port).as_str()) + .header("Content-Length", "5") + .body(ylong_http_client::async_impl::Body::slice("hello")) + .expect("Request build failed"); + + let response = client.request(request).await.err(); + + assert_eq!( + format!("{:?}", response.expect("response is not an error")), + "HttpClientError { ErrorKind: Connect, Cause: Custom { kind: Other, error: SslError {\ + code: SslErrorCode(1), internal: Some(User(VerifyError { ErrorKind: PubKeyPinning, \ + Cause: Pinned public key verification failed. })) } } }" + ); + }); + runtime + .block_on(shutdown_handle) + .expect("Runtime block on server shutdown failed"); +}