Skip to content
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

fix LocalCertificateSelectionCallback on unix #63200

Merged
merged 14 commits into from
Jan 12, 2022
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 @@ -215,6 +215,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 proseed without it.
wfurt marked this conversation as resolved.
Show resolved Hide resolved
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);
}
rzikm marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -283,6 +322,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 @@ -320,7 +366,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 @@ -341,6 +387,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 @@ -385,7 +436,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);
rzikm marked this conversation as resolved.
Show resolved Hide resolved

[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);
var errorCode = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, inputBuffer, out output, out outputSize);
wfurt marked this conversation as resolved.
Show resolved Hide resolved

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();
}
}
rzikm marked this conversation as resolved.
Show resolved Hide resolved

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);
wfurt marked this conversation as resolved.
Show resolved Hide resolved
}
catch (Exception exc)
{
Expand Down
Loading