Skip to content

Commit 6dfbc6e

Browse files
authored
Fix IsMutuallyAuthenticated on Linux and OSX (#63945)
* WIP - prepared a failing test * Fix IsMutuallyAuthenticated on Linux * Fix failing unit tests * Minor cleanup * Port changes to OSX * Fix comment * Invoke cert selection inline, don't allocate new credentials on Linux/OSX * Fix tests on OSX * Code review feedback * Move tests to separate file * Fix build * Fix Failing tests
1 parent 24df302 commit 6dfbc6e

File tree

9 files changed

+238
-120
lines changed

9 files changed

+238
-120
lines changed

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,12 +327,9 @@ internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credentia
327327
Crypto.ErrClearError();
328328
}
329329

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-
}
330+
// Set client cert callback, this will interrupt the handshake with SecurityStatusPalErrorCode.CredentialsNeeded
331+
// if server actually requests a certificate.
332+
Ssl.SslSetClientCertCallback(sslHandle, 1);
336333
}
337334

338335
if (sslAuthenticationOptions.IsServer && sslAuthenticationOptions.RemoteCertRequired)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ private static SafeSslHandle CreateSslContext(SafeFreeSslCredentials credential,
130130
SetCertificate(sslContext, credential.CertificateContext);
131131
}
132132

133+
Interop.AppleCrypto.SslBreakOnCertRequested(sslContext, true);
133134
Interop.AppleCrypto.SslBreakOnServerAuth(sslContext, true);
134135
Interop.AppleCrypto.SslBreakOnClientAuth(sslContext, true);
135136
}

src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -295,62 +295,24 @@ private string[] GetRequestCertificateAuthorities()
295295
return issuers;
296296
}
297297

298-
/*++
299-
AcquireCredentials - Attempts to find Client Credential
300-
Information, that can be sent to the server. In our case,
301-
this is only Client Certificates, that we have Credential Info.
302-
303-
How it works:
304-
case 0: Cert Selection delegate is present
305-
Always use its result as the client cert answer.
306-
Try to use cached credential handle whenever feasible.
307-
Do not use cached anonymous creds if the delegate has returned null
308-
and the collection is not empty (allow responding with the cert later).
309-
310-
case 1: Certs collection is empty
311-
Always use the same statically acquired anonymous SSL Credential
312-
313-
case 2: Before our Connection with the Server
314-
If we have a cached credential handle keyed by first X509Certificate
315-
**content** in the passed collection, then we use that cached
316-
credential and hoping to restart a session.
317-
318-
Otherwise create a new anonymous (allow responding with the cert later).
319-
320-
case 3: After our Connection with the Server (i.e. during handshake or re-handshake)
321-
The server has requested that we send it a Certificate then
322-
we Enumerate a list of server sent Issuers trying to match against
323-
our list of Certificates, the first match is sent to the server.
324-
325-
Once we got a cert we again try to match cached credential handle if possible.
326-
This will not restart a session but helps minimizing the number of handles we create.
327-
328-
In the case of an error getting a Certificate or checking its private Key we fall back
329-
to the behavior of having no certs, case 1.
330-
331-
Returns: True if cached creds were used, false otherwise.
332-
333-
--*/
334-
335-
private bool AcquireClientCredentials(ref byte[]? thumbPrint)
298+
internal X509Certificate2? SelectClientCertificate(out bool sessionRestartAttempt)
336299
{
337-
// Acquire possible Client Certificate information and set it on the handle.
338-
X509Certificate? clientCertificate = null; // This is a candidate that can come from the user callback or be guessed when targeting a session restart.
339-
List<X509Certificate>? filteredCerts = null; // This is an intermediate client certs collection that try to use if no selectedCert is available yet.
340-
string[] issuers; // This is a list of issuers sent by the server, only valid is we do know what the server cert is.
300+
sessionRestartAttempt = false;
341301

342-
bool sessionRestartAttempt = false; // If true and no cached creds we will use anonymous creds.
302+
X509Certificate? clientCertificate = null; // candidate certificate that can come from the user callback or be guessed when targeting a session restart.
303+
X509Certificate2? selectedCert = null; // final selected cert (ensured that it does have private key with it).
304+
List<X509Certificate>? filteredCerts = null; // This is an intermediate client certs collection that try to use if no selectedCert is available yet.
305+
string[] issuers; // This is a list of issuers sent by the server, only valid if we do know what the server cert is.
343306

344307
if (_sslAuthenticationOptions.CertSelectionDelegate != null)
345308
{
346-
issuers = GetRequestCertificateAuthorities();
347-
348309
if (NetEventSource.Log.IsEnabled())
349310
NetEventSource.Info(this, "Calling CertificateSelectionCallback");
350311

351312
X509Certificate2? remoteCert = null;
352313
try
353314
{
315+
issuers = GetRequestCertificateAuthorities();
354316
remoteCert = CertificateValidationPal.GetRemoteCertificate(_securityContext!);
355317
if (_sslAuthenticationOptions.ClientCertificates == null)
356318
{
@@ -363,7 +325,6 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint)
363325
remoteCert?.Dispose();
364326
}
365327

366-
367328
if (clientCertificate != null)
368329
{
369330
if (_credentialsHandle == null)
@@ -505,9 +466,6 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint)
505466
}
506467
}
507468

