Skip to content

Commit e2f9462

Browse files
authored
add NegotiateClientCertificateAsync support on Windows (#51905)
* add NegotiateClientCertificateAsync support on Windows * feedback from review and test update * throw on data during renegotiation * disable NegotiateClientCertificateAsync on Win7 * feedback from review * use Interlocked.Exchang instead of CompareExchange * add trace message
1 parent 1d9ff9e commit e2f9462

15 files changed

Lines changed: 433 additions & 19 deletions

File tree

src/libraries/Common/src/Interop/Windows/SChannel/Interop.SECURITY_STATUS.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ internal enum SECURITY_STATUS
5050
BadBinding = unchecked((int)0x80090346),
5151
DowngradeDetected = unchecked((int)0x80090350),
5252
ApplicationProtocolMismatch = unchecked((int)0x80090367),
53+
NoRenegotiation = unchecked((int)0x00090360),
5354
}
5455
}

src/libraries/Common/src/System/Net/SecurityStatusAdapterPal.Windows.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace System.Net
99
{
1010
internal static class SecurityStatusAdapterPal
1111
{
12-
private const int StatusDictionarySize = 42;
12+
private const int StatusDictionarySize = 43;
1313

1414
#if DEBUG
1515
static SecurityStatusAdapterPal()
@@ -61,7 +61,8 @@ static SecurityStatusAdapterPal()
6161
{ Interop.SECURITY_STATUS.UnsupportedPreauth, SecurityStatusPalErrorCode.UnsupportedPreauth },
6262
{ Interop.SECURITY_STATUS.Unsupported, SecurityStatusPalErrorCode.Unsupported },
6363
{ Interop.SECURITY_STATUS.UntrustedRoot, SecurityStatusPalErrorCode.UntrustedRoot },
64-
{ Interop.SECURITY_STATUS.WrongPrincipal, SecurityStatusPalErrorCode.WrongPrincipal }
64+
{ Interop.SECURITY_STATUS.WrongPrincipal, SecurityStatusPalErrorCode.WrongPrincipal },
65+
{ Interop.SECURITY_STATUS.NoRenegotiation, SecurityStatusPalErrorCode.NoRenegotiation }
6566
};
6667

6768
internal static SecurityStatusPal GetSecurityStatusPalFromNativeInt(int win32SecurityStatus)

src/libraries/Common/src/System/Net/SecurityStatusPal.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ internal enum SecurityStatusPalErrorCode
6969
UnsupportedPreauth,
7070
BadBinding,
7171
DowngradeDetected,
72-
ApplicationProtocolMismatch
72+
ApplicationProtocolMismatch,
73+
NoRenegotiation
7374
}
7475
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ public virtual void AuthenticateAsServer(System.Security.Cryptography.X509Certif
215215
public virtual System.Threading.Tasks.Task AuthenticateAsServerAsync(System.Security.Cryptography.X509Certificates.X509Certificate serverCertificate) { throw null; }
216216
public virtual System.Threading.Tasks.Task AuthenticateAsServerAsync(System.Security.Cryptography.X509Certificates.X509Certificate serverCertificate, bool clientCertificateRequired, bool checkCertificateRevocation) { throw null; }
217217
public virtual System.Threading.Tasks.Task AuthenticateAsServerAsync(System.Security.Cryptography.X509Certificates.X509Certificate serverCertificate, bool clientCertificateRequired, System.Security.Authentication.SslProtocols enabledSslProtocols, bool checkCertificateRevocation) { throw null; }
218+
public System.Threading.Tasks.Task AuthenticateAsServerAsync(ServerOptionsSelectionCallback optionsCallback, object? state, System.Threading.CancellationToken cancellationToken = default) { throw null; }
218219
public virtual System.IAsyncResult BeginAuthenticateAsClient(string targetHost, System.AsyncCallback? asyncCallback, object? asyncState) { throw null; }
219220
public virtual System.IAsyncResult BeginAuthenticateAsClient(string targetHost, System.Security.Cryptography.X509Certificates.X509CertificateCollection? clientCertificates, bool checkCertificateRevocation, System.AsyncCallback? asyncCallback, object? asyncState) { throw null; }
220221
public virtual System.IAsyncResult BeginAuthenticateAsClient(string targetHost, System.Security.Cryptography.X509Certificates.X509CertificateCollection? clientCertificates, System.Security.Authentication.SslProtocols enabledSslProtocols, bool checkCertificateRevocation, System.AsyncCallback? asyncCallback, object? asyncState) { throw null; }
@@ -232,6 +233,7 @@ public override void EndWrite(System.IAsyncResult asyncResult) { }
232233
~SslStream() { }
233234
public override void Flush() { }
234235
public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
236+
public virtual System.Threading.Tasks.Task NegotiateClientCertificateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
235237
public override int Read(byte[] buffer, int offset, int count) { throw null; }
236238
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
237239
public override System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
@@ -243,7 +245,6 @@ public void Write(byte[] buffer) { }
243245
public override void Write(byte[] buffer, int offset, int count) { }
244246
public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
245247
public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
246-
public System.Threading.Tasks.Task AuthenticateAsServerAsync(ServerOptionsSelectionCallback optionsCallback, object? state, System.Threading.CancellationToken cancellationToken = default) { throw null; }
247248
}
248249
[System.CLSCompliantAttribute(false)]
249250
public enum TlsCipherSuite : ushort

