Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/liburlx-ffi/include/urlx.h
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ typedef enum CURLoption {
CURLOPT_STREAM_WEIGHT = 239,
CURLOPT_TCP_FASTOPEN = 244,
CURLOPT_SOCKS5_AUTH = 267,
CURLOPT_SSL_VERIFYSTATUS = 232,
CURLOPT_HTTP09_ALLOWED = 285,
CURLOPT_POSTFIELDSIZE_LARGE = 30120,
CURLOPT_INFILESIZE_LARGE = 30115,
Expand Down
8 changes: 8 additions & 0 deletions crates/liburlx-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ pub enum CURLoption {
CURLOPT_STREAM_WEIGHT = 239,
CURLOPT_TCP_FASTOPEN = 244,
CURLOPT_SOCKS5_AUTH = 267,
CURLOPT_SSL_VERIFYSTATUS = 232,
CURLOPT_HTTP09_ALLOWED = 285,

// Off_t options (CURLOPTTYPE_OFF_T = 30000)
Expand Down Expand Up @@ -2035,6 +2036,12 @@ pub unsafe extern "C" fn curl_easy_setopt(
CURLcode::CURLE_OK
}

// CURLOPT_SSL_VERIFYSTATUS = 232
232 => {
h.easy.ssl_verify_status(value as c_long != 0);
CURLcode::CURLE_OK
}

// CURLOPT_PROXY_SSL_VERIFYPEER = 248
248 => {
h.easy.proxy_ssl_verify_peer(value as c_long != 0);
Expand Down Expand Up @@ -4893,6 +4900,7 @@ fn error_to_curlcode(err: &liburlx::Error) -> CURLcode {
match err {
liburlx::Error::UrlParse(_) => CURLcode::CURLE_URL_MALFORMAT,
liburlx::Error::Connect(_) => CURLcode::CURLE_COULDNT_CONNECT,
liburlx::Error::SslInvalidCertStatus(_) => CURLcode::CURLE_SSL_INVALIDCERTSTATUS,
liburlx::Error::Tls(_) => CURLcode::CURLE_SSL_CONNECT_ERROR,
liburlx::Error::Http(msg) => {
if msg.contains("unsupported scheme") {
Expand Down
10 changes: 10 additions & 0 deletions crates/liburlx/src/easy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,16 @@ impl Easy {
self.tls_config.session_cache = enable;
}

/// Enable or disable OCSP certificate status verification.
///
/// When enabled, the server must provide a valid OCSP stapled response
/// during the TLS handshake. If no stapled response is provided, the
/// transfer fails with [`crate::error::Error::SslInvalidCertStatus`].
/// Equivalent to curl's `--cert-status` flag or `CURLOPT_SSL_VERIFYSTATUS`.
pub const fn ssl_verify_status(&mut self, enable: bool) {
self.tls_config.verify_status = enable;
}

/// Set the TLS-SRP username. Equivalent to curl's `--tlsuser`.
pub fn ssl_srp_user(&mut self, user: &str) {
self.tls_config.srp_user = Some(user.to_string());
Expand Down
5 changes: 5 additions & 0 deletions crates/liburlx/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ pub enum Error {
#[error("RTSP Session mismatch: {0}")]
RtspSessionError(String),

/// The server did not provide a valid OCSP certificate status response.
/// Maps to `CURLE_SSL_INVALIDCERTSTATUS` (91) at the FFI boundary.
#[error("SSL certificate status verification FAILED: {0}")]
SslInvalidCertStatus(String),

/// An SMB protocol error occurred.
#[error("SMB error: {0}")]
Smb(String),
Expand Down
205 changes: 191 additions & 14 deletions crates/liburlx/src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub enum TlsVersion {
/// Controls certificate verification, custom CA bundles, client certificates,
/// and TLS version negotiation.
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct TlsConfig {
/// Whether to verify the server's TLS certificate (default: true).
///
Expand Down Expand Up @@ -108,6 +109,14 @@ pub struct TlsConfig {

/// TLS-SRP password for password-based TLS authentication (RFC 5054).
pub srp_password: Option<String>,

/// Whether to request and verify the server's OCSP certificate status (default: false).
///
/// When `true`, the server must provide a valid OCSP stapled response
/// during the TLS handshake. If no stapled response is provided, the
/// connection fails with [`crate::error::Error::SslInvalidCertStatus`].
/// Equivalent to curl's `--cert-status` flag or `CURLOPT_SSL_VERIFYSTATUS`.
pub verify_status: bool,
}

impl Default for TlsConfig {
Expand All @@ -129,6 +138,7 @@ impl Default for TlsConfig {
crl_file: None,
srp_user: None,
srp_password: None,
verify_status: false,
}
}
}
Expand Down Expand Up @@ -158,6 +168,8 @@ mod rustls_impl {
config: Arc<rustls::ClientConfig>,
/// SHA-256 pin of the server's public key (base64-encoded).
pinned_public_key: Option<String>,
/// Whether to require OCSP stapled responses from the server.
verify_status: bool,
}

impl TlsConnector {
Expand Down Expand Up @@ -187,12 +199,16 @@ mod rustls_impl {
Self::build(tls_config, false)
}

/// Marker prefix used in OCSP verification errors for detection in `connect`.
const OCSP_ERROR_PREFIX: &str = "OCSP_CERT_STATUS:";

/// Internal builder shared by `new()` and `new_no_alpn()`.
#[allow(clippy::too_many_lines)] // verify_status branches add unavoidable depth
fn build(tls_config: &TlsConfig, use_http_alpn: bool) -> Result<Self, Error> {
let versions = Self::protocol_versions(tls_config);

let config = if !tls_config.verify_peer {
// Insecure mode: accept any certificate
// Insecure mode: accept any certificate (no OCSP check)
let mut config = Self::config_builder(&versions)
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoVerifier))
Expand All @@ -218,6 +234,8 @@ mod rustls_impl {
Error::Tls(format!("CRL verifier build failed: {e}").into())
})?;

let verifier = Self::maybe_wrap_ocsp(verifier, tls_config.verify_status);

let builder = Self::config_builder(&versions)
.dangerous()
.with_custom_certificate_verifier(verifier);
Expand All @@ -227,6 +245,26 @@ mod rustls_impl {
Self::configure_alpn(&mut config);
}
config
} else if tls_config.verify_status {
// Need explicit verifier for OCSP status checking
let verifier =
rustls::client::WebPkiServerVerifier::builder(Arc::new(root_store))
.build()
.map_err(|e| {
Error::Tls(format!("verifier build failed: {e}").into())
})?;
let verifier: Arc<dyn rustls::client::danger::ServerCertVerifier> =
Arc::new(OcspCheckingVerifier { inner: verifier });

let builder = Self::config_builder(&versions)
.dangerous()
.with_custom_certificate_verifier(verifier);

let mut config = Self::with_client_auth(builder, tls_config)?;
if use_http_alpn {
Self::configure_alpn(&mut config);
}
config
} else {
let builder =
Self::config_builder(&versions).with_root_certificates(root_store);
Expand All @@ -241,25 +279,69 @@ mod rustls_impl {
// Custom CA bundle from in-memory blob
let root_store = load_ca_certs_from_blob(ca_blob)?;

let builder = Self::config_builder(&versions).with_root_certificates(root_store);
if tls_config.verify_status {
let verifier =
rustls::client::WebPkiServerVerifier::builder(Arc::new(root_store))
.build()
.map_err(|e| {
Error::Tls(format!("verifier build failed: {e}").into())
})?;
let verifier: Arc<dyn rustls::client::danger::ServerCertVerifier> =
Arc::new(OcspCheckingVerifier { inner: verifier });

let mut config = Self::with_client_auth(builder, tls_config)?;
if use_http_alpn {
Self::configure_alpn(&mut config);
let builder = Self::config_builder(&versions)
.dangerous()
.with_custom_certificate_verifier(verifier);

let mut config = Self::with_client_auth(builder, tls_config)?;
if use_http_alpn {
Self::configure_alpn(&mut config);
}
config
} else {
let builder =
Self::config_builder(&versions).with_root_certificates(root_store);

let mut config = Self::with_client_auth(builder, tls_config)?;
if use_http_alpn {
Self::configure_alpn(&mut config);
}
config
}
config
} else {
// Default: system root certificates
let root_store: rustls::RootCertStore =
webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect();

let builder = Self::config_builder(&versions).with_root_certificates(root_store);
if tls_config.verify_status {
let verifier =
rustls::client::WebPkiServerVerifier::builder(Arc::new(root_store))
.build()
.map_err(|e| {
Error::Tls(format!("verifier build failed: {e}").into())
})?;
let verifier: Arc<dyn rustls::client::danger::ServerCertVerifier> =
Arc::new(OcspCheckingVerifier { inner: verifier });

let mut config = Self::with_client_auth(builder, tls_config)?;
if use_http_alpn {
Self::configure_alpn(&mut config);
let builder = Self::config_builder(&versions)
.dangerous()
.with_custom_certificate_verifier(verifier);

let mut config = Self::with_client_auth(builder, tls_config)?;
if use_http_alpn {
Self::configure_alpn(&mut config);
}
config
} else {
let builder =
Self::config_builder(&versions).with_root_certificates(root_store);

let mut config = Self::with_client_auth(builder, tls_config)?;
if use_http_alpn {
Self::configure_alpn(&mut config);
}
config
}
config
};

// Extract the base64 hash from the "sha256//..." format
Expand All @@ -268,7 +350,23 @@ mod rustls_impl {
.as_ref()
.and_then(|pin| pin.strip_prefix("sha256//").map(ToString::to_string));

Ok(Self { config: Arc::new(config), pinned_public_key })
Ok(Self {
config: Arc::new(config),
pinned_public_key,
verify_status: tls_config.verify_status,
})
}

/// Optionally wrap a verifier with OCSP status checking.
fn maybe_wrap_ocsp(
verifier: Arc<dyn rustls::client::danger::ServerCertVerifier>,
verify_status: bool,
) -> Arc<dyn rustls::client::danger::ServerCertVerifier> {
if verify_status {
Arc::new(OcspCheckingVerifier { inner: verifier })
} else {
verifier
}
}

/// Determine the allowed TLS protocol versions based on config.
Expand Down Expand Up @@ -351,7 +449,7 @@ mod rustls_impl {
let tls_stream = connector
.connect(server_name, stream)
.await
.map_err(|e| Error::Tls(Box::new(e)))?;
.map_err(|e| Self::map_tls_error(e, self.verify_status))?;

// Verify certificate pinning if configured
if let Some(ref expected_hash) = self.pinned_public_key {
Expand Down Expand Up @@ -392,7 +490,7 @@ mod rustls_impl {
let tls_stream = connector
.connect(server_name, stream)
.await
.map_err(|e| Error::Tls(Box::new(e)))?;
.map_err(|e| Self::map_tls_error(e, self.verify_status))?;

let alpn = tls_stream
.get_ref()
Expand Down Expand Up @@ -441,6 +539,85 @@ mod rustls_impl {

Ok(())
}

/// Map a TLS I/O error to the appropriate liburlx error type.
///
/// When `verify_status` is true, detects OCSP-related failures from the
/// custom verifier and returns [`Error::SslInvalidCertStatus`] instead
/// of the generic [`Error::Tls`].
fn map_tls_error(e: std::io::Error, verify_status: bool) -> Error {
if verify_status {
let msg = e.to_string();
if msg.contains(Self::OCSP_ERROR_PREFIX) {
return Error::SslInvalidCertStatus(
"SSL certificate status verification FAILED".to_string(),
);
}
}
Error::Tls(Box::new(e))
}
}

/// A certificate verifier wrapper that requires OCSP stapled responses.
///
/// Delegates normal certificate verification to the inner verifier,
/// then checks that the server provided a non-empty OCSP response.
/// Used when `--cert-status` / `CURLOPT_SSL_VERIFYSTATUS` is enabled.
#[derive(Debug)]
struct OcspCheckingVerifier {
inner: Arc<dyn rustls::client::danger::ServerCertVerifier>,
}

impl rustls::client::danger::ServerCertVerifier for OcspCheckingVerifier {
fn verify_server_cert(
&self,
end_entity: &rustls::pki_types::CertificateDer<'_>,
intermediates: &[rustls::pki_types::CertificateDer<'_>],
server_name: &rustls::pki_types::ServerName<'_>,
ocsp_response: &[u8],
now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
// Perform normal certificate verification first
let _ = self.inner.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
)?;

// Then require a non-empty OCSP stapled response
if ocsp_response.is_empty() {
return Err(rustls::Error::General(format!(
"{} no certificate status response from server",
TlsConnector::OCSP_ERROR_PREFIX,
)));
}

Ok(rustls::client::danger::ServerCertVerified::assertion())
}

fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls12_signature(message, cert, dss)
}

fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls13_signature(message, cert, dss)
}

fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.inner.supported_verify_schemes()
}
}

/// A certificate verifier that accepts any server certificate.
Expand Down
Loading
Loading