Skip to content

Commit 0be429f

Browse files
authored
Merge pull request #106 from Cysharp/feature/CertificateVerificationHandler
Add support for server certificate verification handler
2 parents 5876645 + ca888ac commit 0be429f

File tree

8 files changed

+246
-8
lines changed

8 files changed

+246
-8
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ Once the handler sends a request, these settings become immutable and cannot be
212212
|MaxIdlePerHost|Gets or sets the maximum idle connection per host allowed in the pool. Default is usize::MAX (no limit).|
213213
|Http2Only|Gets or sets a value that indicates whether to force the use of HTTP/2.|
214214
|SkipCertificateVerification|Gets or sets a value that indicates whether to skip certificate verification.|
215+
|OnVerifyServerCertificate|Gets or sets a custom handler that validates server certificates.|
215216
|RootCertificates|Gets or sets a custom root CA. By default, the built-in root CA (Mozilla's root certificates) is used. See also https://github.com/rustls/webpki-roots. |
216217
|ClientAuthCertificates|Gets or sets a custom client auth key.|
217218
|ClientAuthKey|Gets or sets a custom client auth certificates.|
@@ -280,6 +281,22 @@ using var handler = new YetAnotherHttpHandler() { RootCertificates = rootCerts }
280281
### Ignore certificate validation errors
281282
We strongly not recommend this, but in some cases, you may want to skip certificate validation when connecting via HTTPS. In this scenario, you can ignore certificate errors by setting the `SkipCertificateVerification` property to `true`.
282283

284+
### Handling server certificate verification
285+
You can customize the server certificate verification process by setting the `OnVerifyServerCertificate` property.
286+
287+
The callback should return `true` or `false` based on the verification result. If the property is set, the root CA verification is not performed.
288+
289+
```csharp
290+
using var httpHandler = new YetAnotherHttpHandler()
291+
{
292+
OnVerifyServerCertificate = (serverName, certificate, now) =>
293+
{
294+
var cert = new X509Certificate2(certificate);
295+
return serverName == "api.example.com" &&
296+
cert.Subject == "CN=api.example.com";
297+
}
298+
};
299+
```
283300

284301
## Development
285302
### Build & Tests

native/yaha_native/src/binding.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,17 @@ pub extern "C" fn yaha_client_config_skip_certificate_verification(
171171
let ctx = YahaNativeContextInternal::from_raw_context(ctx);
172172
ctx.skip_certificate_verification = Some(val);
173173
}
174+
175+
#[no_mangle]
176+
pub extern "C" fn yaha_client_config_set_server_certificate_verification_handler(
177+
ctx: *mut YahaNativeContext,
178+
handler: Option<extern "C" fn(state: NonZeroIsize, server_name: *const u8, server_name_len: usize, certificate_der: *const u8, certificate_der_len: usize, now: u64) -> bool>,
179+
callback_state: NonZeroIsize
180+
) {
181+
let ctx = YahaNativeContextInternal::from_raw_context(ctx);
182+
ctx.server_certificate_verification_handler = handler.map(|x| (x, callback_state));
183+
}
184+
174185
#[no_mangle]
175186
pub extern "C" fn yaha_client_config_pool_idle_timeout(
176187
ctx: *mut YahaNativeContext,
@@ -327,7 +338,7 @@ pub extern "C" fn yaha_client_config_http2_initial_max_send_streams(
327338
#[no_mangle]
328339
pub extern "C" fn yaha_build_client(ctx: *mut YahaNativeContext) {
329340
let ctx = YahaNativeContextInternal::from_raw_context(ctx);
330-
ctx.build_client(ctx.skip_certificate_verification.unwrap_or_default());
341+
ctx.build_client();
331342
}
332343

333344
#[no_mangle]

native/yaha_native/src/context.rs

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ use hyper_tls::HttpsConnector;
2626
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
2727
use tokio_util::sync::CancellationToken;
2828

29-
use crate::primitives::{YahaHttpVersion, CompletionReason};
29+
use crate::{primitives::{CompletionReason, YahaHttpVersion}};
3030

3131
type OnStatusCodeAndHeadersReceive =
3232
extern "C" fn(req_seq: i32, state: NonZeroIsize, status_code: i32, version: YahaHttpVersion);
3333
type OnReceive = extern "C" fn(req_seq: i32, state: NonZeroIsize, length: usize, buf: *const u8);
3434
type OnComplete = extern "C" fn(req_seq: i32, state: NonZeroIsize, reason: CompletionReason, h2_error_code: u32);
35+
type OnServerCertificateVerificationHandler = extern "C" fn(callback_state: NonZeroIsize, server_name: *const u8, server_name_len: usize, certificate_der: *const u8, certificate_der_len: usize, now: u64) -> bool;
3536

3637
pub struct YahaNativeRuntimeContext;
3738
pub struct YahaNativeRuntimeContextInternal {
@@ -54,6 +55,7 @@ pub struct YahaNativeContextInternal<'a> {
5455
pub runtime: tokio::runtime::Handle,
5556
pub client_builder: Option<client::legacy::Builder>,
5657
pub skip_certificate_verification: Option<bool>,
58+
pub server_certificate_verification_handler: Option<(OnServerCertificateVerificationHandler, NonZeroIsize)>,
5759
pub root_certificates: Option<rustls::RootCertStore>,
5860
pub override_server_name: Option<String>,
5961
pub connect_timeout: Option<Duration>,
@@ -81,6 +83,7 @@ impl YahaNativeContextInternal<'_> {
8183
client: None,
8284
client_builder: Some(Client::builder(TokioExecutor::new())),
8385
skip_certificate_verification: None,
86+
server_certificate_verification_handler: None,
8487
root_certificates: None,
8588
override_server_name: None,
8689
connect_timeout: None,
@@ -92,24 +95,32 @@ impl YahaNativeContextInternal<'_> {
9295
}
9396
}
9497

95-
pub fn build_client(&mut self, skip_verify_certificates: bool) {
98+
pub fn build_client(&mut self) {
9699
let mut builder = self.client_builder.take().unwrap();
97-
let https = self.new_connector(skip_verify_certificates);
100+
let https = self.new_connector();
98101
self.client = Some(builder.timer(TokioTimer::new()).build(https));
99102
}
100103

101104
#[cfg(feature = "rustls")]
102-
fn new_connector(&mut self, skip_verify_certificates: bool) -> HttpsConnector<HttpConnector> {
105+
fn new_connector(&mut self) -> HttpsConnector<HttpConnector> {
103106
let tls_config_builder = rustls::ClientConfig::builder();
104107

105108
// Configure certificate root store.
106109
let tls_config: rustls::ClientConfig;
107-
if skip_verify_certificates {
110+
if let Some(server_certificate_verification_handler) = self.server_certificate_verification_handler {
111+
// Use custom certificate verification handler
108112
tls_config = tls_config_builder
109113
.dangerous()
110-
.with_custom_certificate_verifier(Arc::new(danger::NoCertificateVerification {}))
114+
.with_custom_certificate_verifier(Arc::new(danger::CustomCerficateVerification { handler: server_certificate_verification_handler }))
115+
.with_no_client_auth();
116+
} else if self.skip_certificate_verification.unwrap_or_default() {
117+
// Skip certificate verification
118+
tls_config = tls_config_builder
119+
.dangerous()
120+
.with_custom_certificate_verifier(Arc::new(danger::NoCertificateVerification{}))
111121
.with_no_client_auth();
112122
} else {
123+
// Configure to use built-in certification store and client authentication.
113124
let tls_config_builder_root: rustls::ConfigBuilder<
114125
rustls::ClientConfig,
115126
rustls::client::WantsClientCert,
@@ -165,21 +176,30 @@ impl YahaNativeContextInternal<'_> {
165176
}
166177

167178
#[cfg(feature = "native")]
168-
fn new_connector(&mut self, skip_verify_certificates: bool) -> HttpsConnector<HttpConnector> {
179+
fn new_connector(&mut self, server_certificate_verification_handler: Option<OnServerCertificateVerificationHandler>) -> HttpsConnector<HttpConnector> {
169180
let https = HttpsConnector::new();
170181
https
171182
}
172183
}
173184

174185
#[cfg(feature = "rustls")]
175186
mod danger {
187+
use std::num::NonZeroIsize;
188+
176189
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
177190
use rustls::{DigitallySignedStruct, Error, SignatureScheme};
178191
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
179192

193+
use super::OnServerCertificateVerificationHandler;
194+
180195
#[derive(Debug)]
181196
pub struct NoCertificateVerification {}
182197

198+
#[derive(Debug)]
199+
pub struct CustomCerficateVerification {
200+
pub handler: (OnServerCertificateVerificationHandler, NonZeroIsize)
201+
}
202+
183203
const ALL_SCHEMES: [SignatureScheme; 12] = [
184204
SignatureScheme::RSA_PKCS1_SHA1,
185205
SignatureScheme::ECDSA_SHA1_Legacy,
@@ -194,6 +214,49 @@ mod danger {
194214
SignatureScheme::ED25519,
195215
SignatureScheme::ED448];
196216

217+
impl rustls::client::danger::ServerCertVerifier for CustomCerficateVerification {
218+
fn verify_server_cert(
219+
&self,
220+
end_entity: &CertificateDer<'_>,
221+
_intermediates: &[CertificateDer<'_>],
222+
server_name: &ServerName<'_>,
223+
_ocsp_response: &[u8],
224+
now: UnixTime,
225+
) -> Result<ServerCertVerified, Error> {
226+
let server_name = server_name.to_str();
227+
let server_name = server_name.as_bytes();
228+
let cetificate_der = end_entity.as_ref();
229+
230+
if (self.handler.0)(self.handler.1, server_name.as_ptr(), server_name.len(), cetificate_der.as_ptr(), cetificate_der.len(), now.as_secs()) {
231+
Ok(ServerCertVerified::assertion())
232+
} else {
233+
Err(Error::InvalidCertificate(rustls::CertificateError::ApplicationVerificationFailure))
234+
}
235+
}
236+
237+
fn verify_tls12_signature(
238+
&self,
239+
_message: &[u8],
240+
_cert: &CertificateDer<'_>,
241+
_dss: &DigitallySignedStruct,
242+
) -> Result<HandshakeSignatureValid, Error> {
243+
Ok(HandshakeSignatureValid::assertion())
244+
}
245+
246+
fn verify_tls13_signature(
247+
&self,
248+
_message: &[u8],
249+
_cert: &CertificateDer<'_>,
250+
_dss: &DigitallySignedStruct,
251+
) -> Result<HandshakeSignatureValid, Error> {
252+
Ok(HandshakeSignatureValid::assertion())
253+
}
254+
255+
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
256+
Vec::from(ALL_SCHEMES)
257+
}
258+
}
259+
197260
impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
198261
fn verify_server_cert(
199262
&self,

src/YetAnotherHttpHandler/NativeHttpHandlerCore.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Buffers;
33
using System.Collections.Concurrent;
4+
using System.Diagnostics;
45
using System.Net;
56
using System.Text;
67
using System.IO.Pipelines;
@@ -25,13 +26,15 @@ internal class NativeHttpHandlerCore : IDisposable
2526

2627
//private unsafe YahaNativeContext* _ctx;
2728
private readonly YahaContextSafeHandle _handle;
29+
private GCHandle? _onVerifyServerCertificateHandle; // The handle must be released in Dispose if it is allocated.
2830
private bool _disposed = false;
2931

3032
// NOTE: We need to keep the callback delegates in advance.
3133
// The delegates are kept on the Rust side, so it will crash if they are garbage collected.
3234
private static readonly unsafe NativeMethods.yaha_init_context_on_status_code_and_headers_receive_delegate OnStatusCodeAndHeaderReceiveCallback = OnStatusCodeAndHeaderReceive;
3335
private static readonly unsafe NativeMethods.yaha_init_context_on_receive_delegate OnReceiveCallback = OnReceive;
3436
private static readonly unsafe NativeMethods.yaha_init_context_on_complete_delegate OnCompleteCallback = OnComplete;
37+
private static readonly unsafe NativeMethods.yaha_client_config_set_server_certificate_verification_handler_handler_delegate OnServerCertificateVerificationCallback = OnServerCertificateVerification;
3538

3639
public unsafe NativeHttpHandlerCore(NativeClientSettings settings)
3740
{
@@ -75,6 +78,16 @@ private unsafe void Initialize(YahaNativeContext* ctx, NativeClientSettings sett
7578
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"Option '{nameof(settings.SkipCertificateVerification)}' = {skipCertificateVerification}");
7679
NativeMethods.yaha_client_config_skip_certificate_verification(ctx, skipCertificateVerification);
7780
}
81+
if (settings.OnVerifyServerCertificate is { } onVerifyServerCertificate)
82+
{
83+
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"Option '{nameof(settings.OnVerifyServerCertificate)}' = {onVerifyServerCertificate}");
84+
85+
// NOTE: We need to keep the handle to call in the static callback method.
86+
// The handle must be released in Dispose if it is allocated.
87+
_onVerifyServerCertificateHandle = GCHandle.Alloc(onVerifyServerCertificate);
88+
89+
NativeMethods.yaha_client_config_set_server_certificate_verification_handler(ctx, OnServerCertificateVerificationCallback, GCHandle.ToIntPtr(_onVerifyServerCertificateHandle.Value));
90+
}
7891
if (settings.RootCertificates is { } rootCertificates)
7992
{
8093
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"Option '{nameof(settings.RootCertificates)}' = Length:{rootCertificates.Length}");
@@ -395,6 +408,33 @@ private static unsafe void OnStatusCodeAndHeaderReceive(int reqSeq, IntPtr state
395408
requestContext.Response.SetStatusCode(statusCode);
396409
}
397410

411+
[MonoPInvokeCallback(typeof(NativeMethods.yaha_client_config_set_server_certificate_verification_handler_handler_delegate))]
412+
private static unsafe bool OnServerCertificateVerification(IntPtr callbackState, byte* serverNamePtr, UIntPtr /*nuint*/ serverNameLength, byte* certificateDerPtr, UIntPtr /*nuint*/ certificateDerLength, ulong now)
413+
{
414+
var serverName = UnsafeUtilities.GetStringFromUtf8Bytes(new ReadOnlySpan<byte>(serverNamePtr, (int)serverNameLength));
415+
var certificateDer = new ReadOnlySpan<byte>(certificateDerPtr, (int)certificateDerLength);
416+
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Trace($"OnServerCertificateVerification: State=0x{callbackState:X}; ServerName={serverName}; CertificateDer.Length={certificateDer.Length}; Now={now}");
417+
418+
var onServerCertificateVerification = (ServerCertificateVerificationHandler)GCHandle.FromIntPtr(callbackState).Target;
419+
Debug.Assert(onServerCertificateVerification != null);
420+
if (onServerCertificateVerification == null)
421+
{
422+
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Warning($"OnServerVerification: The verification callback was called, but onServerCertificateVerification is null.");
423+
return false;
424+
}
425+
try
426+
{
427+
var success = onServerCertificateVerification(serverName, certificateDer, DateTimeOffset.FromUnixTimeSeconds((long)now));
428+
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Trace($"OnServerVerification: Success = {success}");
429+
return success;
430+
}
431+
catch (Exception e)
432+
{
433+
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Error($"OnServerVerification: The verification callback thrown an exception: {e.ToString()}");
434+
return false;
435+
}
436+
}
437+
398438
[MonoPInvokeCallback(typeof(NativeMethods.yaha_init_context_on_receive_delegate))]
399439
private static unsafe void OnReceive(int reqSeq, IntPtr state, UIntPtr length, byte* buf)
400440
{
@@ -496,6 +536,8 @@ private void Dispose(bool disposing)
496536

497537
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"Dispose {nameof(NativeHttpHandlerCore)}; disposing={disposing}");
498538

539+
_onVerifyServerCertificateHandle?.Free();
540+
499541
NativeRuntime.Instance.Release(); // We always need to release runtime.
500542

501543
if (disposing)

src/YetAnotherHttpHandler/NativeMethods.Uwp.g.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ internal static unsafe partial class NativeMethods
5959
[DllImport(__DllName, EntryPoint = "yaha_client_config_skip_certificate_verification", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
6060
public static extern void yaha_client_config_skip_certificate_verification(YahaNativeContext* ctx, [MarshalAs(UnmanagedType.U1)] bool val);
6161

62+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
63+
public delegate bool yaha_client_config_set_server_certificate_verification_handler_handler_delegate(nint state, byte* server_name, nuint server_name_len, byte* certificate_der, nuint certificate_der_len, ulong now);
64+
65+
[DllImport(__DllName, EntryPoint = "yaha_client_config_set_server_certificate_verification_handler", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
66+
public static extern void yaha_client_config_set_server_certificate_verification_handler(YahaNativeContext* ctx, yaha_client_config_set_server_certificate_verification_handler_handler_delegate handler, nint callback_state);
67+
6268
[DllImport(__DllName, EntryPoint = "yaha_client_config_pool_idle_timeout", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
6369
public static extern void yaha_client_config_pool_idle_timeout(YahaNativeContext* ctx, ulong val_milliseconds);
6470

src/YetAnotherHttpHandler/NativeMethods.g.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ internal static unsafe partial class NativeMethods
6464
[DllImport(__DllName, EntryPoint = "yaha_client_config_skip_certificate_verification", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
6565
public static extern void yaha_client_config_skip_certificate_verification(YahaNativeContext* ctx, [MarshalAs(UnmanagedType.U1)] bool val);
6666

67+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
68+
public delegate bool yaha_client_config_set_server_certificate_verification_handler_handler_delegate(nint state, byte* server_name, nuint server_name_len, byte* certificate_der, nuint certificate_der_len, ulong now);
69+
70+
[DllImport(__DllName, EntryPoint = "yaha_client_config_set_server_certificate_verification_handler", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
71+
public static extern void yaha_client_config_set_server_certificate_verification_handler(YahaNativeContext* ctx, yaha_client_config_set_server_certificate_verification_handler_handler_delegate handler, nint callback_state);
72+
6773
[DllImport(__DllName, EntryPoint = "yaha_client_config_pool_idle_timeout", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
6874
public static extern void yaha_client_config_pool_idle_timeout(YahaNativeContext* ctx, ulong val_milliseconds);
6975

0 commit comments

Comments
 (0)