src/libraries/System.Net.Security/src/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,4 +449,10 @@
449449
<data name="SystemNetSecurity_PlatformNotSupported" xml:space="preserve">
450450
<value>System.Net.Security is not supported on this platform.</value>
451451
</data>
452+
<data name="net_ssl_certificate_exist" xml:space="preserve">
453+
<value>Remote certificate is already available.</value>
454+
</data>
455+
<data name="net_ssl_renegotiate_data" xml:space="preserve">
456+
<value>Received data during renegotiation.</value>
457+
</data>
452458
</root>

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,15 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan<byte> inputBuffer, ref byte
830830
return status;
831831
}
832832

833+
internal SecurityStatusPal Renegotiate(out byte[]? output)
834+
{
835+
return SslStreamPal.Renegotiate(
836+
ref _credentialsHandle!,
837+
ref _securityContext,
838+
_sslAuthenticationOptions,
839+
out output);
840+
}
841+
833842
/*++
834843
ProcessHandshakeSuccess -
835844
Called on successful completion of Handshake -

src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ private enum Framing
2626
BeforeSSL3, // SSlv2
2727
SinceSSL3, // SSlv3 & TLS
2828
Unified, // Intermediate on first frame until response is processes.
29-
Invalid // Somthing is wrong.
29+
Invalid // Something is wrong.
3030
}
3131

3232
// This is set on the first packet to figure out the framing style.
@@ -305,6 +305,59 @@ private async Task ReplyOnReAuthenticationAsync<TIOAdapter>(TIOAdapter adapter,
305305
}
306306
}
307307

308+
// This will initiate renegotiation or PHA for Tls1.3
309+
private async Task RenegotiateAsync(CancellationToken cancellationToken)
310+
{
311+
if (Interlocked.Exchange(ref _nestedAuth, 1) == 1)
312+
{
313+
throw new InvalidOperationException(SR.Format(SR.net_io_invalidnestedcall, "NegotiateClientCertificateAsync", "renegotiate"));
314+
}
315+
316+
if (Interlocked.Exchange(ref _nestedRead, 1) == 1)
317+
{
318+
throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, nameof(SslStream.ReadAsync), "read"));
319+
}
320+
321+
if (Interlocked.Exchange(ref _nestedWrite, 1) == 1)
322+
{
323+
_nestedRead = 0;
324+
throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, nameof(WriteAsync), "write"));
325+
}
326+
327+
_sslAuthenticationOptions!.RemoteCertRequired = true;
328+
IReadWriteAdapter adapter = new AsyncReadWriteAdapter(InnerStream, cancellationToken);
329+
330+
try
331+
{
332+
SecurityStatusPal status = _context!.Renegotiate(out byte[]? nextmsg);
333+
if (nextmsg?.Length > 0)
334+
{
335+
await adapter.WriteAsync(nextmsg, 0, nextmsg.Length).ConfigureAwait(false);
336+
await adapter.FlushAsync().ConfigureAwait(false);
337+
}
338+
339+
if (status.ErrorCode != SecurityStatusPalErrorCode.OK)
340+
{
341+
if (status.ErrorCode == SecurityStatusPalErrorCode.NoRenegotiation)
342+
{
343+
// peer does not want to renegotiate. That should keep session usable.
344+
return;
345+
}
346+
347+
throw SslStreamPal.GetException(status);
348+
}
349+
350+
// Issue empty read to get renegotiation going.
351+
await ReadAsyncInternal(adapter, Memory<byte>.Empty, renegotiation: true).ConfigureAwait(false);
352+
}
353+
finally
354+
{
355+
_nestedRead = 0;
356+
_nestedWrite = 0;
357+
// We will not release _nestedAuth at this point to prevent another renegotiation attempt.
358+
}
359+
}
360+
308361
// reAuthenticationData is only used on Windows in case of renegotiation.
309362
private async Task ForceAuthenticationAsync<TIOAdapter>(TIOAdapter adapter, bool receiveFirst, byte[]? reAuthenticationData, bool isApm = false)
310363
where TIOAdapter : IReadWriteAdapter
@@ -612,6 +665,15 @@ private bool CompleteHandshake(ref ProtocolToken? alertToken, out SslPolicyError
612665
{
613666
_context!.ProcessHandshakeSuccess();
614667

668+
if (_nestedAuth != 1)
669+
{
670+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Ignoring unsolicited renegotiated certificate.");
671+
// ignore certificates received outside of handshake or requested renegotiation.
672+
sslPolicyErrors = SslPolicyErrors.None;
673+
chainStatus = X509ChainStatusFlags.NoError;
674+
return true;
675+
}
676+
615677
if (!_context.VerifyRemoteCertificate(_sslAuthenticationOptions!.CertValidationDelegate, ref alertToken, out sslPolicyErrors, out chainStatus))
616678
{
617679
_handshakeCompleted = false;
@@ -753,12 +815,15 @@ private void ReturnReadBufferIfEmpty()
753815
}
754816
}
755817

756-
private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(TIOAdapter adapter, Memory<byte> buffer)
818+
private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(TIOAdapter adapter, Memory<byte> buffer, bool renegotiation = false)
757819
where TIOAdapter : IReadWriteAdapter
758820
{
759-
if (Interlocked.Exchange(ref _nestedRead, 1) == 1)
821+
if (!renegotiation)
760822
{
761-
throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, nameof(SslStream.ReadAsync), "read"));
823+
if (Interlocked.Exchange(ref _nestedRead, 1) == 1)
824+
{
825+
throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, nameof(SslStream.ReadAsync), "read"));
826+
}
762827
}
763828

764829
Debug.Assert(_internalBuffer is null || _internalBufferCount > 0 || _decryptedBytesCount > 0, "_internalBuffer allocated when no data is buffered.");
@@ -769,10 +834,15 @@ private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(TIOAdapter adapter, M
769834
{
770835
if (_decryptedBytesCount != 0)
771836
{
837+
if (renegotiation)
838+
{
839+
throw new InvalidOperationException(SR.net_ssl_renegotiate_data);
840+
}
841+
772842
return CopyDecryptedData(buffer);
773843
}
774844

775-
if (buffer.Length == 0 && _internalBuffer is null)
845+
if (buffer.Length == 0 && _internalBuffer is null && !renegotiation)
776846
{
777847
// User requested a zero-byte read, and we have no data available in the buffer for processing.
778848
// This zero-byte read indicates their desire to trade off the extra cost of a zero-byte read
@@ -844,7 +914,6 @@ private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(TIOAdapter adapter, M
844914
// If that happen before EncryptData() runs, _handshakeWaiter will be set to null
845915
// and EncryptData() will work normally e.g. no waiting, just exclusion with DecryptData()
846916

847-
848917
if (_sslAuthenticationOptions!.AllowRenegotiation || SslProtocol == SslProtocols.Tls13)
849918
{
850919
// create TCS only if we plan to proceed. If not, we will throw in block bellow outside of the lock.
@@ -880,8 +949,12 @@ private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(TIOAdapter adapter, M
880949
{
881950
throw new IOException(SR.net_ssl_io_renego);
882951
}
883-
884952
await ReplyOnReAuthenticationAsync(adapter, extraBuffer).ConfigureAwait(false);
953+
if (renegotiation)
954+
{
955+
// if we received data frame instead, we would not be here but we would decrypt data and hit check above.
956+
return 0;
957+
}
885958
// Loop on read.
886959
continue;
887960
}
@@ -897,7 +970,7 @@ private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(TIOAdapter adapter, M
897970
}
898971
catch (Exception e)
899972
{
900-
if (e is IOException || (e is OperationCanceledException && adapter.CancellationToken.IsCancellationRequested))
973+
if (e is IOException || (e is OperationCanceledException && adapter.CancellationToken.IsCancellationRequested) || renegotiation)
901974
{
902975
throw;
903976
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,17 @@ public override long Position
689689

690690
public override Task FlushAsync(CancellationToken cancellationToken) => InnerStream.FlushAsync(cancellationToken);
691691

692+
public virtual Task NegotiateClientCertificateAsync(CancellationToken cancellationToken = default)
693+
{
694+
ThrowIfExceptionalOrNotAuthenticated();
695+
if (RemoteCertificate != null)
696+
{
697+
throw new InvalidOperationException(SR.net_ssl_certificate_exist);
698+
}
699+
700+
return RenegotiateAsync(cancellationToken);
701+
}
702+
692703
protected override void Dispose(bool disposing)
693704
{
694705
try

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public static SecurityStatusPal InitializeSecurityContext(
4545
return HandshakeInternal(credential, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
4646
}
4747

48+
public static SecurityStatusPal Renegotiate(ref SafeFreeCredentials? credentialsHandle, ref SafeDeleteSslContext? context, SslAuthenticationOptions sslAuthenticationOptions, out byte[]? outputBuffer)
49+
{
50+
throw new PlatformNotSupportedException();
51+
}
52+
4853
public static SafeFreeCredentials AcquireCredentialsHandle(
4954
SslStreamCertificateContext? certificateContext,
5055
SslProtocols protocols,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public static SecurityStatusPal InitializeSecurityContext(
5252
return HandshakeInternal(credential, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
5353
}
5454

55+
public static SecurityStatusPal Renegotiate(ref SafeFreeCredentials? credentialsHandle, ref SafeDeleteSslContext? context, SslAuthenticationOptions sslAuthenticationOptions, out byte[]? outputBuffer)
56+
{
57+
throw new PlatformNotSupportedException();
58+
}
59+
5560
public static SafeFreeCredentials AcquireCredentialsHandle(
5661
SslStreamCertificateContext? certificateContext,
5762
SslProtocols protocols,

0 commit comments

Comments
 (0)