diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs index 48019c3d86b8e4..2975df40fb3163 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs @@ -11,6 +11,7 @@ using System.IO; using System.Net; using System.Net.Security; +using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using System.Security.Authentication; @@ -217,6 +218,8 @@ internal static unsafe SafeSslContextHandle AllocateSslContext(SslAuthentication throw CreateSslException(SR.net_allocate_ssl_context_failed); } + Ssl.SslCtxSetCertVerifyCallback(sslCtx, &CertVerifyCallback); + Ssl.SslCtxSetProtocolOptions(sslCtx, protocols); if (sslAuthenticationOptions.EncryptionPolicy != EncryptionPolicy.RequireEncryption) @@ -388,7 +391,7 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth // Dispose() here will not close the handle. using SafeSslContextHandle sslCtxHandle = GetOrCreateSslContextHandle(sslAuthenticationOptions, cacheSslContext); - GCHandle alpnHandle = default; + GCHandle authOptionsHandle = default; try { sslHandle = SafeSslHandle.Create(sslCtxHandle, sslAuthenticationOptions.IsServer); @@ -418,6 +421,11 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth sslHandle.SslContextHandle = sslCtxHandle; } + authOptionsHandle = GCHandle.Alloc(sslAuthenticationOptions); + Interop.Ssl.SslSetData(sslHandle, GCHandle.ToIntPtr(authOptionsHandle)); + sslHandle.AuthOptionsHandle = authOptionsHandle; + authOptionsHandle = default; // the ownership is transferred to sslHandle, so we should not free it in the caller anymore. + if (!sslAuthenticationOptions.AllowRsaPssPadding || !sslAuthenticationOptions.AllowRsaPkcs1Padding) { ConfigureSignatureAlgorithms(sslHandle, sslAuthenticationOptions.AllowRsaPssPadding, sslAuthenticationOptions.AllowRsaPkcs1Padding); @@ -425,14 +433,7 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth if (sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0) { - if (sslAuthenticationOptions.IsServer) - { - Debug.Assert(Interop.Ssl.SslGetData(sslHandle) == IntPtr.Zero); - alpnHandle = GCHandle.Alloc(sslAuthenticationOptions.ApplicationProtocols); - Interop.Ssl.SslSetData(sslHandle, GCHandle.ToIntPtr(alpnHandle)); - sslHandle.AlpnHandle = alpnHandle; - } - else + if (!sslAuthenticationOptions.IsServer) { if (Interop.Ssl.SslSetAlpnProtos(sslHandle, sslAuthenticationOptions.ApplicationProtocols) != 0) { @@ -443,6 +444,9 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth if (sslAuthenticationOptions.IsClient) { + // Client side always verifies the server's certificate. + Ssl.SslSetVerifyPeer(sslHandle, failIfNoPeerCert: false); + if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !IPAddress.IsValid(sslAuthenticationOptions.TargetHost)) { // Similar to windows behavior, set SNI on openssl by default for client context, ignore errors. @@ -474,7 +478,14 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth { if (sslAuthenticationOptions.RemoteCertRequired) { - Ssl.SslSetVerifyPeer(sslHandle); + // When no user callback is registered, also set + // SSL_VERIFY_FAIL_IF_NO_PEER_CERT so that OpenSSL sends the + // appropriate TLS alert when the client doesn't provide a + // certificate. When a callback IS registered, the application + // may choose to accept connections without a client certificate, + // so we only set SSL_VERIFY_PEER and let managed code handle it. + bool failIfNoPeerCert = sslAuthenticationOptions.CertValidationDelegate is null; + Ssl.SslSetVerifyPeer(sslHandle, failIfNoPeerCert); } if (sslAuthenticationOptions.CertificateContext != null) @@ -514,9 +525,9 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth } catch { - if (alpnHandle.IsAllocated) + if (authOptionsHandle.IsAllocated) { - alpnHandle.Free(); + authOptionsHandle.Free(); } throw; @@ -694,7 +705,12 @@ internal static SecurityStatusPalErrorCode DoSslHandshake(SafeSslHandle context, return SecurityStatusPalErrorCode.CredentialsNeeded; } - if ((retVal != -1) || (errorCode != Ssl.SslErrorCode.SSL_ERROR_WANT_READ)) + if (errorCode == Ssl.SslErrorCode.SSL_ERROR_SSL && context.CertificateValidationException is Exception ex) + { + handshakeException = ex; + context.CertificateValidationException = null; + } + else if ((retVal != -1) || (errorCode != Ssl.SslErrorCode.SSL_ERROR_WANT_READ)) { Exception? innerError = GetSslError(retVal, errorCode); @@ -731,7 +747,7 @@ internal static SecurityStatusPalErrorCode DoSslHandshake(SafeSslHandle context, if (handshakeException != null) { - throw handshakeException; + ExceptionDispatchInfo.Throw(handshakeException); } // in case of TLS 1.3 post-handshake authentication, SslDoHandhaske @@ -858,18 +874,110 @@ private static void QueryUniqueChannelBinding(SafeSslHandle context, SafeChannel bindingHandle.SetCertHashLength(certHashLength); } -#pragma warning disable IDE0060 [UnmanagedCallersOnly] - private static int VerifyClientCertificate(int preverify_ok, IntPtr x509_ctx_ptr) + internal static Interop.Crypto.X509VerifyStatusCodeUniversal CertVerifyCallback(IntPtr ssl, IntPtr store) { - // Full validation is handled after the handshake in VerifyCertificateProperties and the - // user callback. It's also up to those handlers to decide if a null certificate - // is appropriate. So just return success to tell OpenSSL that the cert is acceptable, - // we'll process it after the handshake finishes. - const int OpenSslSuccess = 1; - return OpenSslSuccess; + IntPtr data = Ssl.SslGetData(ssl); + Debug.Assert(data != IntPtr.Zero, "Expected non-null data pointer from SslGetData"); + GCHandle gch = GCHandle.FromIntPtr(data); + SslAuthenticationOptions options = (SslAuthenticationOptions)gch.Target!; + + using SafeX509StoreCtxHandle storeHandle = new(store, ownsHandle: false); + + // the chain will get properly disposed inside the VerifyRemoteCertificate call + X509Chain chain = new X509Chain(); + if (options.CertificateChainPolicy is not null) + { + chain.ChainPolicy = options.CertificateChainPolicy; + } + X509Certificate2? certificate = null; + SafeSslHandle sslHandle = (SafeSslHandle)options.SslStream!._securityContext!; + + using (SafeSharedX509StackHandle chainStack = Interop.Crypto.X509StoreCtxGetSharedUntrusted(storeHandle)) + { + if (!chainStack.IsInvalid) + { + int count = Interop.Crypto.GetX509StackFieldCount(chainStack); + + for (int i = 0; i < count; i++) + { + IntPtr certPtr = Interop.Crypto.GetX509StackField(chainStack, i); + + if (certPtr != IntPtr.Zero) + { + // X509Certificate2(IntPtr) calls X509_dup, so the reference is appropriately tracked. + X509Certificate2 chainCert = new X509Certificate2(certPtr); + + if (certificate is null) + { + // First cert in the stack is the leaf cert. + certificate = chainCert; + } + else + { + chain.ChainPolicy.ExtraStore.Add(chainCert); + } + } + } + } + } + + Debug.Assert(certificate != null, "OpenSSL only calls the callback if the certificate is present."); + + SslCertificateTrust? trust = options.CertificateContext?.Trust; + + ProtocolToken alertToken = default; + + try + { + if (options.SslStream!.VerifyRemoteCertificate(certificate, chain, trust, ref alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus)) + { + // success + return Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_OK; + } + + sslHandle.CertificateValidationException = SslStream.CreateCertificateValidationException(options, sslPolicyErrors, chainStatus); + + if (options.CertValidationDelegate != null) + { + // rejected by user validation callback + return Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_APPLICATION_VERIFICATION; + } + + TlsAlertMessage alert; + if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != SslPolicyErrors.None) + { + alert = SslStream.GetAlertMessageFromChain(chain); + } + else if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != SslPolicyErrors.None) + { + alert = TlsAlertMessage.BadCertificate; + } + else + { + alert = TlsAlertMessage.CertificateUnknown; + } + + // since we can't set the alert directly, we pick one of the error verify statuses + // which will result in the same alert being sent + + return alert switch + { + TlsAlertMessage.BadCertificate => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_CERT_REJECTED, + TlsAlertMessage.UnknownCA => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT, + TlsAlertMessage.CertificateRevoked => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_CERT_REVOKED, + TlsAlertMessage.CertificateExpired => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_CERT_HAS_EXPIRED, + TlsAlertMessage.UnsupportedCert => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_INVALID_PURPOSE, + _ => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_CERT_REJECTED, + }; + } + catch (Exception ex) + { + sslHandle.CertificateValidationException = ex; + + return Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_UNSPECIFIED; + } } -#pragma warning restore IDE0060 [UnmanagedCallersOnly] private static unsafe int AlpnServerSelectCallback(IntPtr ssl, byte** outp, byte* outlen, byte* inp, uint inlen, IntPtr arg) @@ -883,8 +991,8 @@ private static unsafe int AlpnServerSelectCallback(IntPtr ssl, byte** outp, byte return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL; } - GCHandle protocolHandle = GCHandle.FromIntPtr(sslData); - if (!(protocolHandle.Target is List protocolList)) + GCHandle authOptionsHandle = GCHandle.FromIntPtr(sslData); + if (!((authOptionsHandle.Target as SslAuthenticationOptions)?.ApplicationProtocols is List protocolList)) { return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL; } @@ -911,17 +1019,9 @@ private static unsafe int AlpnServerSelectCallback(IntPtr ssl, byte** outp, byte } catch { - // No common application protocol was negotiated, set the target on the alpnHandle to null. - // It is ok to clear the handle value here, this results in handshake failure, so the SslStream object is disposed. - protocolHandle.Target = null; - - return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL; } - // No common application protocol was negotiated, set the target on the alpnHandle to null. - // It is ok to clear the handle value here, this results in handshake failure, so the SslStream object is disposed. - protocolHandle.Target = null; - + // No common application protocol was negotiated return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL; } @@ -955,7 +1055,13 @@ private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session) Interop.Ssl.SslSessionSetData(session, cert); - IntPtr ptr = Ssl.SslGetData(ssl); + IntPtr ctx = Ssl.SslGetSslCtx(ssl); + IntPtr ptr = Ssl.SslCtxGetData(ctx); + // while SSL_CTX is kept alive by reference from SSL, the same is not true + // for the stored GCHandle pointing to SafeSslContextHandle. The managed + // SafeSslContextHandle may have been disposed and its GCHandle freed, in + // which case SslCtxGetData returns IntPtr.Zero and no GCHandle should be + // reconstructed. if (ptr != IntPtr.Zero) { GCHandle gch = GCHandle.FromIntPtr(ptr); @@ -963,12 +1069,14 @@ private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session) Debug.Assert(name != null); SafeSslContextHandle? ctxHandle = gch.Target as SafeSslContextHandle; - // There is no relation between SafeSslContextHandle and SafeSslHandle so the handle - // may be released while the ssl session is still active. - if (ctxHandle != null && ctxHandle.TryAddSession(name, session)) + + if (ctxHandle != null) { - // offered session was stored in our cache. - return 1; + if (ctxHandle.TryAddSession(name, session)) + { + // offered session was stored in our cache. + return 1; + } } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs index 2365e276609396..c48c4272ab44ac 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs @@ -185,7 +185,7 @@ internal static SafeSharedX509StackHandle SslGetPeerCertChain(SafeSslHandle ssl) internal static unsafe partial bool SslSetCiphers(SafeSslHandle ssl, byte* cipherList, byte* cipherSuites); [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSetVerifyPeer")] - internal static partial void SslSetVerifyPeer(SafeSslHandle ssl); + internal static partial void SslSetVerifyPeer(SafeSslHandle ssl, [MarshalAs(UnmanagedType.Bool)] bool failIfNoPeerCert); [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetData")] internal static partial IntPtr SslGetData(IntPtr ssl); @@ -235,6 +235,9 @@ internal static SafeSharedX509StackHandle SslGetPeerCertChain(SafeSslHandle ssl) [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSessionSetData")] internal static partial void SslSessionSetData(IntPtr session, IntPtr val); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetSslCtx")] + internal static partial IntPtr SslGetSslCtx(IntPtr ssl); + internal static class Capabilities { // needs separate type (separate static cctor) to be sure OpenSSL is initialized. @@ -394,11 +397,15 @@ internal sealed class SafeSslHandle : SafeDeleteSslContext private bool _isServer; private bool _handshakeCompleted; - public GCHandle AlpnHandle; + public GCHandle AuthOptionsHandle; // Reference to the parent SSL_CTX handle in the SSL_CTX is being cached. Only used for // refcount management. public SafeSslContextHandle? SslContextHandle; + // Storage for the exception that occurred during certificate validation callback so that + // we may rethrow it after returning to managed code. + public Exception? CertificateValidationException; + public bool IsServer { get { return _isServer; } @@ -492,10 +499,10 @@ protected override bool ReleaseHandle() SslContextHandle?.Dispose(); - if (AlpnHandle.IsAllocated) + if (AuthOptionsHandle.IsAllocated) { Interop.Ssl.SslSetData(handle, IntPtr.Zero); - AlpnHandle.Free(); + AuthOptionsHandle.Free(); } IntPtr h = handle; diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs index cfd2edab56c05e..2cf3db3fd7e4cb 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs @@ -63,6 +63,9 @@ internal static bool AddExtraChainCertificates(SafeSslContextHandle ctx, ReadOnl return true; } + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxSetCertVerifyCallback")] + internal static unsafe partial void SslCtxSetCertVerifyCallback(SafeSslContextHandle ctx, delegate* unmanaged callback); } } @@ -254,11 +257,6 @@ internal bool TrySetSession(SafeSslHandle sslHandle, string name) return false; } - // even if we don't have matching session, we can get new one and we need - // way how to link SSL back to `this`. - Debug.Assert(Interop.Ssl.SslGetData(sslHandle) == IntPtr.Zero); - Interop.Ssl.SslSetData(sslHandle, (IntPtr)_gch); - lock (_sslSessions) { if (_sslSessions.TryGetValue(name, out IntPtr session)) diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 7acc7195fa70ab..5cd51cd41c8920 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -315,7 +315,7 @@ public async Task SendMailAsync_CanBeCanceled_CancellationToken() // The server will introduce some fake latency so that the operation can be canceled before the request completes CancellationTokenSource cts = new CancellationTokenSource(); - + server.OnConnected += _ => cts.Cancel(); var message = new MailMessage("foo@internet.com", "bar@internet.com", "Foo", "Bar"); diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs index c3a78a098584f5..1382b3029c7fdb 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs @@ -225,6 +225,10 @@ internal void SetCertificateContextFromCert(X509Certificate2 certificate, bool? internal SslStream.JavaProxy? SslStreamProxy { get; set; } #endif +#if !TARGET_WINDOWS && !SYSNETSECURITY_NO_OPENSSL + internal SslStream? SslStream { get; set; } +#endif + public void Dispose() { if (OwnsCertificateContext && CertificateContext != null) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs index 317375cec0b1d4..8be1ff74e78bf9 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs @@ -16,7 +16,6 @@ private JavaProxy.RemoteCertificateValidationResult VerifyRemoteCertificate() { ProtocolToken alertToken = default; var isValid = VerifyRemoteCertificate( - _sslAuthenticationOptions.CertValidationDelegate, _sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out SslPolicyErrors sslPolicyErrors, diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs index cda4db5e7f7cad..90dce59217d16d 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs @@ -601,11 +601,35 @@ private bool CompleteHandshake(ref ProtocolToken alertToken, out SslPolicyErrors } #endif - if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertValidationDelegate, _sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out sslPolicyErrors, out chainStatus)) +#pragma warning disable CS0162 // unreachable code on some platforms + if (!SslStreamPal.CertValidationInCallback) { - _handshakeCompleted = false; - return false; + if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out sslPolicyErrors, out chainStatus)) + { + _handshakeCompleted = false; + return false; + } + } + else if (_remoteCertificate is null) + { + // CertVerifyCallback was not called during the handshake. This happens when: + // 1. The session was resumed — the cert is available from the SSL handle + // but OpenSSL skips the verify callback. + // 2. The peer didn't provide a certificate at all. + // In both cases, run VerifyRemoteCertificate to invoke the user's callback + // and perform full validation. + if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out sslPolicyErrors, out chainStatus)) + { + _handshakeCompleted = false; + return false; + } } + else + { + sslPolicyErrors = SslPolicyErrors.None; + chainStatus = X509ChainStatusFlags.NoError; + } +#pragma warning restore CS0162 // unreachable code on some platforms _handshakeCompleted = true; return true; @@ -616,21 +640,33 @@ private void CompleteHandshake(SslAuthenticationOptions sslAuthenticationOptions ProtocolToken alertToken = default; if (!CompleteHandshake(ref alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus)) { - if (sslAuthenticationOptions!.CertValidationDelegate != null) - { - // there may be some chain errors but the decision was made by custom callback. Details should be tracing if enabled. - SendAuthResetSignal(new ReadOnlySpan(alertToken.Payload), ExceptionDispatchInfo.Capture(new AuthenticationException(SR.net_ssl_io_cert_custom_validation, null))); - } - else if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors && chainStatus != X509ChainStatusFlags.NoError) - { - // We failed only because of chain and we have some insight. - SendAuthResetSignal(new ReadOnlySpan(alertToken.Payload), ExceptionDispatchInfo.Capture(new AuthenticationException(SR.Format(SR.net_ssl_io_cert_chain_validation, chainStatus), null))); - } - else - { - // Simple add sslPolicyErrors as crude info. - SendAuthResetSignal(new ReadOnlySpan(alertToken.Payload), ExceptionDispatchInfo.Capture(new AuthenticationException(SR.Format(SR.net_ssl_io_cert_validation, sslPolicyErrors), null))); - } + SendAuthResetSignal(new ReadOnlySpan(alertToken.Payload), ExceptionDispatchInfo.Capture(CreateCertificateValidationException(sslAuthenticationOptions, sslPolicyErrors, chainStatus))); + } + +#if !TARGET_WINDOWS && !SYSNETSECURITY_NO_OPENSSL + // Release the SslStream reference that was needed during the handshake + // for the CertVerifyCallback. Clearing it avoids retaining the SslStream + // through the SslAuthenticationOptions -> GCHandle -> SSL chain. + _sslAuthenticationOptions.SslStream = null; +#endif + } + + internal static Exception CreateCertificateValidationException(SslAuthenticationOptions options, SslPolicyErrors sslPolicyErrors, X509ChainStatusFlags chainStatus) + { + if (options.CertValidationDelegate != null) + { + // there may be some chain errors but the decision was made by custom callback. Details should be tracing if enabled. + return ExceptionDispatchInfo.SetCurrentStackTrace(new AuthenticationException(SR.net_ssl_io_cert_custom_validation, null)); + } + else if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors && chainStatus != X509ChainStatusFlags.NoError) + { + // We failed only because of chain and we have some insight. + return ExceptionDispatchInfo.SetCurrentStackTrace(new AuthenticationException(SR.Format(SR.net_ssl_io_cert_chain_validation, chainStatus), null)); + } + else + { + // Simple add sslPolicyErrors as crude info. + return ExceptionDispatchInfo.SetCurrentStackTrace(new AuthenticationException(SR.Format(SR.net_ssl_io_cert_validation, sslPolicyErrors), null)); } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index 582c0a3f22c313..5bde47cdaaa69c 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -85,7 +85,7 @@ internal static bool EnableServerAiaDownloads // on OSX, we have two implementations of SafeDeleteContext, so store a reference to the base class private SafeDeleteContext? _securityContext; #else - private SafeDeleteSslContext? _securityContext; + internal SafeDeleteSslContext? _securityContext; #endif private SslConnectionInfo _connectionInfo; @@ -1068,14 +1068,20 @@ internal SecurityStatusPal Decrypt(Span buffer, out int outputOffset, out --*/ //This method validates a remote certificate. - internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remoteCertValidationCallback, SslCertificateTrust? trust, ref ProtocolToken alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus) + internal bool VerifyRemoteCertificate(SslCertificateTrust? trust, ref ProtocolToken alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus) + { + X509Chain? chain = null; + X509Certificate2? certificate = CertificateValidationPal.GetRemoteCertificate(_securityContext, ref chain, _sslAuthenticationOptions.CertificateChainPolicy); + + return VerifyRemoteCertificate(certificate, chain, trust, ref alertToken, out sslPolicyErrors, out chainStatus); + } + + internal bool VerifyRemoteCertificate(X509Certificate2? certificate, X509Chain? chain, SslCertificateTrust? trust, ref ProtocolToken alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus) { sslPolicyErrors = SslPolicyErrors.None; chainStatus = X509ChainStatusFlags.NoError; - // We don't catch exceptions in this method, so it's safe for "accepted" be initialized with true. bool success = false; - X509Chain? chain = null; // We need to note the number of certs in ExtraStore that were // provided (by the user), we will add more from the received peer @@ -1083,10 +1089,10 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot // validation. // TODO: this forces allocation of X509Certificate2Collection int preexistingExtraCertsCount = _sslAuthenticationOptions.CertificateChainPolicy?.ExtraStore?.Count ?? 0; + RemoteCertificateValidationCallback? remoteCertValidationCallback = _sslAuthenticationOptions.CertValidationDelegate; try { - X509Certificate2? certificate = CertificateValidationPal.GetRemoteCertificate(_securityContext, ref chain, _sslAuthenticationOptions.CertificateChainPolicy); if (_remoteCertificate != null && certificate != null && certificate.RawDataMemory.Span.SequenceEqual(_remoteCertificate.RawDataMemory.Span)) @@ -1156,6 +1162,15 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot if (remoteCertValidationCallback != null) { + // Ensure connection info is populated before calling the user callback, + // which may access properties like SslProtocol or CipherAlgorithm. + // During inline cert validation the handshake hasn't completed yet, so + // _connectionInfo may not have been set by ProcessHandshakeSuccess. + if (_connectionInfo.Protocol == 0 && _securityContext is not null) + { + SslStreamPal.QueryContextConnectionInfo(_securityContext, ref _connectionInfo); + } + success = remoteCertValidationCallback(this, certificate, chain, sslPolicyErrors); } else @@ -1165,7 +1180,7 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNotAvailable; } - success = (sslPolicyErrors == SslPolicyErrors.None); + success = sslPolicyErrors == SslPolicyErrors.None; } if (NetEventSource.Log.IsEnabled()) @@ -1177,7 +1192,7 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot if (!success) { #pragma warning disable CS0162 // unreachable code detected (compile time const) - if (SslStreamPal.CanGenerateCustomAlerts) + if (SslStreamPal.CanGenerateCustomAlerts && !SslStreamPal.CertValidationInCallback) { CreateFatalHandshakeAlertToken(sslPolicyErrors, chain!, ref alertToken); } @@ -1288,7 +1303,7 @@ private ProtocolToken GenerateAlertToken() return GenerateToken(default, out _); } - private static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain) + internal static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain) { foreach (X509ChainStatus chainStatus in chain.ChainStatus) { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs index c64402cf89ea3b..41019a73cd6103 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs @@ -220,6 +220,10 @@ public SslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificat _sslAuthenticationOptions.SslStreamProxy = new SslStream.JavaProxy(sslStream: this); #endif +#if !TARGET_WINDOWS && !SYSNETSECURITY_NO_OPENSSL + _sslAuthenticationOptions.SslStream = this; +#endif + if (NetEventSource.Log.IsEnabled()) NetEventSource.Log.SslStreamCtor(this, innerStream); } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs index a57db95f7bd9e5..68df93d01217cd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs @@ -20,6 +20,7 @@ public static Exception GetException(SecurityStatusPal status) } internal const bool StartMutualAuthAsAnonymous = false; + internal const bool CertValidationInCallback = false; internal const bool CanEncryptEmptyMessage = false; // There is no API to generate custom alerts on Android, but the interop layer currently diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs index 98eb6cbfa6af81..7aff801dab8260 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs @@ -26,6 +26,7 @@ public static Exception GetException(SecurityStatusPal status) } internal const bool StartMutualAuthAsAnonymous = true; + internal const bool CertValidationInCallback = false; // SecureTransport is okay with a 0 byte input, but it produces a 0 byte output. // Since ST is not producing the framed empty message just call this false and avoid the diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs index 336c5467003386..77c06a6dac022b 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs @@ -18,6 +18,7 @@ public static Exception GetException(SecurityStatusPal status) } internal const bool StartMutualAuthAsAnonymous = false; + internal const bool CertValidationInCallback = true; internal const bool CanEncryptEmptyMessage = false; internal const bool CanGenerateCustomAlerts = false; @@ -215,19 +216,6 @@ private static ProtocolToken HandshakeInternal(ref SafeDeleteSslContext? context errorCode = Interop.OpenSsl.DoSslHandshake((SafeSslHandle)context, ReadOnlySpan.Empty, ref token); } - // When the handshake is done, and the context is server, check if the alpnHandle target was set to null during ALPN. - // If it was, then that indicates ALPN failed, send failure. - // We have this workaround, as openssl supports terminating handshake only from version 1.1.0, - // whereas ALPN is supported from version 1.0.2. - SafeSslHandle sslContext = (SafeSslHandle)context; - if (errorCode == SecurityStatusPalErrorCode.OK && sslAuthenticationOptions.IsServer - && sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0 - && sslContext.AlpnHandle.IsAllocated && sslContext.AlpnHandle.Target == null) - { - token.Status = new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, Interop.OpenSsl.CreateSslException(SR.net_alpn_failed)); - return token; - } - token.Status = new SecurityStatusPal(errorCode); } catch (Exception exc) when (exc is not ArgumentException) @@ -238,7 +226,10 @@ private static ProtocolToken HandshakeInternal(ref SafeDeleteSslContext? context return token; } - public static SecurityStatusPal ApplyAlertToken(SafeDeleteContext? securityContext, TlsAlertType alertType, TlsAlertMessage alertMessage) + public static SecurityStatusPal ApplyAlertToken( + SafeDeleteContext? securityContext, + TlsAlertType alertType, + TlsAlertMessage alertMessage) { // There doesn't seem to be an exposed API for writing an alert, // the API seems to assume that all alerts are generated internally by diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index 3bdb83666d4c61..20481456198bd6 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -44,6 +44,7 @@ public static Exception GetException(SecurityStatusPal status) } internal const bool StartMutualAuthAsAnonymous = true; + internal const bool CertValidationInCallback = false; internal const bool CanEncryptEmptyMessage = true; internal const bool CanGenerateCustomAlerts = true; diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs index 44861f71ee3082..ef59c802863a53 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs @@ -209,7 +209,7 @@ public async Task ServerAsyncAuthenticate_VerificationDelegate_Success() { bool validationCallbackCalled = false; var serverOptions = new SslServerAuthenticationOptions() { ServerCertificate = _serverCertificate, ClientCertificateRequired = true, }; - var clientOptions = new SslClientAuthenticationOptions() { TargetHost = _serverCertificate.GetNameInfo(X509NameType.SimpleName, false) }; + var clientOptions = new SslClientAuthenticationOptions() { TargetHost = _serverCertificate.GetNameInfo(X509NameType.SimpleName, false), AllowTlsResume = false }; clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; serverOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { @@ -241,7 +241,7 @@ public async Task ServerAsyncAuthenticate_ConstructorVerificationDelegate_Succes { bool validationCallbackCalled = false; var serverOptions = new SslServerAuthenticationOptions() { ServerCertificate = _serverCertificate, ClientCertificateRequired = true, }; - var clientOptions = new SslClientAuthenticationOptions() { TargetHost = _serverCertificate.GetNameInfo(X509NameType.SimpleName, false) }; + var clientOptions = new SslClientAuthenticationOptions() { TargetHost = _serverCertificate.GetNameInfo(X509NameType.SimpleName, false), AllowTlsResume = false }; clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); diff --git a/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs b/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs index 31346cc9715e52..ad47676ffb1636 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs @@ -24,6 +24,7 @@ private class FakeOptions public RemoteCertificateValidationCallback? CertValidationDelegate; public LocalCertificateSelectionCallback? CertSelectionDelegate; public X509RevocationMode CertificateRevocationCheckMode; + public SslStream? SslStream; public void UpdateOptions(SslServerAuthenticationOptions sslServerAuthenticationOptions) { @@ -40,6 +41,7 @@ internal void UpdateOptions(ServerOptionsSelectionCallback optionCallback, objec private FakeOptions _sslAuthenticationOptions = new FakeOptions(); private SslConnectionInfo _connectionInfo; + internal SafeDeleteSslContext? _securityContext; internal ChannelBinding? GetChannelBinding(ChannelBindingKind kind) => null; private bool _remoteCertificateExposed; private X509Certificate2? LocalClientCertificate; @@ -103,6 +105,23 @@ private ProtocolToken CreateShutdownToken() { return null; } + + internal bool VerifyRemoteCertificate(X509Certificate2? certificate, X509Chain? chain, SslCertificateTrust? trust, ref ProtocolToken alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus) + { + chainStatus = X509ChainStatusFlags.NoError; + sslPolicyErrors = SslPolicyErrors.None; + return true; + } + + internal static Exception CreateCertificateValidationException(SslAuthenticationOptions options, SslPolicyErrors sslPolicyErrors, X509ChainStatusFlags chainStatus) + { + return new Exception(); + } + + internal static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain) + { + return TlsAlertMessage.CloseNotify; + } } internal class ProtocolToken diff --git a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj index 42381dbab6c6b8..e18995da0a54ba 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj @@ -130,10 +130,20 @@ + + + + + + diff --git a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c index a6e647ecaf50e5..9923a4ffd80bd0 100644 --- a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c @@ -367,6 +367,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_SslCtxDestroy) DllImportEntry(CryptoNative_SslCtxGetData) DllImportEntry(CryptoNative_SslCtxSetAlpnSelectCb) + DllImportEntry(CryptoNative_SslCtxSetCertVerifyCallback) DllImportEntry(CryptoNative_SslCtxSetData) DllImportEntry(CryptoNative_SslCtxSetProtocolOptions) DllImportEntry(CryptoNative_SslCtxSetQuietShutdown) @@ -379,6 +380,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_SslGetClientCAList) DllImportEntry(CryptoNative_SslGetCurrentCipherId) DllImportEntry(CryptoNative_SslGetData) + DllImportEntry(CryptoNative_SslGetSslCtx) DllImportEntry(CryptoNative_SslGetError) DllImportEntry(CryptoNative_SslGetFinished) DllImportEntry(CryptoNative_SslGetPeerCertChain) diff --git a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h index 76c2c4bee82b54..e42d55b8335403 100644 --- a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h +++ b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h @@ -672,6 +672,7 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(SSL_CTX_set_security_level) \ REQUIRED_FUNCTION(SSL_CTX_set_session_id_context) \ REQUIRED_FUNCTION(SSL_CTX_set_verify) \ + REQUIRED_FUNCTION(SSL_CTX_set_cert_verify_callback) \ REQUIRED_FUNCTION(SSL_CTX_use_certificate) \ REQUIRED_FUNCTION(SSL_CTX_use_PrivateKey) \ REQUIRED_FUNCTION(SSL_do_handshake) \ @@ -682,6 +683,8 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(SSL_get_current_cipher) \ REQUIRED_FUNCTION(SSL_get_error) \ REQUIRED_FUNCTION(SSL_get_ex_data) \ + REQUIRED_FUNCTION(SSL_get_ex_data_X509_STORE_CTX_idx) \ + REQUIRED_FUNCTION(SSL_get_pending_cipher) \ REQUIRED_FUNCTION(SSL_get_finished) \ REQUIRED_FUNCTION(SSL_get_peer_cert_chain) \ REQUIRED_FUNCTION(SSL_get_peer_finished) \ @@ -693,6 +696,9 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(SSL_get_certificate) \ REQUIRED_FUNCTION(SSL_new) \ REQUIRED_FUNCTION(SSL_peek) \ + REQUIRED_FUNCTION(SSL_state_string_long) \ + REQUIRED_FUNCTION(SSL_alert_desc_string_long) \ + REQUIRED_FUNCTION(SSL_alert_type_string_long) \ REQUIRED_FUNCTION(SSL_read) \ REQUIRED_FUNCTION(SSL_renegotiate) \ REQUIRED_FUNCTION(SSL_renegotiate_pending) \ @@ -711,6 +717,8 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(SSL_set_ex_data) \ REQUIRED_FUNCTION(SSL_set_options) \ REQUIRED_FUNCTION(SSL_set_session) \ + REQUIRED_FUNCTION(SSL_set_verify_result) \ + REQUIRED_FUNCTION(SSL_get_verify_result) \ REQUIRED_FUNCTION(SSL_get_session) \ REQUIRED_FUNCTION(SSL_set_verify) \ REQUIRED_FUNCTION(SSL_shutdown) \ @@ -792,6 +800,7 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(X509_STORE_CTX_set_verify_cb) \ REQUIRED_FUNCTION(X509_STORE_CTX_set_ex_data) \ REQUIRED_FUNCTION(X509_STORE_CTX_get_ex_data) \ + REQUIRED_FUNCTION(X509_STORE_CTX_set_error) \ REQUIRED_FUNCTION(X509_STORE_free) \ REQUIRED_FUNCTION(X509_STORE_get0_param) \ REQUIRED_FUNCTION(X509_STORE_new) \ @@ -1235,6 +1244,7 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define SSL_CTX_set_security_level SSL_CTX_set_security_level_ptr #define SSL_CTX_set_session_id_context SSL_CTX_set_session_id_context_ptr #define SSL_CTX_set_verify SSL_CTX_set_verify_ptr +#define SSL_CTX_set_cert_verify_callback SSL_CTX_set_cert_verify_callback_ptr #define SSL_CTX_use_certificate SSL_CTX_use_certificate_ptr #define SSL_CTX_use_PrivateKey SSL_CTX_use_PrivateKey_ptr #define SSL_do_handshake SSL_do_handshake_ptr @@ -1246,9 +1256,11 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define SSL_get_current_cipher SSL_get_current_cipher_ptr #define SSL_get_error SSL_get_error_ptr #define SSL_get_ex_data SSL_get_ex_data_ptr +#define SSL_get_ex_data_X509_STORE_CTX_idx SSL_get_ex_data_X509_STORE_CTX_idx_ptr #define SSL_get_finished SSL_get_finished_ptr #define SSL_get_peer_cert_chain SSL_get_peer_cert_chain_ptr #define SSL_get_peer_finished SSL_get_peer_finished_ptr +#define SSL_get_pending_cipher SSL_get_pending_cipher_ptr #define SSL_get_servername SSL_get_servername_ptr #define SSL_get_SSL_CTX SSL_get_SSL_CTX_ptr #define SSL_get_version SSL_get_version_ptr @@ -1258,6 +1270,8 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define SSL_new SSL_new_ptr #define SSL_peek SSL_peek_ptr #define SSL_state_string_long SSL_state_string_long_ptr +#define SSL_alert_type_string_long SSL_alert_type_string_long_ptr +#define SSL_alert_desc_string_long SSL_alert_desc_string_long_ptr #define SSL_read SSL_read_ptr #define SSL_renegotiate SSL_renegotiate_ptr #define SSL_renegotiate_pending SSL_renegotiate_pending_ptr @@ -1276,6 +1290,8 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define SSL_set_ex_data SSL_set_ex_data_ptr #define SSL_set_options SSL_set_options_ptr #define SSL_set_session SSL_set_session_ptr +#define SSL_set_verify_result SSL_set_verify_result_ptr +#define SSL_get_verify_result SSL_get_verify_result_ptr #define SSL_get_session SSL_get_session_ptr #define SSL_set_verify SSL_set_verify_ptr #define SSL_shutdown SSL_shutdown_ptr @@ -1357,6 +1373,7 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define X509_STORE_CTX_set_verify_cb X509_STORE_CTX_set_verify_cb_ptr #define X509_STORE_CTX_set_ex_data X509_STORE_CTX_set_ex_data_ptr #define X509_STORE_CTX_get_ex_data X509_STORE_CTX_get_ex_data_ptr +#define X509_STORE_CTX_set_error X509_STORE_CTX_set_error_ptr #define X509_STORE_free X509_STORE_free_ptr #define X509_STORE_get0_param X509_STORE_get0_param_ptr #define X509_STORE_new X509_STORE_new_ptr diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_ssl.c b/src/native/libs/System.Security.Cryptography.Native/pal_ssl.c index c612eee05ee5ce..f096f30aebd322 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_ssl.c +++ b/src/native/libs/System.Security.Cryptography.Native/pal_ssl.c @@ -422,14 +422,6 @@ int32_t CryptoNative_SslRead(SSL* ssl, void* buf, int32_t num, int32_t* error) return result; } -static int verify_callback(int preverify_ok, X509_STORE_CTX* store) -{ - (void)preverify_ok; - (void)store; - // We don't care. Real verification happens in managed code. - return 1; -} - int32_t CryptoNative_SslRenegotiate(SSL* ssl, int32_t* error) { ERR_clear_error(); @@ -442,7 +434,7 @@ int32_t CryptoNative_SslRenegotiate(SSL* ssl, int32_t* error) if (SSL_version(ssl) == TLS1_3_VERSION) { // Post-handshake auth reqires SSL_VERIFY_PEER to be set - CryptoNative_SslSetVerifyPeer(ssl); + CryptoNative_SslSetVerifyPeer(ssl, 0); return SSL_verify_client_post_handshake(ssl); } #endif @@ -453,7 +445,7 @@ int32_t CryptoNative_SslRenegotiate(SSL* ssl, int32_t* error) int pending = SSL_renegotiate_pending(ssl); if (!pending) { - SSL_set_verify(ssl, SSL_VERIFY_PEER, verify_callback); + CryptoNative_SslSetVerifyPeer(ssl, 0); int ret = SSL_renegotiate(ssl); if(ret != 1) { @@ -593,10 +585,15 @@ X509NameStack* CryptoNative_SslGetClientCAList(SSL* ssl) return SSL_get_client_CA_list(ssl); } -void CryptoNative_SslSetVerifyPeer(SSL* ssl) +void CryptoNative_SslSetVerifyPeer(SSL* ssl, int32_t failIfNoPeerCert) { // void shim functions don't lead to exceptions, so skip the unconditional error clearing. - SSL_set_verify(ssl, SSL_VERIFY_PEER, verify_callback); + int mode = SSL_VERIFY_PEER; + if (failIfNoPeerCert) + { + mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + } + SSL_set_verify(ssl, mode, NULL); } int CryptoNative_SslCtxSetCaching(SSL_CTX* ctx, int mode, int cacheSize, int contextIdLength, uint8_t* contextId, SslCtxNewSessionCallback newSessionCb, SslCtxRemoveSessionCallback removeSessionCb) @@ -1039,6 +1036,14 @@ int32_t CryptoNative_SslGetCurrentCipherId(SSL* ssl, int32_t* cipherId) const SSL_CIPHER* cipher = SSL_get_current_cipher(ssl); if (!cipher) + { + // During the handshake (e.g. inside the cert verify callback), + // the current cipher may not be set yet (TLS 1.2 sets it at + // ChangeCipherSpec). Fall back to the pending cipher which is + // available as soon as ServerHello is processed. + cipher = SSL_get_pending_cipher(ssl); + } + if (!cipher) { *cipherId = -1; return 0; @@ -1308,3 +1313,32 @@ void CryptoNative_SslStapleOcsp(SSL* ssl, uint8_t* buf, int32_t len) OPENSSL_free(copy); } } + +static int CertVerifyCallback(X509_STORE_CTX* store, void* param) +{ + (void)param; + SslCtxCertValidationCallback callback = (SslCtxCertValidationCallback) param; + SSL* ssl = (SSL*) X509_STORE_CTX_get_ex_data(store, SSL_get_ex_data_X509_STORE_CTX_idx()); + + int verifyResult = callback(ssl, store); + + X509_STORE_CTX_set_error(store, verifyResult); + return verifyResult == X509_V_OK; +} + +void CryptoNative_SslCtxSetCertVerifyCallback(SSL_CTX* ctx, SslCtxCertValidationCallback callback) +{ + if (ctx != NULL) + { + SSL_CTX_set_cert_verify_callback(ctx, CertVerifyCallback, (void*)callback); + } +} + +/* +Shims SSL_get_SSL_CTX to retrieve the SSL_CTX from the SSL. +*/ +PALEXPORT SSL_CTX* CryptoNative_SslGetSslCtx(SSL* ssl) +{ + // No error queue impact. + return SSL_get_SSL_CTX(ssl); +} diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_ssl.h b/src/native/libs/System.Security.Cryptography.Native/pal_ssl.h index 66457e17710771..23f0874d22606a 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_ssl.h +++ b/src/native/libs/System.Security.Cryptography.Native/pal_ssl.h @@ -131,6 +131,9 @@ typedef void (*SslCtxRemoveSessionCallback)(SSL_CTX* ctx, SSL_SESSION* session); // the function pointer for keylog typedef void (*SslCtxSetKeylogCallback)(const SSL* ssl, const char *line); +// the function for remote certificate validation.. +typedef int32_t (*SslCtxCertValidationCallback)(SSL* ssl, X509_STORE_CTX* store); + /* Ensures that libssl is correctly initialized and ready to use. */ @@ -363,8 +366,6 @@ Returns 1 upon success, otherwise 0. */ PALEXPORT int32_t CryptoNative_SslUsePrivateKey(SSL* ssl, EVP_PKEY* pkey); - - /* Shims the SSL_CTX_use_certificate method. @@ -406,7 +407,7 @@ PALEXPORT X509NameStack* CryptoNative_SslGetClientCAList(SSL* ssl); /* Shims the SSL_set_verify method. */ -PALEXPORT void CryptoNative_SslSetVerifyPeer(SSL* ssl); +PALEXPORT void CryptoNative_SslSetVerifyPeer(SSL* ssl, int32_t failIfNoPeerCert); /* Shims SSL_set_ex_data to attach application context. @@ -428,6 +429,11 @@ Shims SSL_CTX_get_ex_data to retrieve application context. */ PALEXPORT void* CryptoNative_SslCtxGetData(SSL_CTX* ctx); +/* +Shims SSL_get_SSL_CTX to retrieve the SSL_CTX from the SSL. +*/ +PALEXPORT SSL_CTX* CryptoNative_SslGetSslCtx(SSL* ssl); + /* Sets the specified encryption policy on the SSL_CTX. @@ -557,3 +563,9 @@ PALEXPORT int32_t CryptoNative_OpenSslGetProtocolSupport(SslProtocols protocol); Staples an encoded OCSP response onto the TLS session */ PALEXPORT void CryptoNative_SslStapleOcsp(SSL* ssl, uint8_t* buf, int32_t len); + +/* +Sets the certificate verification callback for the SSL_CTX. +*/ +PALEXPORT void CryptoNative_SslCtxSetCertVerifyCallback(SSL_CTX* ctx, SslCtxCertValidationCallback callback); +