Skip to content

Commit

Permalink
fix LocalCertificateSelectionCallback on unix (dotnet#63200)
Browse files Browse the repository at this point in the history
* fix LocalCertificateSelectionCallback on unix

* fix linux

* use Tls12

* attempt to fix build

* fix macOS

* fix test on windows

* skip win7

* add issue reference for windows

* feedback from review

* fix space
  • Loading branch information
wfurt authored Jan 12, 2022
1 parent b25b96e commit 8fef95b
Show file tree
Hide file tree
Showing 15 changed files with 369 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ internal enum PAL_TlsHandshakeState
WouldBlock,
ServerAuthCompleted,
ClientAuthCompleted,
ClientCertRequested,
}

internal enum PAL_TlsIo
Expand Down Expand Up @@ -99,6 +100,12 @@ private static partial int AppleCryptoNative_SslSetBreakOnClientAuth(
int setBreak,
out int pOSStatus);

[GeneratedDllImport(Interop.Libraries.AppleCryptoNative)]
private static partial int AppleCryptoNative_SslSetBreakOnCertRequested(
SafeSslHandle sslHandle,
int setBreak,
out int pOSStatus);

[GeneratedDllImport(Interop.Libraries.AppleCryptoNative)]
private static partial int AppleCryptoNative_SslSetCertificate(
SafeSslHandle sslHandle,
Expand Down Expand Up @@ -266,6 +273,25 @@ internal static void SslBreakOnClientAuth(SafeSslHandle sslHandle, bool setBreak
throw new SslException();
}

internal static void SslBreakOnCertRequested(SafeSslHandle sslHandle, bool setBreak)
{
int osStatus;
int result = AppleCryptoNative_SslSetBreakOnCertRequested(sslHandle, setBreak ? 1 : 0, out osStatus);

if (result == 1)
{
return;
}

if (result == 0)
{
throw CreateExceptionForOSStatus(osStatus);
}

Debug.Fail($"AppleCryptoNative_SslSetBreakOnCertRequested returned {result}");
throw new SslException();
}

internal static void SslSetCertificate(SafeSslHandle sslHandle, IntPtr[] certChainPtrs)
{
using (SafeCreateHandle cfCertRefs = CoreFoundation.CFArrayCreate(certChainPtrs, (UIntPtr)certChainPtrs.Length))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,45 @@ internal static SafeSslContextHandle AllocateSslContext(SafeFreeSslCredentials c
return sslCtx;
}

internal static void UpdateClientCertiticate(SafeSslHandle ssl, SslAuthenticationOptions sslAuthenticationOptions)
{
// Disable certificate selection callback. We either got certificate or we will try to proceed without it.
Interop.Ssl.SslSetClientCertCallback(ssl, 0);

if (sslAuthenticationOptions.CertificateContext == null)
{
return;
}

var credential = new SafeFreeSslCredentials(sslAuthenticationOptions.CertificateContext, sslAuthenticationOptions.EnabledSslProtocols, sslAuthenticationOptions.EncryptionPolicy, sslAuthenticationOptions.IsServer);
SafeX509Handle? certHandle = credential.CertHandle;
SafeEvpPKeyHandle? certKeyHandle = credential.CertKeyHandle;

Debug.Assert(certHandle != null);
Debug.Assert(certKeyHandle != null);

int retVal = Ssl.SslUseCertificate(ssl, certHandle);
if (1 != retVal)
{
throw CreateSslException(SR.net_ssl_use_cert_failed);
}

retVal = Ssl.SslUsePrivateKey(ssl, certKeyHandle);
if (1 != retVal)
{
throw CreateSslException(SR.net_ssl_use_private_key_failed);
}

if (sslAuthenticationOptions.CertificateContext.IntermediateCertificates.Length > 0)
{
if (!Ssl.AddExtraChainCertificates(ssl, sslAuthenticationOptions.CertificateContext.IntermediateCertificates))
{
throw CreateSslException(SR.net_ssl_use_cert_failed);
}
}

}

// This essentially wraps SSL* SSL_new()
internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credential, SslAuthenticationOptions sslAuthenticationOptions)
{
Expand Down Expand Up @@ -287,6 +326,13 @@ internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credentia
{
Crypto.ErrClearError();
}

if (sslAuthenticationOptions.CertSelectionDelegate != null && sslAuthenticationOptions.CertificateContext == null)
{
// We don't have certificate but we have callback. We should wait for remote certificate and
// possible trusted issuer list.
Interop.Ssl.SslSetClientCertCallback(sslHandle, 1);
}
}

if (sslAuthenticationOptions.IsServer && sslAuthenticationOptions.RemoteCertRequired)
Expand Down Expand Up @@ -324,7 +370,7 @@ internal static SecurityStatusPal SslRenegotiate(SafeSslHandle sslContext, out b
return new SecurityStatusPal(SecurityStatusPalErrorCode.OK);
}

internal static bool DoSslHandshake(SafeSslHandle context, ReadOnlySpan<byte> input, out byte[]? sendBuf, out int sendCount)
internal static SecurityStatusPalErrorCode DoSslHandshake(SafeSslHandle context, ReadOnlySpan<byte> input, out byte[]? sendBuf, out int sendCount)
{
sendBuf = null;
sendCount = 0;
Expand All @@ -345,6 +391,11 @@ internal static bool DoSslHandshake(SafeSslHandle context, ReadOnlySpan<byte> in
Exception? innerError;
Ssl.SslErrorCode error = GetSslError(context, retVal, out innerError);

if (error == Ssl.SslErrorCode.SSL_ERROR_WANT_X509_LOOKUP)
{
return SecurityStatusPalErrorCode.CredentialsNeeded;
}

if ((retVal != -1) || (error != Ssl.SslErrorCode.SSL_ERROR_WANT_READ))
{
// Handshake failed, but even if the handshake does not need to read, there may be an Alert going out.
Expand Down Expand Up @@ -389,7 +440,8 @@ internal static bool DoSslHandshake(SafeSslHandle context, ReadOnlySpan<byte> in
{
context.MarkHandshakeCompleted();
}
return stateOk;

return stateOk ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.ContinueNeeded;
}

internal static int Encrypt(SafeSslHandle context, ReadOnlySpan<byte> input, ref byte[] output, out Ssl.SslErrorCode errorCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Win32.SafeHandles;

Expand Down Expand Up @@ -149,6 +150,15 @@ internal static partial class Ssl
[GeneratedDllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSetData")]
internal static partial int SslSetData(IntPtr ssl, IntPtr data);

[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslUseCertificate")]
internal static extern int SslUseCertificate(SafeSslHandle ssl, SafeX509Handle certPtr);

[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslUsePrivateKey")]
internal static extern int SslUsePrivateKey(SafeSslHandle ssl, SafeEvpPKeyHandle keyPtr);

[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSetClientCertCallback")]
internal static extern unsafe void SslSetClientCertCallback(SafeSslHandle ssl, int set);

[GeneratedDllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_Tls13Supported")]
private static partial int Tls13SupportedImpl();

Expand Down Expand Up @@ -192,6 +202,28 @@ internal static byte[] ConvertAlpnProtocolListToByteArray(List<SslApplicationPro
return buffer;
}

[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslAddExtraChainCert")]
internal static extern bool SslAddExtraChainCert(SafeSslHandle ssl, SafeX509Handle x509);

internal static bool AddExtraChainCertificates(SafeSslHandle ssl, X509Certificate2[] chain)
{
// send pre-computed list of intermediates.
for (int i = 0; i < chain.Length; i++)
{
SafeX509Handle dupCertHandle = Crypto.X509UpRef(chain[i].Handle);
Crypto.CheckValidOpenSslHandle(dupCertHandle);
if (!SslAddExtraChainCert(ssl, dupCertHandle))
{
Crypto.ErrClearError();
dupCertHandle.Dispose(); // we still own the safe handle; clean it up
return false;
}
dupCertHandle.SetHandleAsInvalid(); // ownership has been transferred to sslHandle; do not free via this safe handle
}

return true;
}

internal static string? GetOpenSslCipherSuiteName(SafeSslHandle ssl, TlsCipherSuite cipherSuite, out bool isTls12OrLower)
{
string? ret = Marshal.PtrToStringAnsi(GetOpenSslCipherSuiteName(ssl, (int)cipherSuite, out int isTls12OrLowerInt));
Expand Down Expand Up @@ -224,6 +256,7 @@ internal enum SslErrorCode
SSL_ERROR_SSL = 1,
SSL_ERROR_WANT_READ = 2,
SSL_ERROR_WANT_WRITE = 3,
SSL_ERROR_WANT_X509_LOOKUP = 4,
SSL_ERROR_SYSCALL = 5,
SSL_ERROR_ZERO_RETURN = 6,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ internal static string[] GetRequestCertificateAuthorities(SafeDeleteContext secu

using (SafeCFArrayHandle dnArray = Interop.AppleCrypto.SslCopyCADistinguishedNames(sslContext))
{
if (dnArray.IsInvalid)
{
return Array.Empty<string>();
}

long size = Interop.CoreFoundation.CFArrayGetCount(dnArray);

if (size == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ private static void SetProtocols(SafeSslHandle sslContext, SslProtocols protocol
Interop.AppleCrypto.SslSetMaxProtocolVersion(sslContext, maxProtocolId);
}

private static void SetCertificate(SafeSslHandle sslContext, SslStreamCertificateContext context)
internal static void SetCertificate(SafeSslHandle sslContext, SslStreamCertificateContext context)
{
Debug.Assert(sslContext != null, "sslContext != null");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ private static SecurityStatusPal HandshakeInternal(
Interop.AppleCrypto.SslSetTargetName(sslContext.SslContext, sslAuthenticationOptions.TargetHost);
}

if (sslAuthenticationOptions.CertificateContext == null && sslAuthenticationOptions.CertSelectionDelegate != null)
{
// certificate was not provided but there is user callback. We can break handshake if server asks for certificate
// and we can try to get it based on remote certificate and trusted issuers.
Interop.AppleCrypto.SslBreakOnCertRequested(sslContext.SslContext, true);
}

if (sslAuthenticationOptions.IsServer && sslAuthenticationOptions.RemoteCertRequired)
{
Interop.AppleCrypto.SslSetAcceptClientCert(sslContext.SslContext);
Expand All @@ -259,6 +266,35 @@ private static SecurityStatusPal HandshakeInternal(

SafeSslHandle sslHandle = sslContext!.SslContext;
SecurityStatusPal status = PerformHandshake(sslHandle);
if (status.ErrorCode == SecurityStatusPalErrorCode.CredentialsNeeded)
{
// we should not be here if CertSelectionDelegate is null but better check before dereferencing..
if (sslAuthenticationOptions.CertSelectionDelegate != null)
{
X509Certificate2? remoteCert = null;
try
{
string[] issuers = CertificateValidationPal.GetRequestCertificateAuthorities(context);
remoteCert = CertificateValidationPal.GetRemoteCertificate(context);
if (sslAuthenticationOptions.ClientCertificates == null)
{
sslAuthenticationOptions.ClientCertificates = new X509CertificateCollection();
}
X509Certificate2 clientCertificate = (X509Certificate2)sslAuthenticationOptions.CertSelectionDelegate(sslAuthenticationOptions.TargetHost!, sslAuthenticationOptions.ClientCertificates, remoteCert, issuers);
if (clientCertificate != null)
{
SafeDeleteSslContext.SetCertificate(sslContext.SslContext, SslStreamCertificateContext.Create(clientCertificate));
}
}
finally
{
remoteCert?.Dispose();
}
}

// We either got certificate or we can proceed without it. It is up to the server to decide if either is OK.
status = PerformHandshake(sslHandle);
}

outputBuffer = sslContext.ReadPendingWrites();
return status;
Expand Down Expand Up @@ -290,6 +326,8 @@ private static SecurityStatusPal PerformHandshake(SafeSslHandle sslHandle)
// So, call SslHandshake again to indicate to Secure Transport that we've
// accepted this handshake and it should go into the ready state.
break;
case PAL_TlsHandshakeState.ClientCertRequested:
return new SecurityStatusPal(SecurityStatusPalErrorCode.CredentialsNeeded);
default:
return new SecurityStatusPal(
SecurityStatusPalErrorCode.InternalError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ private static SecurityStatusPal MapNativeErrorCode(Interop.Ssl.SslErrorCode err
{
Interop.Ssl.SslErrorCode.SSL_ERROR_RENEGOTIATE => new SecurityStatusPal(SecurityStatusPalErrorCode.Renegotiate),
Interop.Ssl.SslErrorCode.SSL_ERROR_ZERO_RETURN => new SecurityStatusPal(SecurityStatusPalErrorCode.ContextExpired),
Interop.Ssl.SslErrorCode.SSL_ERROR_WANT_X509_LOOKUP => new SecurityStatusPal(SecurityStatusPalErrorCode.CredentialsNeeded),
Interop.Ssl.SslErrorCode.SSL_ERROR_NONE or
Interop.Ssl.SslErrorCode.SSL_ERROR_WANT_READ => new SecurityStatusPal(SecurityStatusPalErrorCode.OK),
_ => new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, new Interop.OpenSsl.SslException((int)errorCode))
Expand Down Expand Up @@ -158,20 +159,50 @@ private static SecurityStatusPal HandshakeInternal(SafeFreeCredentials credentia
context = new SafeDeleteSslContext((credential as SafeFreeSslCredentials)!, sslAuthenticationOptions);
}

bool done = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, inputBuffer, out output, out outputSize);
SecurityStatusPalErrorCode errorCode = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, inputBuffer, out output, out outputSize);

if (errorCode == SecurityStatusPalErrorCode.CredentialsNeeded)
{
if (sslAuthenticationOptions.CertSelectionDelegate != null)
{
X509Certificate2? remoteCert = null;
string[] issuers = CertificateValidationPal.GetRequestCertificateAuthorities(context);
try
{
remoteCert = CertificateValidationPal.GetRemoteCertificate(context);
if (sslAuthenticationOptions.ClientCertificates == null)
{
sslAuthenticationOptions.ClientCertificates = new X509CertificateCollection();
}
X509Certificate2 clientCertificate = (X509Certificate2)sslAuthenticationOptions.CertSelectionDelegate(sslAuthenticationOptions.TargetHost!, sslAuthenticationOptions.ClientCertificates, remoteCert, issuers);
if (clientCertificate != null && clientCertificate.HasPrivateKey)
{
sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(clientCertificate);
}
}
finally
{
remoteCert?.Dispose();
}
}

Interop.OpenSsl.UpdateClientCertiticate(((SafeDeleteSslContext)context).SslContext, sslAuthenticationOptions);
errorCode = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, null, out output, out outputSize);
}

// sometimes during renegotiation processing messgae does not yield new output.
// That seems to be flaw in OpenSSL state machine and we have workaround to peek it and try it again.
if (outputSize == 0 && Interop.Ssl.IsSslRenegotiatePending(((SafeDeleteSslContext)context).SslContext))
{
done = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, ReadOnlySpan<byte>.Empty, out output, out outputSize);
errorCode = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, ReadOnlySpan<byte>.Empty, out output, out outputSize);
}

// 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 = context.SslContext;
if (done && sslAuthenticationOptions.IsServer
if (errorCode == SecurityStatusPalErrorCode.OK && sslAuthenticationOptions.IsServer
&& sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0
&& sslContext.AlpnHandle.IsAllocated && sslContext.AlpnHandle.Target == null)
{
Expand All @@ -183,7 +214,7 @@ private static SecurityStatusPal HandshakeInternal(SafeFreeCredentials credentia
outputSize == output!.Length ? output :
new Span<byte>(output, 0, outputSize).ToArray();

return new SecurityStatusPal(done ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.ContinueNeeded);
return new SecurityStatusPal(errorCode);
}
catch (Exception exc)
{
Expand Down
Loading

0 comments on commit 8fef95b

Please sign in to comment.