508-
bool cachedCred = false; // This is a return result from this method.
509-
X509Certificate2? selectedCert = null; // This is a final selected cert (ensured that it does have private key with it).
510-
511469
clientCertificate = null;
512470

513471
if (NetEventSource.Log.IsEnabled())
@@ -550,6 +508,56 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint)
550508

551509
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Selected cert = {selectedCert}");
552510

511+
_selectedClientCertificate = clientCertificate;
512+
return selectedCert;
513+
}
514+
515+
/*++
516+
AcquireCredentials - Attempts to find Client Credential
517+
Information, that can be sent to the server. In our case,
518+
this is only Client Certificates, that we have Credential Info.
519+
520+
How it works:
521+
case 0: Cert Selection delegate is present
522+
Always use its result as the client cert answer.
523+
Try to use cached credential handle whenever feasible.
524+
Do not use cached anonymous creds if the delegate has returned null
525+
and the collection is not empty (allow responding with the cert later).
526+
527+
case 1: Certs collection is empty
528+
Always use the same statically acquired anonymous SSL Credential
529+
530+
case 2: Before our Connection with the Server
531+
If we have a cached credential handle keyed by first X509Certificate
532+
**content** in the passed collection, then we use that cached
533+
credential and hoping to restart a session.
534+
535+
Otherwise create a new anonymous (allow responding with the cert later).
536+
537+
case 3: After our Connection with the Server (i.e. during handshake or re-handshake)
538+
The server has requested that we send it a Certificate then
539+
we Enumerate a list of server sent Issuers trying to match against
540+
our list of Certificates, the first match is sent to the server.
541+
542+
Once we got a cert we again try to match cached credential handle if possible.
543+
This will not restart a session but helps minimizing the number of handles we create.
544+
545+
In the case of an error getting a Certificate or checking its private Key we fall back
546+
to the behavior of having no certs, case 1.
547+
548+
Returns: True if cached creds were used, false otherwise.
549+
550+
--*/
551+
552+
private bool AcquireClientCredentials(ref byte[]? thumbPrint)
553+
{
554+
// Acquire possible Client Certificate information and set it on the handle.
555+
556+
bool sessionRestartAttempt; // If true and no cached creds we will use anonymous creds.
557+
bool cachedCred = false; // this is a return result from this method.
558+
559+
X509Certificate2? selectedCert = SelectClientCertificate(out sessionRestartAttempt);
560+
553561
try
554562
{
555563
// Try to locate cached creds first.
@@ -574,22 +582,21 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint)
574582
// So we don't want to reuse **anonymous** cached credential for a new SSL connection if the client has passed some certificate.
575583
// The following block happens if client did specify a certificate but no cached creds were found in the cache.
576584
// Since we don't restart a session the server side can still challenge for a client cert.
577-
if ((object?)clientCertificate != (object?)selectedCert)
585+
if ((object?)_selectedClientCertificate != (object?)selectedCert)
578586
{
579587
selectedCert.Dispose();
580588
}
581589

582590
guessedThumbPrint = null;
583591
selectedCert = null;
584-
clientCertificate = null;
592+
_selectedClientCertificate = null;
585593
}
586594

587595
if (cachedCredentialHandle != null)
588596
{
589597
if (NetEventSource.Log.IsEnabled())
590598
NetEventSource.Log.UsingCachedCredential(this);
591599
_credentialsHandle = cachedCredentialHandle;
592-
_selectedClientCertificate = clientCertificate;
593600
cachedCred = true;
594601
if (selectedCert != null)
595602
{
@@ -607,7 +614,6 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint)
607614
_sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer);
608615

