Skip to content

Commit 8fef95b

Browse files
authored
fix LocalCertificateSelectionCallback on unix (#63200)
* 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
1 parent b25b96e commit 8fef95b

File tree

15 files changed

+369
-9
lines changed

15 files changed

+369
-9
lines changed

src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ssl.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ internal enum PAL_TlsHandshakeState
4646
WouldBlock,
4747
ServerAuthCompleted,
4848
ClientAuthCompleted,
49+
ClientCertRequested,
4950
}
5051

5152
internal enum PAL_TlsIo
@@ -99,6 +100,12 @@ private static partial int AppleCryptoNative_SslSetBreakOnClientAuth(
99100
int setBreak,
100101
out int pOSStatus);
101102

103+
[GeneratedDllImport(Interop.Libraries.AppleCryptoNative)]
104+
private static partial int AppleCryptoNative_SslSetBreakOnCertRequested(
105+
SafeSslHandle sslHandle,
106+
int setBreak,
107+
out int pOSStatus);
108+
102109
[GeneratedDllImport(Interop.Libraries.AppleCryptoNative)]
103110
private static partial int AppleCryptoNative_SslSetCertificate(
104111
SafeSslHandle sslHandle,
@@ -266,6 +273,25 @@ internal static void SslBreakOnClientAuth(SafeSslHandle sslHandle, bool setBreak
266273
throw new SslException();
267274
}
268275

276+
internal static void SslBreakOnCertRequested(SafeSslHandle sslHandle, bool setBreak)
277+
{
278+
int osStatus;
279+
int result = AppleCryptoNative_SslSetBreakOnCertRequested(sslHandle, setBreak ? 1 : 0, out osStatus);
280+
281+
if (result == 1)
282+
{
283+
return;
284+
}
285+
286+
if (result == 0)
287+
{
288+
throw CreateExceptionForOSStatus(osStatus);
289+
}
290+
291+
Debug.Fail($"AppleCryptoNative_SslSetBreakOnCertRequested returned {result}");
292+
throw new SslException();
293+
}
294+
269295
internal static void SslSetCertificate(SafeSslHandle sslHandle, IntPtr[] certChainPtrs)
270296
{
271297
using (SafeCreateHandle cfCertRefs = CoreFoundation.CFArrayCreate(certChainPtrs, (UIntPtr)certChainPtrs.Length))

src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,45 @@ internal static SafeSslContextHandle AllocateSslContext(SafeFreeSslCredentials c
219219
return sslCtx;
220220
}
221221

222+
internal static void UpdateClientCertiticate(SafeSslHandle ssl, SslAuthenticationOptions sslAuthenticationOptions)
223+
{
224+
// Disable certificate selection callback. We either got certificate or we will try to proceed without it.
225+
Interop.Ssl.SslSetClientCertCallback(ssl, 0);
226+
227+
if (sslAuthenticationOptions.CertificateContext == null)
228+
{
229+
return;
230+
}
231+
232+
var credential = new SafeFreeSslCredentials(sslAuthenticationOptions.CertificateContext, sslAuthenticationOptions.EnabledSslProtocols, sslAuthenticationOptions.EncryptionPolicy, sslAuthenticationOptions.IsServer);
233+
SafeX509Handle? certHandle = credential.CertHandle;
234+
SafeEvpPKeyHandle? certKeyHandle = credential.CertKeyHandle;
235+
236+
Debug.Assert(certHandle != null);
237+
Debug.Assert(certKeyHandle != null);
238+
239+
int retVal = Ssl.SslUseCertificate(ssl, certHandle);
240+
if (1 != retVal)
241+
{
242+
throw CreateSslException(SR.net_ssl_use_cert_failed);
243+
}
244+
245+
retVal = Ssl.SslUsePrivateKey(ssl, certKeyHandle);
246+
if (1 != retVal)
247+
{
248+
throw CreateSslException(SR.net_ssl_use_private_key_failed);
249+
}
250+
251+
if (sslAuthenticationOptions.CertificateContext.IntermediateCertificates.Length > 0)
252+
{
253+
if (!Ssl.AddExtraChainCertificates(ssl, sslAuthenticationOptions.CertificateContext.IntermediateCertificates))
254+
{
255+
throw CreateSslException(SR.net_ssl_use_cert_failed);
256+
}
257+
}
258+
259+
}
260+
222261
// This essentially wraps SSL* SSL_new()
223262
internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credential, SslAuthenticationOptions sslAuthenticationOptions)
224263
{
@@ -287,6 +326,13 @@ internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credentia
287326
{
288327
Crypto.ErrClearError();
289328
}
329+
330+
if (sslAuthenticationOptions.CertSelectionDelegate != null && sslAuthenticationOptions.CertificateContext == null)
331+
{
332+
// We don't have certificate but we have callback. We should wait for remote certificate and
333+
// possible trusted issuer list.
334+
Interop.Ssl.SslSetClientCertCallback(sslHandle, 1);
335+
}
290336
}
291337

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

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

394+
if (error == Ssl.SslErrorCode.SSL_ERROR_WANT_X509_LOOKUP)
395+
{
396+
return SecurityStatusPalErrorCode.CredentialsNeeded;
397+
}
398+
348399
if ((retVal != -1) || (error != Ssl.SslErrorCode.SSL_ERROR_WANT_READ))
349400
{
350401
// Handshake failed, but even if the handshake does not need to read, there may be an Alert going out.
@@ -389,7 +440,8 @@ internal static bool DoSslHandshake(SafeSslHandle context, ReadOnlySpan<byte> in
389440
{
390441
context.MarkHandshakeCompleted();
391442
}
392-
return stateOk;
443+
444+
return stateOk ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.ContinueNeeded;
393445
}
394446

395447
internal static int Encrypt(SafeSslHandle context, ReadOnlySpan<byte> input, ref byte[] output, out Ssl.SslErrorCode errorCode)

src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Net.Security;
88
using System.Runtime.InteropServices;
9+
using System.Security.Cryptography;
910
using System.Security.Cryptography.X509Certificates;
1011
using Microsoft.Win32.SafeHandles;
1112

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

153+
[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslUseCertificate")]
154+
internal static extern int SslUseCertificate(SafeSslHandle ssl, SafeX509Handle certPtr);
155+
156+
[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslUsePrivateKey")]
157+
internal static extern int SslUsePrivateKey(SafeSslHandle ssl, SafeEvpPKeyHandle keyPtr);
158+
159+
[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSetClientCertCallback")]
160+
internal static extern unsafe void SslSetClientCertCallback(SafeSslHandle ssl, int set);
161+
152162
[GeneratedDllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_Tls13Supported")]
153163
private static partial int Tls13SupportedImpl();
154164

@@ -192,6 +202,28 @@ internal static byte[] ConvertAlpnProtocolListToByteArray(List<SslApplicationPro
192202
return buffer;
193203
}
194204

205+
[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslAddExtraChainCert")]
206+
internal static extern bool SslAddExtraChainCert(SafeSslHandle ssl, SafeX509Handle x509);
207+
208+
internal static bool AddExtraChainCertificates(SafeSslHandle ssl, X509Certificate2[] chain)
209+
{
210+
// send pre-computed list of intermediates.
211+
for (int i = 0; i < chain.Length; i++)
212+
{
213+
SafeX509Handle dupCertHandle = Crypto.X509UpRef(chain[i].Handle);
214+
Crypto.CheckValidOpenSslHandle(dupCertHandle);
215+
if (!SslAddExtraChainCert(ssl, dupCertHandle))
216+
{
217+
Crypto.ErrClearError();
218+
dupCertHandle.Dispose(); // we still own the safe handle; clean it up
219+
return false;
220+
}
221+
dupCertHandle.SetHandleAsInvalid(); // ownership has been transferred to sslHandle; do not free via this safe handle
222+
}
223+
224+
return true;
225+
}
226+
195227
internal static string? GetOpenSslCipherSuiteName(SafeSslHandle ssl, TlsCipherSuite cipherSuite, out bool isTls12OrLower)
196228
{
197229
string? ret = Marshal.PtrToStringAnsi(GetOpenSslCipherSuiteName(ssl, (int)cipherSuite, out int isTls12OrLowerInt));
@@ -224,6 +256,7 @@ internal enum SslErrorCode
224256
SSL_ERROR_SSL = 1,
225257
SSL_ERROR_WANT_READ = 2,
226258
SSL_ERROR_WANT_WRITE = 3,
259+
SSL_ERROR_WANT_X509_LOOKUP = 4,
227260
SSL_ERROR_SYSCALL = 5,
228261
SSL_ERROR_ZERO_RETURN = 6,
229262

src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ internal static string[] GetRequestCertificateAuthorities(SafeDeleteContext secu
128128

129129
using (SafeCFArrayHandle dnArray = Interop.AppleCrypto.SslCopyCADistinguishedNames(sslContext))
130130
{
131+
if (dnArray.IsInvalid)
132+
{
133+
return Array.Empty<string>();
134+
}
135+
131136
long size = Interop.CoreFoundation.CFArrayGetCount(dnArray);
132137

133138
if (size == 0)

src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ private static void SetProtocols(SafeSslHandle sslContext, SslProtocols protocol
316316
Interop.AppleCrypto.SslSetMaxProtocolVersion(sslContext, maxProtocolId);
317317
}
318318

319-
private static void SetCertificate(SafeSslHandle sslContext, SslStreamCertificateContext context)
319+
internal static void SetCertificate(SafeSslHandle sslContext, SslStreamCertificateContext context)
320320
{
321321
Debug.Assert(sslContext != null, "sslContext != null");
322322

src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,13 @@ private static SecurityStatusPal HandshakeInternal(
246246
Interop.AppleCrypto.SslSetTargetName(sslContext.SslContext, sslAuthenticationOptions.TargetHost);
247247
}
248248

249+
if (sslAuthenticationOptions.CertificateContext == null && sslAuthenticationOptions.CertSelectionDelegate != null)
250+
{
251+
// certificate was not provided but there is user callback. We can break handshake if server asks for certificate
252+
// and we can try to get it based on remote certificate and trusted issuers.
253+
Interop.AppleCrypto.SslBreakOnCertRequested(sslContext.SslContext, true);
254+
}
255+
249256
if (sslAuthenticationOptions.IsServer && sslAuthenticationOptions.RemoteCertRequired)
250257
{
251258
Interop.AppleCrypto.SslSetAcceptClientCert(sslContext.SslContext);
@@ -259,6 +266,35 @@ private static SecurityStatusPal HandshakeInternal(
259266

260267
SafeSslHandle sslHandle = sslContext!.SslContext;
261268
SecurityStatusPal status = PerformHandshake(sslHandle);
269+
if (status.ErrorCode == SecurityStatusPalErrorCode.CredentialsNeeded)
270+
{
271+
// we should not be here if CertSelectionDelegate is null but better check before dereferencing..
272+
if (sslAuthenticationOptions.CertSelectionDelegate != null)
273+
{
274+
X509Certificate2? remoteCert = null;
275+
try
276+
{
277+
string[] issuers = CertificateValidationPal.GetRequestCertificateAuthorities(context);
278+
remoteCert = CertificateValidationPal.GetRemoteCertificate(context);
279+
if (sslAuthenticationOptions.ClientCertificates == null)
280+
{
281+
sslAuthenticationOptions.ClientCertificates = new X509CertificateCollection();
282+
}
283+
X509Certificate2 clientCertificate = (X509Certificate2)sslAuthenticationOptions.CertSelectionDelegate(sslAuthenticationOptions.TargetHost!, sslAuthenticationOptions.ClientCertificates, remoteCert, issuers);
284+
if (clientCertificate != null)
285+
{
286+
SafeDeleteSslContext.SetCertificate(sslContext.SslContext, SslStreamCertificateContext.Create(clientCertificate));
287+
}
288+
}
289+
finally
290+
{
291+
remoteCert?.Dispose();
292+
}
293+
}
294+
295+
// We either got certificate or we can proceed without it. It is up to the server to decide if either is OK.
296+
status = PerformHandshake(sslHandle);
297+
}
262298

263299
outputBuffer = sslContext.ReadPendingWrites();
264300
return status;
@@ -290,6 +326,8 @@ private static SecurityStatusPal PerformHandshake(SafeSslHandle sslHandle)
290326
// So, call SslHandshake again to indicate to Secure Transport that we've
291327
// accepted this handshake and it should go into the ready state.
292328
break;
329+
case PAL_TlsHandshakeState.ClientCertRequested:
330+
return new SecurityStatusPal(SecurityStatusPalErrorCode.CredentialsNeeded);
293331
default:
294332
return new SecurityStatusPal(
295333
SecurityStatusPalErrorCode.InternalError,

src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ private static SecurityStatusPal MapNativeErrorCode(Interop.Ssl.SslErrorCode err
8787
{
8888
Interop.Ssl.SslErrorCode.SSL_ERROR_RENEGOTIATE => new SecurityStatusPal(SecurityStatusPalErrorCode.Renegotiate),
8989
Interop.Ssl.SslErrorCode.SSL_ERROR_ZERO_RETURN => new SecurityStatusPal(SecurityStatusPalErrorCode.ContextExpired),
90+
Interop.Ssl.SslErrorCode.SSL_ERROR_WANT_X509_LOOKUP => new SecurityStatusPal(SecurityStatusPalErrorCode.CredentialsNeeded),
9091
Interop.Ssl.SslErrorCode.SSL_ERROR_NONE or
9192
Interop.Ssl.SslErrorCode.SSL_ERROR_WANT_READ => new SecurityStatusPal(SecurityStatusPalErrorCode.OK),
9293
_ => new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, new Interop.OpenSsl.SslException((int)errorCode))
@@ -158,20 +159,50 @@ private static SecurityStatusPal HandshakeInternal(SafeFreeCredentials credentia
158159
context = new SafeDeleteSslContext((credential as SafeFreeSslCredentials)!, sslAuthenticationOptions);
159160
}
160161

161-
bool done = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, inputBuffer, out output, out outputSize);
162+
SecurityStatusPalErrorCode errorCode = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, inputBuffer, out output, out outputSize);
163+
164+
if (errorCode == SecurityStatusPalErrorCode.CredentialsNeeded)
165+
{
166+
if (sslAuthenticationOptions.CertSelectionDelegate != null)
167+
{
168+
X509Certificate2? remoteCert = null;
169+
string[] issuers = CertificateValidationPal.GetRequestCertificateAuthorities(context);
170+
try
171+
{
172+
remoteCert = CertificateValidationPal.GetRemoteCertificate(context);
173+
if (sslAuthenticationOptions.ClientCertificates == null)
174+
{
175+
sslAuthenticationOptions.ClientCertificates = new X509CertificateCollection();
176+
}
177+
X509Certificate2 clientCertificate = (X509Certificate2)sslAuthenticationOptions.CertSelectionDelegate(sslAuthenticationOptions.TargetHost!, sslAuthenticationOptions.ClientCertificates, remoteCert, issuers);
178+
if (clientCertificate != null && clientCertificate.HasPrivateKey)
179+
{
180+
sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(clientCertificate);
181+
}
182+
}
183+
finally
184+
{
185+
remoteCert?.Dispose();
186+
}
187+
}
188+
189+
Interop.OpenSsl.UpdateClientCertiticate(((SafeDeleteSslContext)context).SslContext, sslAuthenticationOptions);
190+
errorCode = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, null, out output, out outputSize);
191+
}
192+
162193
// sometimes during renegotiation processing messgae does not yield new output.
163194
// That seems to be flaw in OpenSSL state machine and we have workaround to peek it and try it again.
164195
if (outputSize == 0 && Interop.Ssl.IsSslRenegotiatePending(((SafeDeleteSslContext)context).SslContext))
165196
{
166-
done = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, ReadOnlySpan<byte>.Empty, out output, out outputSize);
197+
errorCode = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, ReadOnlySpan<byte>.Empty, out output, out outputSize);
167198
}
168199

169200
// When the handshake is done, and the context is server, check if the alpnHandle target was set to null during ALPN.
170201
// If it was, then that indicates ALPN failed, send failure.
171202
// We have this workaround, as openssl supports terminating handshake only from version 1.1.0,
172203
// whereas ALPN is supported from version 1.0.2.
173204
SafeSslHandle sslContext = context.SslContext;
174-
if (done && sslAuthenticationOptions.IsServer
205+
if (errorCode == SecurityStatusPalErrorCode.OK && sslAuthenticationOptions.IsServer
175206
&& sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0
176207
&& sslContext.AlpnHandle.IsAllocated && sslContext.AlpnHandle.Target == null)
177208
{
@@ -183,7 +214,7 @@ private static SecurityStatusPal HandshakeInternal(SafeFreeCredentials credentia
183214
outputSize == output!.Length ? output :
184215
new Span<byte>(output, 0, outputSize).ToArray();
185216

186-
return new SecurityStatusPal(done ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.ContinueNeeded);
217+
return new SecurityStatusPal(errorCode);
187218
}
188219
catch (Exception exc)
189220
{

0 commit comments

Comments
 (0)