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 10e475e78acfe5..a7fa684d3ceda1 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,138 +391,129 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth // Dispose() here will not close the handle. using SafeSslContextHandle sslCtxHandle = GetOrCreateSslContextHandle(sslAuthenticationOptions, cacheSslContext); - GCHandle alpnHandle = default; - try + sslHandle = SafeSslHandle.Create(sslCtxHandle, sslAuthenticationOptions); + Debug.Assert(sslHandle != null, "Expected non-null return value from SafeSslHandle.Create"); + if (sslHandle.IsInvalid) { - sslHandle = SafeSslHandle.Create(sslCtxHandle, sslAuthenticationOptions.IsServer); - Debug.Assert(sslHandle != null, "Expected non-null return value from SafeSslHandle.Create"); - if (sslHandle.IsInvalid) - { - sslHandle.Dispose(); - throw CreateSslException(SR.net_allocate_ssl_context_failed); - } + sslHandle.Dispose(); + throw CreateSslException(SR.net_allocate_ssl_context_failed); + } - if (cacheSslContext) - { - // For non-cached SSL_CTX instances, we free the `sslCtxHandle` - // after creating the SSL instance and don't use it again. We don't - // access it afterwards and OpenSSL has internal refcount which - // keeps it alive until the last SSL using it is freed. - // - // For cached SSL_CTX instances, we want to keep an outstanding - // up-ref to indicate that it is in use and does not get - // evicted from the cache. - // - // This call should always succeed because we already - // increased the rent count when getting the context from - // the cache. - bool success = sslCtxHandle.TryAddRentCount(); - Debug.Assert(success); - sslHandle.SslContextHandle = sslCtxHandle; - } + if (cacheSslContext) + { + // For non-cached SSL_CTX instances, we free the `sslCtxHandle` + // after creating the SSL instance and don't use it again. We don't + // access it afterwards and OpenSSL has internal refcount which + // keeps it alive until the last SSL using it is freed. + // + // For cached SSL_CTX instances, we want to keep an outstanding + // up-ref to indicate that it is in use and does not get + // evicted from the cache. + // + // This call should always succeed because we already + // increased the rent count when getting the context from + // the cache. + bool success = sslCtxHandle.TryAddRentCount(); + Debug.Assert(success); + sslHandle.SslContextHandle = sslCtxHandle; + } - if (!sslAuthenticationOptions.AllowRsaPssPadding || !sslAuthenticationOptions.AllowRsaPkcs1Padding) - { - ConfigureSignatureAlgorithms(sslHandle, sslAuthenticationOptions.AllowRsaPssPadding, sslAuthenticationOptions.AllowRsaPkcs1Padding); - } + if (!sslAuthenticationOptions.AllowRsaPssPadding || !sslAuthenticationOptions.AllowRsaPkcs1Padding) + { + ConfigureSignatureAlgorithms(sslHandle, sslAuthenticationOptions.AllowRsaPssPadding, sslAuthenticationOptions.AllowRsaPkcs1Padding); + } - if (sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0) + if (sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0) + { + if (sslAuthenticationOptions.IsClient) { - if (sslAuthenticationOptions.IsServer) + if (Interop.Ssl.SslSetAlpnProtos(sslHandle, sslAuthenticationOptions.ApplicationProtocols) != 0) { - 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 (Interop.Ssl.SslSetAlpnProtos(sslHandle, sslAuthenticationOptions.ApplicationProtocols) != 0) - { - throw CreateSslException(SR.net_alpn_config_failed); - } + throw CreateSslException(SR.net_alpn_config_failed); } } + } - if (sslAuthenticationOptions.IsClient) + 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)) { - if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !IPAddress.IsValid(sslAuthenticationOptions.TargetHost)) + // Similar to windows behavior, set SNI on openssl by default for client context, ignore errors. + if (!Ssl.SslSetTlsExtHostName(sslHandle, sslAuthenticationOptions.TargetHost)) { - // Similar to windows behavior, set SNI on openssl by default for client context, ignore errors. - if (!Ssl.SslSetTlsExtHostName(sslHandle, sslAuthenticationOptions.TargetHost)) - { - Crypto.ErrClearError(); - } - - if (cacheSslContext) - { - sslCtxHandle.TrySetSession(sslHandle, sslAuthenticationOptions.TargetHost); - } + Crypto.ErrClearError(); } - // relevant to TLS 1.3 only: if user supplied a client cert or cert callback, - // advertise that we are willing to send the certificate post-handshake. - if (sslAuthenticationOptions.CertificateContext != null || - sslAuthenticationOptions.ClientCertificates?.Count > 0 || - sslAuthenticationOptions.CertSelectionDelegate != null) + if (cacheSslContext) { - Ssl.SslSetPostHandshakeAuth(sslHandle, 1); + sslCtxHandle.TrySetSession(sslHandle, sslAuthenticationOptions.TargetHost); } + } - // Set client cert callback, this will interrupt the handshake with SecurityStatusPalErrorCode.CredentialsNeeded - // if server actually requests a certificate. - Ssl.SslSetClientCertCallback(sslHandle, 1); + // relevant to TLS 1.3 only: if user supplied a client cert or cert callback, + // advertise that we are willing to send the certificate post-handshake. + if (sslAuthenticationOptions.CertificateContext != null || + sslAuthenticationOptions.ClientCertificates?.Count > 0 || + sslAuthenticationOptions.CertSelectionDelegate != null) + { + Ssl.SslSetPostHandshakeAuth(sslHandle, 1); } - else // sslAuthenticationOptions.IsServer + + // Set client cert callback, this will interrupt the handshake with SecurityStatusPalErrorCode.CredentialsNeeded + // if server actually requests a certificate. + Ssl.SslSetClientCertCallback(sslHandle, 1); + } + else // sslAuthenticationOptions.IsServer + { + if (sslAuthenticationOptions.RemoteCertRequired) { - 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) + if (sslAuthenticationOptions.CertificateContext != null) + { + if (sslAuthenticationOptions.CertificateContext.Trust?._sendTrustInHandshake == true) { - if (sslAuthenticationOptions.CertificateContext.Trust?._sendTrustInHandshake == true) - { - SslCertificateTrust trust = sslAuthenticationOptions.CertificateContext!.Trust!; - X509Certificate2Collection certList = (trust._trustList ?? trust._store!.Certificates); - - Debug.Assert(certList != null); - Span handles = certList.Count <= 256 ? - stackalloc IntPtr[256] : - new IntPtr[certList.Count]; + SslCertificateTrust trust = sslAuthenticationOptions.CertificateContext!.Trust!; + X509Certificate2Collection certList = (trust._trustList ?? trust._store!.Certificates); - for (int i = 0; i < certList.Count; i++) - { - handles[i] = certList[i].Handle; - } + Debug.Assert(certList != null); + const int StackAllocCertLimit = 32; + Span handles = certList.Count <= StackAllocCertLimit ? + stackalloc IntPtr[StackAllocCertLimit] : + new IntPtr[certList.Count]; - if (!Ssl.SslAddClientCAs(sslHandle, handles.Slice(0, certList.Count))) - { - // The method can fail only when the number of cert names exceeds the maximum capacity - // supported by STACK_OF(X509_NAME) structure, which should not happen under normal - // operation. - Debug.Fail("Failed to add issuer to trusted CA list."); - } + for (int i = 0; i < certList.Count; i++) + { + handles[i] = certList[i].Handle; } - byte[]? ocspResponse = sslAuthenticationOptions.CertificateContext.GetOcspResponseNoWaiting(); - - if (ocspResponse != null) + if (!Ssl.SslAddClientCAs(sslHandle, handles.Slice(0, certList.Count))) { - Ssl.SslStapleOcsp(sslHandle, ocspResponse); + // The method can fail only when the number of cert names exceeds the maximum capacity + // supported by STACK_OF(X509_NAME) structure, which should not happen under normal + // operation. + Debug.Fail("Failed to add issuer to trusted CA list."); } } - } - } - catch - { - if (alpnHandle.IsAllocated) - { - alpnHandle.Free(); - } - throw; + byte[]? ocspResponse = sslAuthenticationOptions.CertificateContext.GetOcspResponseNoWaiting(); + + if (ocspResponse != null) + { + Ssl.SslStapleOcsp(sslHandle, ocspResponse); + } + } } return sslHandle; @@ -694,7 +688,15 @@ 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) + { + // Clear the OpenSSL error queue since we are using our own + // stored exception instead of the OpenSSL error. + Crypto.ErrClearError(); + handshakeException = ex; + context.CertificateValidationException = null; + } + else if ((retVal != -1) || (errorCode != Ssl.SslErrorCode.SSL_ERROR_WANT_READ)) { Exception? innerError = GetSslError(retVal, errorCode); @@ -731,7 +733,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 +860,161 @@ 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 int CertVerifyCallback(IntPtr storeCtx, IntPtr arg) { - // 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; + SafeSslHandle? sslHandle = null; + + try + { + IntPtr ssl = Ssl.X509StoreCtxGetSslPtr(storeCtx); + IntPtr data = Ssl.SslGetData(ssl); + Debug.Assert(data != IntPtr.Zero, "Expected non-null data pointer from SslGetData"); + WeakGCHandle.FromIntPtr(data) + .TryGetTarget(out SslAuthenticationOptions? options); + Debug.Assert(options != null, "Expected to get SslAuthenticationOptions from GCHandle"); + + sslHandle = (SafeSslHandle)options!.SslStream!._securityContext!; + + // We need to note the number of certs in ExtraStore that were + // provided (by the user), we will add more from the received peer + // chain and we want to dispose only these after we perform the + // validation. + // TODO: this forces allocation of X509Certificate2Collection + int preexistingExtraCertsCount = options.CertificateChainPolicy?.ExtraStore?.Count ?? 0; + + (X509Certificate2 certificate, X509Chain chain) = GetPeerCertChainFromStoreCtx(sslHandle, storeCtx, options); + + try + { + ProtocolToken alertToken = default; + if (options.SslStream!.VerifyRemoteCertificate(certificate, chain, options.CertificateContext?.Trust, ref alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus)) + { + Ssl.X509StoreCtxSetError(storeCtx, (int)Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_OK); + return 1; + } + + sslHandle.CertificateValidationException = SslStream.CreateCertificateValidationException(options, sslPolicyErrors, chainStatus); + + Interop.Crypto.X509VerifyStatusCodeUniversal verifyError; + if (options.CertValidationDelegate != null) + { + verifyError = Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_APPLICATION_VERIFICATION; + } + else + { + TlsAlertMessage alert; + if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != SslPolicyErrors.None) + { + // the chain is disposed at this point, but the ChainStatus property is still available + 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 + verifyError = 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, + }; + } + + Ssl.X509StoreCtxSetError(storeCtx, (int)verifyError); + return 0; + } + finally + { + // Only cleanup certificates if no user callback was provided. + // When a callback is provided, users might add their own certificates to ExtraStore + // or keep references to certificates from ChainElements. + if (options.CertValidationDelegate == null) + { + // Dispose only the certificates that were added by GetRemoteCertificate + for (int i = preexistingExtraCertsCount; i < chain.ChainPolicy.ExtraStore.Count; i++) + { + chain.ChainPolicy.ExtraStore[i].Dispose(); + } + + int elementsCount = chain.ChainElements.Count; + for (int i = 0; i < elementsCount; i++) + { + chain.ChainElements[i].Certificate.Dispose(); + } + } + + chain.Dispose(); + } + } + catch (Exception ex) + { + if (sslHandle is not null) + { + sslHandle.CertificateValidationException = ex; + } + + Ssl.X509StoreCtxSetError(storeCtx, (int)Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_UNSPECIFIED); + return 0; + } + + static (X509Certificate2 certificate, X509Chain chain) GetPeerCertChainFromStoreCtx(SafeSslHandle sslHandle, IntPtr storeCtx, SslAuthenticationOptions options) + { + X509Certificate2? certificate = null; + X509Chain chain = new X509Chain(); + + using SafeX509StoreCtxHandle storeHandle = new(storeCtx, ownsHandle: false); + if (options.CertificateChainPolicy is not null) + { + chain.ChainPolicy = options.CertificateChainPolicy; + } + + 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; + Interop.Ssl.SslUpdateOcspStaple(sslHandle, certificate.Handle); + } + else + { + chain.ChainPolicy.ExtraStore.Add(chainCert); + } + } + } + } + } + + Debug.Assert(certificate != null, "Remote certificate should not be null here."); + + return (certificate, chain); + } } -#pragma warning restore IDE0060 + [UnmanagedCallersOnly] private static unsafe int AlpnServerSelectCallback(IntPtr ssl, byte** outp, byte* outlen, byte* inp, uint inlen, IntPtr arg) @@ -883,8 +1028,10 @@ 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)) + WeakGCHandle.FromIntPtr(sslData) + .TryGetTarget(out SslAuthenticationOptions? options); + Debug.Assert(options != null, "Expected to get SslAuthenticationOptions from GCHandle"); + if (!(options?.ApplicationProtocols is List protocolList)) { return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL; } @@ -911,17 +1058,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 +1094,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 +1108,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..af4eb0f78a8b01 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 @@ -134,6 +134,9 @@ internal static ushort[] GetDefaultSignatureAlgorithms() [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetPeerCertificate")] internal static partial IntPtr SslGetPeerCertificate(SafeSslHandle ssl); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslUpdateOcspStaple")] + internal static partial void SslUpdateOcspStaple(SafeSslHandle ssl, IntPtr cert); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetCertificate")] internal static partial IntPtr SslGetCertificate(SafeSslHandle ssl); @@ -185,7 +188,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 +238,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 +400,15 @@ internal sealed class SafeSslHandle : SafeDeleteSslContext private bool _isServer; private bool _handshakeCompleted; - public GCHandle AlpnHandle; + private WeakGCHandle _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; } @@ -425,7 +435,7 @@ internal void MarkHandshakeCompleted() _handshakeCompleted = true; } - public static SafeSslHandle Create(SafeSslContextHandle context, bool isServer) + public static SafeSslHandle Create(SafeSslContextHandle context, SslAuthenticationOptions options) { SafeBioHandle readBio = Interop.Crypto.CreateMemoryBio(); SafeBioHandle writeBio = Interop.Crypto.CreateMemoryBio(); @@ -437,7 +447,9 @@ public static SafeSslHandle Create(SafeSslContextHandle context, bool isServer) handle.Dispose(); // will make IsInvalid==true if it's not already return handle; } - handle._isServer = isServer; + handle._isServer = options.IsServer; + handle._authOptionsHandle = new WeakGCHandle(options); + Interop.Ssl.SslSetData(handle, WeakGCHandle.ToIntPtr(handle._authOptionsHandle)); // SslSetBio will transfer ownership of the BIO handles to the SSL context try @@ -456,7 +468,7 @@ public static SafeSslHandle Create(SafeSslContextHandle context, bool isServer) throw; } - if (isServer) + if (options.IsServer) { Interop.Ssl.SslSetAcceptState(handle); } @@ -492,10 +504,11 @@ protected override bool ReleaseHandle() SslContextHandle?.Dispose(); - if (AlpnHandle.IsAllocated) + if (_authOptionsHandle.IsAllocated) { Interop.Ssl.SslSetData(handle, IntPtr.Zero); - AlpnHandle.Free(); + _authOptionsHandle.Dispose(); + _authOptionsHandle = default; } 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..1579ec9c3e22c7 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,15 @@ 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); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X509StoreCtxGetSslPtr")] + internal static partial IntPtr X509StoreCtxGetSslPtr(IntPtr storeCtx); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X509StoreCtxSetError")] + internal static partial void X509StoreCtxSetError(IntPtr storeCtx, int error); } } @@ -254,11 +263,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/CipherSuitesPolicyPal.Linux.cs b/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Linux.cs index 72b56306a27e05..eb4832016c2f1d 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Linux.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Linux.cs @@ -34,7 +34,9 @@ internal CipherSuitesPolicyPal(IEnumerable allowedCipherSuites) throw OpenSsl.CreateSslException(SR.net_allocate_ssl_context_failed); } - using (SafeSslHandle ssl = SafeSslHandle.Create(innerContext, false)) + // Create a client SSL object (so that we don't need to worry about certificates) + // and use it to get the OpenSSL names for the cipher suites. + using (SafeSslHandle ssl = SafeSslHandle.Create(innerContext, new SslAuthenticationOptions() { IsServer = false })) { if (ssl.IsInvalid) { 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 10dfef558f0441..6aaa1932ea76e2 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 @@ -223,6 +223,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..beedabafa4fbc1 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,26 @@ 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))); + } + } + + 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 c13bf7aa85d4fa..51f673f2111edc 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 @@ -24,7 +24,7 @@ public partial class SslStream // 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; @@ -1007,15 +1007,8 @@ 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) { - 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 // chain and we want to dispose only these after we perform the @@ -1023,154 +1016,190 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot // TODO: this forces allocation of X509Certificate2Collection int preexistingExtraCertsCount = _sslAuthenticationOptions.CertificateChainPolicy?.ExtraStore?.Count ?? 0; + X509Chain? chain = null; + try { X509Certificate2? certificate = CertificateValidationPal.GetRemoteCertificate(_securityContext, ref chain, _sslAuthenticationOptions.CertificateChainPolicy); - if (_remoteCertificate != null && - certificate != null && - certificate.RawDataMemory.Span.SequenceEqual(_remoteCertificate.RawDataMemory.Span)) - { - // This is renegotiation or TLS 1.3 and the certificate did not change. - // There is no reason to process callback again as we already established trust. - certificate.Dispose(); - return true; - } - // don't assign to _remoteCertificate yet, this prevents weird exceptions if SslStream is disposed in parallel with X509Chain building + return VerifyRemoteCertificate(certificate, chain, trust, ref alertToken, out sslPolicyErrors, out chainStatus); + } + finally + { + // At least on Win2k server the chain is found to have dependencies on the original cert context. + // So it should be closed first. - if (certificate == null) - { - if (NetEventSource.Log.IsEnabled() && RemoteCertRequired) NetEventSource.Error(this, $"Remote certificate required, but no remote certificate received"); - sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable; - } - else + if (chain != null) { - chain ??= new X509Chain(); - - if (_sslAuthenticationOptions.CertificateChainPolicy != null) - { - chain.ChainPolicy = _sslAuthenticationOptions.CertificateChainPolicy; - } - else + // Only cleanup certificates if no user callback was provided. + // When a callback is provided, users might add their own certificates to ExtraStore + // or keep references to certificates from ChainElements. + if (_sslAuthenticationOptions.CertValidationDelegate == null) { - chain.ChainPolicy.RevocationMode = _sslAuthenticationOptions.CertificateRevocationCheckMode; - chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; - - if (_sslAuthenticationOptions.IsServer && !LocalAppContextSwitches.EnableServerAiaDownloads) + // Dispose only the certificates that were added by GetRemoteCertificate + for (int i = preexistingExtraCertsCount; i < chain.ChainPolicy.ExtraStore.Count; i++) { - chain.ChainPolicy.DisableCertificateDownloads = true; + chain.ChainPolicy.ExtraStore[i].Dispose(); } - if (trust != null) + int elementsCount = chain.ChainElements.Count; + for (int i = 0; i < elementsCount; i++) { - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - if (trust._store != null) - { - chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates); - } - if (trust._trustList != null) - { - chain.ChainPolicy.CustomTrustStore.AddRange(trust._trustList); - } + chain.ChainElements[i].Certificate.Dispose(); } } - // set ApplicationPolicy unless already provided. - if (chain.ChainPolicy.ApplicationPolicy.Count == 0) - { - // Authenticate the remote party: (e.g. when operating in server mode, authenticate the client). - chain.ChainPolicy.ApplicationPolicy.Add(_sslAuthenticationOptions.IsServer ? s_clientAuthOid : s_serverAuthOid); - } - - sslPolicyErrors |= CertificateValidationPal.VerifyCertificateProperties( - _securityContext!, - chain, - certificate, - _sslAuthenticationOptions.CheckCertName, - _sslAuthenticationOptions.IsServer, - TargetHostNameHelper.NormalizeHostName(_sslAuthenticationOptions.TargetHost)); + chain.Dispose(); } + } + } - _remoteCertificate = certificate; + internal bool VerifyRemoteCertificate( + X509Certificate2? certificate, + X509Chain? chain, + SslCertificateTrust? trust, + ref ProtocolToken alertToken, + out SslPolicyErrors sslPolicyErrors, + out X509ChainStatusFlags chainStatus) + { + sslPolicyErrors = SslPolicyErrors.None; + chainStatus = X509ChainStatusFlags.NoError; - if (remoteCertValidationCallback != null) + bool success = false; + + RemoteCertificateValidationCallback? remoteCertValidationCallback = _sslAuthenticationOptions.CertValidationDelegate; + + if (_remoteCertificate != null && + certificate != null && + certificate.RawDataMemory.Span.SequenceEqual(_remoteCertificate.RawDataMemory.Span)) + { + // This is renegotiation or TLS 1.3 post-handshake auth and the (remote) certificate did not change. + // Revalidating the same certificate MAY fail for a couple of reasons (expiration, revocation, + // change in system trust, ...), but we have already established trust on this particular + // connection to even get this far. + certificate.Dispose(); + return true; + } + + // don't assign to _remoteCertificate yet, this prevents weird exceptions if SslStream is disposed in parallel with X509Chain building + + if (certificate == null) + { + if (NetEventSource.Log.IsEnabled() && RemoteCertRequired) { - success = remoteCertValidationCallback(this, certificate, chain, sslPolicyErrors); + NetEventSource.Error(this, $"Remote certificate required, but no remote certificate received"); + } + sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable; + } + else + { + chain ??= new X509Chain(); + + if (_sslAuthenticationOptions.CertificateChainPolicy != null) + { + chain.ChainPolicy = _sslAuthenticationOptions.CertificateChainPolicy; } else { - if (!RemoteCertRequired) + chain.ChainPolicy.RevocationMode = _sslAuthenticationOptions.CertificateRevocationCheckMode; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + + if (_sslAuthenticationOptions.IsServer && !LocalAppContextSwitches.EnableServerAiaDownloads) { - sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNotAvailable; + chain.ChainPolicy.DisableCertificateDownloads = true; } - success = (sslPolicyErrors == SslPolicyErrors.None); + if (trust != null) + { + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + if (trust._store != null) + { + chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates); + } + if (trust._trustList != null) + { + chain.ChainPolicy.CustomTrustStore.AddRange(trust._trustList); + } + } } - if (NetEventSource.Log.IsEnabled()) + // set ApplicationPolicy unless already provided. + if (chain.ChainPolicy.ApplicationPolicy.Count == 0) { - LogCertificateValidation(remoteCertValidationCallback, sslPolicyErrors, success, chain!); - NetEventSource.Info(this, $"Cert validation, remote cert = {_remoteCertificate}"); + // Authenticate the remote party: (e.g. when operating in server mode, authenticate the client). + chain.ChainPolicy.ApplicationPolicy.Add(_sslAuthenticationOptions.IsServer ? s_clientAuthOid : s_serverAuthOid); } - if (!success) + sslPolicyErrors |= CertificateValidationPal.VerifyCertificateProperties( + _securityContext!, + chain, + certificate, + _sslAuthenticationOptions.CheckCertName, + _sslAuthenticationOptions.IsServer, + TargetHostNameHelper.NormalizeHostName(_sslAuthenticationOptions.TargetHost)); + } + + _remoteCertificate = certificate; + + 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) { -#pragma warning disable CS0162 // unreachable code detected (compile time const) - if (SslStreamPal.CanGenerateCustomAlerts) - { - CreateFatalHandshakeAlertToken(sslPolicyErrors, chain!, ref alertToken); - } -#pragma warning restore CS0162 // unreachable code detected (compile time const) + SslStreamPal.QueryContextConnectionInfo(_securityContext, ref _connectionInfo); + } - if (chain != null) - { - foreach (X509ChainStatus status in chain.ChainStatus) - { - chainStatus |= status.Status; - } - } + success = remoteCertValidationCallback(this, certificate, chain, sslPolicyErrors); + } + else + { + if (!RemoteCertRequired) + { + sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNotAvailable; } + + success = sslPolicyErrors == SslPolicyErrors.None; } - finally + + if (NetEventSource.Log.IsEnabled()) { - // At least on Win2k server the chain is found to have dependencies on the original cert context. - // So it should be closed first. + LogCertificateValidation(remoteCertValidationCallback, sslPolicyErrors, success, chain); + NetEventSource.Info(this, $"Cert validation, remote cert = {_remoteCertificate}"); + } + + if (!success) + { +#pragma warning disable CS0162 // unreachable code detected (compile time const) + if (SslStreamPal.CanGenerateCustomAlerts && !SslStreamPal.CertValidationInCallback) + { + CreateFatalHandshakeAlertToken(sslPolicyErrors, chain!, ref alertToken); + } +#pragma warning restore CS0162 // unreachable code detected (compile time const) if (chain != null) { - // Only cleanup certificates if no user callback was provided. - // When a callback is provided, users might add their own certificates to ExtraStore - // or keep references to certificates from ChainElements. - if (remoteCertValidationCallback == null) + foreach (X509ChainStatus status in chain.ChainStatus) { - // Dispose only the certificates that were added by GetRemoteCertificate - for (int i = preexistingExtraCertsCount; i < chain.ChainPolicy.ExtraStore.Count; i++) - { - chain.ChainPolicy.ExtraStore[i].Dispose(); - } - - int elementsCount = chain.ChainElements.Count; - for (int i = 0; i < elementsCount; i++) - { - chain.ChainElements[i].Certificate.Dispose(); - } + chainStatus |= status.Status; } - - chain.Dispose(); } } return success; } - private void CreateFatalHandshakeAlertToken(SslPolicyErrors sslPolicyErrors, X509Chain chain, ref ProtocolToken alertToken) + private void CreateFatalHandshakeAlertToken(SslPolicyErrors sslPolicyErrors, X509Chain? chain, ref ProtocolToken alertToken) { TlsAlertMessage alertMessage; switch (sslPolicyErrors) { case SslPolicyErrors.RemoteCertificateChainErrors: - alertMessage = GetAlertMessageFromChain(chain); + Debug.Assert(chain != null); + alertMessage = GetAlertMessageFromChain(chain!); break; case SslPolicyErrors.RemoteCertificateNameMismatch: alertMessage = TlsAlertMessage.BadCertificate; @@ -1227,7 +1256,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) { @@ -1263,8 +1292,8 @@ private static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain) if ((chainStatus.Status & (X509ChainStatusFlags.CtlNotSignatureValid | X509ChainStatusFlags.InvalidExtension | - X509ChainStatusFlags.NotSignatureValid | X509ChainStatusFlags.InvalidPolicyConstraints) | - X509ChainStatusFlags.NoIssuanceChainPolicy | X509ChainStatusFlags.NotValidForUsage) != 0) + X509ChainStatusFlags.NotSignatureValid | X509ChainStatusFlags.InvalidPolicyConstraints | + X509ChainStatusFlags.NoIssuanceChainPolicy | X509ChainStatusFlags.NotValidForUsage)) != 0) { return TlsAlertMessage.BadCertificate; } @@ -1276,7 +1305,7 @@ private static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain) return TlsAlertMessage.BadCertificate; } - private void LogCertificateValidation(RemoteCertificateValidationCallback? remoteCertValidationCallback, SslPolicyErrors sslPolicyErrors, bool success, X509Chain chain) + private void LogCertificateValidation(RemoteCertificateValidationCallback? remoteCertValidationCallback, SslPolicyErrors sslPolicyErrors, bool success, X509Chain? chain) { if (!NetEventSource.Log.IsEnabled()) return; @@ -1296,8 +1325,9 @@ private void LogCertificateValidation(RemoteCertificateValidationCallback? remot if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0) { + Debug.Assert(chain != null); string chainStatusString = "ChainStatus: "; - foreach (X509ChainStatus chainStatus in chain.ChainStatus) + foreach (X509ChainStatus chainStatus in chain!.ChainStatus) { chainStatusString += "\t" + chainStatus.StatusInformation; } 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 aecaa2feca9e07..ceedfc68e8e8d0 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/FunctionalTests/SslStreamAlertsTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs index 8e134363e0b424..57f1794af59af9 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs @@ -7,6 +7,7 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -21,7 +22,7 @@ public class SslStreamAlertsTest private const uint SEC_E_CERT_UNKNOWN = 0x80090327; [Fact] - [ActiveIssue("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/dotnet/runtime/issues/18837", TestPlatforms.AnyUnix)] + [PlatformSpecific(TestPlatforms.Windows)] public async Task SslStream_StreamToStream_HandshakeAlert_Ok() { (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); @@ -53,7 +54,7 @@ public async Task SslStream_StreamToStream_HandshakeAlert_Ok() } [Fact] - [ActiveIssue("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/dotnet/runtime/issues/18837", TestPlatforms.AnyUnix)] + [ActiveIssue("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/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux))] public async Task SslStream_StreamToStream_ServerInitiatedCloseNotify_Ok() { (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); @@ -85,7 +86,7 @@ public async Task SslStream_StreamToStream_ServerInitiatedCloseNotify_Ok() [Theory] [InlineData(false)] [InlineData(true)] - [ActiveIssue("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/dotnet/runtime/issues/18837", TestPlatforms.AnyUnix)] + [ActiveIssue("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/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux))] public async Task SslStream_StreamToStream_ClientInitiatedCloseNotify_Ok(bool sendData) { (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); @@ -125,7 +126,7 @@ public async Task SslStream_StreamToStream_ClientInitiatedCloseNotify_Ok(bool se } [Fact] - [ActiveIssue("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/dotnet/runtime/issues/18837", TestPlatforms.AnyUnix)] + [ActiveIssue("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/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux))] public async Task SslStream_StreamToStream_DataAfterShutdown_Fail() { (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); @@ -208,6 +209,142 @@ await Task.WhenAll( } } + [Theory] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + [ActiveIssue("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/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux))] + public async Task SslStream_NoCallback_UntrustedCert_SendsAlert(SslProtocols protocol) + { + // When no RemoteCertificateValidationCallback is set and the server's cert + // is not trusted, the cert verify callback causes the TLS stack to send an + // alert so the client sees a proper error. + + X509Certificate2 cert = Configuration.Certificates.GetSelfSignedServerCertificate(); + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); + using (clientStream) + using (serverStream) + using (SslStream client = new SslStream(clientStream)) + using (SslStream server = new SslStream(serverStream)) + { + var serverOptions = new SslServerAuthenticationOptions + { + ServerCertificate = cert, + EnabledSslProtocols = protocol, + }; + + var clientOptions = new SslClientAuthenticationOptions + { + TargetHost = "localhost", + CertificateRevocationCheckMode = X509RevocationMode.NoCheck, + EnabledSslProtocols = protocol, + }; + + Task serverTask = server.AuthenticateAsServerAsync(serverOptions, CancellationToken.None); + Task clientTask = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None); + + // Client should fail because the validation failed locally, and it should send an alert. + await Assert.ThrowsAsync(() => clientTask).WaitAsync(TestConfiguration.PassingTestTimeout); + + // Server side should receive the alert and fail the handshake, the exact timing depends on the platform + // Windows: after the handshake, during data exchange + // Linux: during the handshake + Exception exception = await Assert.ThrowsAnyAsync(async () => + { + await serverTask; + byte[] buffer = new byte[1]; + await server.WriteAsync(buffer).AsTask().WaitAsync(TestConfiguration.PassingTestTimeout); + await server.ReadAsync(buffer).AsTask().WaitAsync(TestConfiguration.PassingTestTimeout); + }).WaitAsync(TestConfiguration.PassingTestTimeout); + + Assert.NotNull(exception.InnerException); + if (PlatformDetection.IsWindows) + { + Assert.IsType(exception); + Assert.IsType(exception.InnerException); + } + + if (PlatformDetection.IsLinux) + { + Assert.IsType(exception); + } + } + } + + [Theory] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + [ActiveIssue("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/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux))] + public async Task SslStream_NoCallback_UntrustedClientCert_ServerSendsAlert(SslProtocols protocol) + { + // When the server requires a client certificate and no + // RemoteCertificateValidationCallback is set, the server should send + // a TLS alert when the client's cert chain cannot be validated. + + X509Certificate2 cert = Configuration.Certificates.GetSelfSignedServerCertificate(); + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); + using (clientStream) + using (serverStream) + using (SslStream client = new SslStream(clientStream)) + using (SslStream server = new SslStream(serverStream)) + { + var serverOptions = new SslServerAuthenticationOptions + { + ServerCertificate = cert, + ClientCertificateRequired = true, + EnabledSslProtocols = protocol, + }; + + var clientOptions = new SslClientAuthenticationOptions + { + TargetHost = "localhost", + CertificateRevocationCheckMode = X509RevocationMode.NoCheck, + RemoteCertificateValidationCallback = delegate { return true; }, + ClientCertificates = new X509CertificateCollection { cert }, + EnabledSslProtocols = protocol, + }; + + Task serverTask = server.AuthenticateAsServerAsync(serverOptions, CancellationToken.None); + Task clientTask = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None); + + // Server should fail because the validation failed locally, and it should send an alert. + await Assert.ThrowsAsync(() => serverTask).WaitAsync(TestConfiguration.PassingTestTimeout); + + // Client side should receive the alert and fail the handshake, the exact timing depends on the platform + // Windows: after the handshake, during data exchange + // Linux: during the handshake, TLS 1.3 sends the alert after the handshake + Exception exception = await Assert.ThrowsAnyAsync(async () => + { + await clientTask; + byte[] buffer = new byte[1]; + await client.WriteAsync(buffer).AsTask().WaitAsync(TestConfiguration.PassingTestTimeout); + await client.ReadAsync(buffer).AsTask().WaitAsync(TestConfiguration.PassingTestTimeout); + }).WaitAsync(TestConfiguration.PassingTestTimeout); + + Assert.NotNull(exception.InnerException); + if (PlatformDetection.IsWindows) + { + Assert.IsType(exception); + Assert.IsType(exception.InnerException); + } + + if (PlatformDetection.IsLinux) + { + if (protocol == SslProtocols.Tls13) + { + // failure during app data (read) + Assert.IsType(exception); + } + else + { + // failure during handshake + Assert.IsType(exception); + } + + Assert.Contains("SslException", exception.InnerException.GetType().Name); + Assert.NotNull(exception.InnerException.InnerException); + Assert.Contains("alert", exception.InnerException.InnerException.Message); + } + } + } + private bool FailClientCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return false; 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 1fe85f24b858ee..e5a8d164c8758f 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 @@ -134,10 +134,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..389504b3265e3f 100644 --- a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c @@ -367,9 +367,12 @@ 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) + DllImportEntry(CryptoNative_X509StoreCtxGetSslPtr) + DllImportEntry(CryptoNative_X509StoreCtxSetError) DllImportEntry(CryptoNative_SslCtxUseCertificate) DllImportEntry(CryptoNative_SslCtxUsePrivateKey) DllImportEntry(CryptoNative_SslAddExtraChainCert) @@ -379,10 +382,12 @@ 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) DllImportEntry(CryptoNative_SslGetPeerCertificate) + DllImportEntry(CryptoNative_SslUpdateOcspStaple) DllImportEntry(CryptoNative_SslGetCertificate) DllImportEntry(CryptoNative_SslGetPeerFinished) DllImportEntry(CryptoNative_SslGetServerName) diff --git a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h index 5ec8bde682bd4f..5a7213f00e6c65 100644 --- a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h +++ b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h @@ -696,6 +696,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) \ @@ -706,6 +707,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) \ @@ -819,6 +822,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) \ @@ -1265,6 +1269,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 @@ -1276,9 +1281,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 @@ -1287,7 +1294,6 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define SSL_is_init_finished SSL_is_init_finished_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_read SSL_read_ptr #define SSL_renegotiate SSL_renegotiate_ptr #define SSL_renegotiate_pending SSL_renegotiate_pending_ptr @@ -1390,6 +1396,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 ed9205c24dd202..2661a176c53210 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_ssl.c +++ b/src/native/libs/System.Security.Cryptography.Native/pal_ssl.c @@ -427,14 +427,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(); @@ -447,7 +439,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 @@ -458,7 +450,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) { @@ -517,11 +509,22 @@ int32_t CryptoNative_IsSslStateOK(SSL* ssl) X509* CryptoNative_SslGetPeerCertificate(SSL* ssl) { + X509* cert = SSL_get1_peer_certificate(ssl); + CryptoNative_SslUpdateOcspStaple(ssl, cert); + + // No error queue impact. + return cert; +} + +void CryptoNative_SslUpdateOcspStaple(SSL* ssl, X509* cert) +{ + if (ssl == NULL || cert == NULL) + return; + const uint8_t* data = NULL; long len = SSL_get_tlsext_status_ocsp_resp(ssl, &data); - X509* cert = SSL_get1_peer_certificate(ssl); - if (len > 0 && cert != NULL && !X509_get_ex_data(cert, g_x509_ocsp_index)) + if (len > 0 && !X509_get_ex_data(cert, g_x509_ocsp_index)) { OCSP_RESPONSE* ocspResp = d2i_OCSP_RESPONSE(NULL, &data, len); @@ -534,9 +537,6 @@ X509* CryptoNative_SslGetPeerCertificate(SSL* ssl) X509_set_ex_data(cert, g_x509_ocsp_index, ocspResp); } } - - // No error queue impact. - return cert; } X509* CryptoNative_SslGetCertificate(SSL* ssl) @@ -598,10 +598,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) @@ -1044,6 +1049,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; @@ -1325,3 +1338,30 @@ void CryptoNative_SslStapleOcsp(SSL* ssl, uint8_t* buf, int32_t len) OPENSSL_free(copy); } } + +void CryptoNative_SslCtxSetCertVerifyCallback(SSL_CTX* ctx, SslCtxCertVerifyCallback callback) +{ + if (ctx != NULL) + { + SSL_CTX_set_cert_verify_callback(ctx, callback, NULL); + } +} + +SSL* CryptoNative_X509StoreCtxGetSslPtr(X509_STORE_CTX* storeCtx) +{ + return (SSL*)X509_STORE_CTX_get_ex_data(storeCtx, SSL_get_ex_data_X509_STORE_CTX_idx()); +} + +void CryptoNative_X509StoreCtxSetError(X509_STORE_CTX* storeCtx, int32_t error) +{ + X509_STORE_CTX_set_error(storeCtx, error); +} + +/* +Shims SSL_get_SSL_CTX to retrieve the SSL_CTX from the SSL. +*/ +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..7ea042b960cd70 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,10 @@ 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 pointer for remote certificate validation, matches the +// signature expected by SSL_CTX_set_cert_verify_callback directly. +typedef int (*SslCtxCertVerifyCallback)(X509_STORE_CTX* store, void* arg); + /* Ensures that libssl is correctly initialized and ready to use. */ @@ -335,6 +339,12 @@ Returns the certificate presented by the peer. */ PALEXPORT X509* CryptoNative_SslGetPeerCertificate(SSL* ssl); +/* +Attaches the OCSP staple response from the SSL session to the given X509 +certificate via ex_data, if one is available and not already set. +*/ +PALEXPORT void CryptoNative_SslUpdateOcspStaple(SSL* ssl, X509* cert); + /* Shims the SSL_get_certificate method. @@ -363,8 +373,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 +414,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 +436,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 +570,18 @@ 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, SslCtxCertVerifyCallback callback); + +/* +Retrieves the SSL object associated with an X509_STORE_CTX during verification. +*/ +PALEXPORT SSL* CryptoNative_X509StoreCtxGetSslPtr(X509_STORE_CTX* storeCtx); + +/* +Sets the error code on an X509_STORE_CTX. +*/ +PALEXPORT void CryptoNative_X509StoreCtxSetError(X509_STORE_CTX* storeCtx, int32_t error);