609616
thumbPrint = guessedThumbPrint; // Delay until here in case something above threw.
610-
_selectedClientCertificate = clientCertificate;
611617
}
612618
}
613619
finally
@@ -792,6 +798,7 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan<byte> inputBuffer, ref byte
792798
if (_sslAuthenticationOptions.IsServer)
793799
{
794800
status = SslStreamPal.AcceptSecurityContext(
801+
this,
795802
ref _credentialsHandle!,
796803
ref _securityContext,
797804
inputBuffer,
@@ -801,6 +808,7 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan<byte> inputBuffer, ref byte
801808
else
802809
{
803810
status = SslStreamPal.InitializeSecurityContext(
811+
this,
804812
ref _credentialsHandle!,
805813
ref _securityContext,
806814
_sslAuthenticationOptions.TargetHost,
@@ -841,6 +849,7 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan<byte> inputBuffer, ref byte
841849
internal SecurityStatusPal Renegotiate(out byte[]? output)
842850
{
843851
return SslStreamPal.Renegotiate(
852+
this,
844853
ref _credentialsHandle!,
845854
ref _securityContext,
846855
_sslAuthenticationOptions,

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public static void VerifyPackageInfo()
2525
}
2626

2727
public static SecurityStatusPal AcceptSecurityContext(
28+
SecureChannel secureChannel,
2829
ref SafeFreeCredentials credential,
2930
ref SafeDeleteSslContext? context,
3031
ReadOnlySpan<byte> inputBuffer,
@@ -35,6 +36,7 @@ public static SecurityStatusPal AcceptSecurityContext(
3536
}
3637

3738
public static SecurityStatusPal InitializeSecurityContext(
39+
SecureChannel secureChannel,
3840
ref SafeFreeCredentials credential,
3941
ref SafeDeleteSslContext? context,
4042
string? targetName,
@@ -45,7 +47,12 @@ public static SecurityStatusPal InitializeSecurityContext(
4547
return HandshakeInternal(credential, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
4648
}
4749

48-
public static SecurityStatusPal Renegotiate(ref SafeFreeCredentials? credentialsHandle, ref SafeDeleteSslContext? context, SslAuthenticationOptions sslAuthenticationOptions, out byte[]? outputBuffer)
50+
public static SecurityStatusPal Renegotiate(
51+
SecureChannel secureChannel,
52+
ref SafeFreeCredentials? credentialsHandle,
53+
ref SafeDeleteSslContext? context,
54+
SslAuthenticationOptions sslAuthenticationOptions,
55+
out byte[]? outputBuffer)
4956
{
5057
throw new PlatformNotSupportedException();
5158
}

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

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static Exception GetException(SecurityStatusPal status)
2020
return status.Exception ?? new Win32Exception((int)status.ErrorCode);
2121
}
2222

23-
internal const bool StartMutualAuthAsAnonymous = false;
23+
internal const bool StartMutualAuthAsAnonymous = true;
2424

2525
// SecureTransport is okay with a 0 byte input, but it produces a 0 byte output.
2626
// Since ST is not producing the framed empty message just call this false and avoid the
@@ -32,27 +32,34 @@ public static void VerifyPackageInfo()
3232
}
3333

3434
public static SecurityStatusPal AcceptSecurityContext(
35+
SecureChannel secureChannel,
3536
ref SafeFreeCredentials credential,
3637
ref SafeDeleteSslContext? context,
3738
ReadOnlySpan<byte> inputBuffer,
3839
ref byte[]? outputBuffer,
3940
SslAuthenticationOptions sslAuthenticationOptions)
4041
{
41-
return HandshakeInternal(credential, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
42+
return HandshakeInternal(secureChannel, credential, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
4243
}
4344

4445
public static SecurityStatusPal InitializeSecurityContext(
46+
SecureChannel secureChannel,
4547
ref SafeFreeCredentials credential,
4648
ref SafeDeleteSslContext? context,
4749
string? targetName,
4850
ReadOnlySpan<byte> inputBuffer,
4951
ref byte[]? outputBuffer,
5052
SslAuthenticationOptions sslAuthenticationOptions)
5153
{
52-
return HandshakeInternal(credential, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
54+
return HandshakeInternal(secureChannel, credential, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
5355
}
5456

55-
public static SecurityStatusPal Renegotiate(ref SafeFreeCredentials? credentialsHandle, ref SafeDeleteSslContext? context, SslAuthenticationOptions sslAuthenticationOptions, out byte[]? outputBuffer)
57+
public static SecurityStatusPal Renegotiate(
58+
SecureChannel secureChannel,
59+
ref SafeFreeCredentials? credentialsHandle,
60+
ref SafeDeleteSslContext? context,
61+
SslAuthenticationOptions sslAuthenticationOptions,
62+
out byte[]? outputBuffer)
5663
{
5764
throw new PlatformNotSupportedException();
5865
}
@@ -224,6 +231,7 @@ public static void QueryContextConnectionInfo(
224231
}
225232

226233
private static SecurityStatusPal HandshakeInternal(
234+
SecureChannel secureChannel,
227235
SafeFreeCredentials credential,
228236
ref SafeDeleteSslContext? context,
229237
ReadOnlySpan<byte> inputBuffer,
@@ -268,28 +276,11 @@ private static SecurityStatusPal HandshakeInternal(
268276
SecurityStatusPal status = PerformHandshake(sslHandle);
269277
if (status.ErrorCode == SecurityStatusPalErrorCode.CredentialsNeeded)
270278
{
271-
// we should not be here if CertSelectionDelegate is null but better check before dereferencing..
272-
if (sslAuthenticationOptions.CertSelectionDelegate != null)
279+
X509Certificate2? clientCertificate = secureChannel.SelectClientCertificate(out _);
280+
if (clientCertificate != null)
273281
{
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-
}
282+
sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(clientCertificate);
283+
SafeDeleteSslContext.SetCertificate(sslContext.SslContext, sslAuthenticationOptions.CertificateContext);
293284
}
294285

295286
// We either got certificate or we can proceed without it. It is up to the server to decide if either is OK.

0 commit comments

Comments
 (0)