-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Support certificate validation TLS alerts for Linux #115996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b6fb762
b84be9d
6450c19
e4e866a
d5568d0
8960d45
7cb0edd
219710d
7a4d674
1087f63
4013d8e
e9fa3c8
ac88dca
57d5d1b
a28c862
73ecb79
0e60f87
e9399fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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,21 +421,19 @@ 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); | ||||||||
| } | ||||||||
|
|
||||||||
| 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, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the end result of my two comments (that I'm leaving in the same comment-posting) are "leave the code as-is", but I'll go ahead and say both of them: This un-mapping looks confusing"Depth-0 self-signed cert is just one of the statuses that could have caused UnknownCA...", after which I re-read the comment above and it clicked that this is "OpenSSL has a switch of status code to TLS alert, we just need to land in the right block" Why call GetAlertMessageFromChain at all?X509Chain turned an X509VerifyStatusCode into an X509ChainStatus, which GetAlertMessageFromChain turns into a TlsAlertMessage which this is turning back into an X509VerifyStatusCode... so why not just avoid all of the intermediate status? Well, X509Chain won't give you the original VerifyStatusCode, so you're already taking an ambiguous code, like NotTimeValid, and guessing that the more common reason is "expired" vs "not yet valid" (math could, of course, be done). And while it's true that GetAlertMessageFromChain is doing bit-testing when chain.ChainStatus does only one set bit per element ("So why is the enum [Flags]?!?!?!"), it does feel brittle to do something akin to |
||||||||
| TlsAlertMessage.CertificateRevoked => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_CERT_REVOKED, | ||||||||
| TlsAlertMessage.CertificateExpired => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_CERT_HAS_EXPIRED, | ||||||||
rzikm marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| TlsAlertMessage.UnsupportedCert => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_INVALID_PURPOSE, | ||||||||
|
||||||||
| TlsAlertMessage.UnsupportedCert => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_INVALID_PURPOSE, | |
| TlsAlertMessage.UnsupportedCert => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_INVALID_PURPOSE, | |
| TlsAlertMessage.CertificateUnknown => Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_ERR_CERT_REJECTED, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CertVerifyCallbackpopulateschain.ChainPolicy.ExtraStore, butSslStream.VerifyRemoteCertificatewill overwritechain.ChainPolicywhenoptions.CertificateChainPolicyis non-null, which discards the just-added certs and can break validation for callers supplying a custom chain policy. Build the chain using the sameX509ChainPolicyinstance thatVerifyRemoteCertificatewill apply (or copy the extracted certs into that policy) so the extracted chain is preserved.