From 77ad8064a2c2d3557eadbee9fdcf24083d4d23e5 Mon Sep 17 00:00:00 2001 From: Filip Navara Date: Fri, 16 Jun 2023 12:47:47 +0200 Subject: [PATCH] Reimplement NegotiateStream using (mostly) public NegotiateAuthenticaton API (#86948) * Reimplement NegotiateStream using (mostly) public NegotiateAuthentication API * Minor adjustment to NegotiateStreamPal on Android/tvOS to allow NegotiateStream to work * Remove unrelated ApiCompat suppression changes --- .../src/Interop/Windows/SspiCli/Interop.SSPI.cs | 14 + .../src/System/Net/NTAuthentication.Common.cs | 30 +- .../src/System/Net/NTAuthentication.Managed.cs | 31 +- .../System/Net/Security/NegotiateStreamPal.Unix.cs | 139 +++---- .../Net/Security/NegotiateStreamPal.Windows.cs | 256 ++++--------- .../System.Net.Security/ref/System.Net.Security.cs | 1 - .../src/System.Net.Security.csproj | 6 +- .../System/Net/Security/NegotiateAuthentication.cs | 48 ++- .../src/System/Net/Security/NegotiateStream.cs | 420 ++++++++++----------- ...tSupported.cs => NegotiateStreamPal.Managed.cs} | 12 +- .../ApiCompatBaseline.NetCoreAppLatestStable.xml | 18 + 11 files changed, 426 insertions(+), 549 deletions(-) rename src/libraries/System.Net.Security/src/System/Net/Security/{NegotiateStreamPal.PlatformNotSupported.cs => NegotiateStreamPal.Managed.cs} (76%) diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs index eaaf058..3d7c146 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -362,6 +362,20 @@ internal static partial class Interop uint* qualityOfProtection); [LibraryImport(Interop.Libraries.SspiCli, SetLastError = true)] + internal static partial int MakeSignature( + ref CredHandle contextHandle, + uint qualityOfProtection, + ref SecBufferDesc inputOutput, + uint sequenceNumber); + + [LibraryImport(Interop.Libraries.SspiCli, SetLastError = true)] + internal static unsafe partial int VerifySignature( + ref CredHandle contextHandle, + in SecBufferDesc input, + uint sequenceNumber, + uint *qualityOfProtection); + + [LibraryImport(Interop.Libraries.SspiCli, SetLastError = true)] internal static partial int QuerySecurityContextToken( ref CredHandle phContext, out SecurityContextTokenHandle handle); diff --git a/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs b/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs index d40d00d..26351a4 100644 --- a/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs +++ b/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs @@ -164,6 +164,16 @@ namespace System.Net return NegotiateStreamPal.UnwrapInPlace(_securityContext!, input, out unwrappedOffset, out unwrappedLength, out wasEncrypted); } + internal bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) + { + return NegotiateStreamPal.VerifyMIC(_securityContext!, (_contextFlags & ContextFlagsPal.Confidentiality) != 0, message, signature); + } + + internal void GetMIC(ReadOnlySpan message, IBufferWriter signature) + { + NegotiateStreamPal.GetMIC(_securityContext!, (_contextFlags & ContextFlagsPal.Confidentiality) != 0, message, signature); + } + internal string? GetOutgoingBlob(string? incomingBlob) { return GetOutgoingBlob(incomingBlob, throwOnError: true, out _); @@ -329,25 +339,5 @@ namespace System.Net return spn; } - - internal int Encrypt(ReadOnlySpan buffer, [NotNull] ref byte[]? output) - { - return NegotiateStreamPal.Encrypt( - _securityContext!, - buffer, - (_contextFlags & ContextFlagsPal.Confidentiality) != 0, - IsNTLM, - ref output); - } - - internal int Decrypt(Span payload, out int newOffset) - { - return NegotiateStreamPal.Decrypt( - _securityContext!, - payload, - (_contextFlags & ContextFlagsPal.Confidentiality) != 0, - IsNTLM, - out newOffset); - } } } diff --git a/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs b/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs index daf50f6..bcbeb54 100644 --- a/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs +++ b/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs @@ -796,7 +796,7 @@ namespace System.Net } } - private bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) + internal bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) { // Check length and version if (signature.Length != SignatureLength || @@ -815,10 +815,21 @@ namespace System.Net return signature.SequenceEqual(expectedSignature); } + internal void GetMIC(ReadOnlySpan message, IBufferWriter signature) + { + Debug.Assert(_clientSeal is not null); + Debug.Assert(_clientSigningKey is not null); + + Span signatureBuffer = signature.GetSpan(SignatureLength); + CalculateSignature(message, _clientSequenceNumber, _clientSigningKey, _clientSeal, signatureBuffer); + _clientSequenceNumber++; + signature.Advance(SignatureLength); + } + private byte[] GetMIC(ReadOnlySpan message) { - Debug.Assert(_clientSeal != null); - Debug.Assert(_clientSigningKey != null); + Debug.Assert(_clientSeal is not null); + Debug.Assert(_clientSigningKey is not null); byte[] signature = new byte[SignatureLength]; CalculateSignature(message, _clientSequenceNumber, _clientSigningKey, _clientSeal, signature); @@ -1084,19 +1095,9 @@ namespace System.Net return NegotiateAuthenticationStatusCode.Completed; } -#pragma warning disable CA1822, IDE0060 - internal int Encrypt(ReadOnlySpan buffer, [NotNull] ref byte[]? output) - { - throw new PlatformNotSupportedException(); - } - - internal int Decrypt(Span payload, out int newOffset) - { - throw new PlatformNotSupportedException(); - } - internal string ProtocolName => _isSpNego ? NegotiationInfoClass.Negotiate : NegotiationInfoClass.NTLM; +#pragma warning disable CA1822, IDE0060 internal bool IsNTLM => true; internal bool IsKerberos => false; @@ -1104,8 +1105,8 @@ namespace System.Net internal bool IsServer { get; set; } internal bool IsValidContext => true; +#pragma warning restore CA1822, IDE0060 internal string? ClientSpecifiedSpn => _spn; -#pragma warning restore CA1822, IDE0060 } } diff --git a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs index c978c80..3dd3dac 100644 --- a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs @@ -499,100 +499,6 @@ namespace System.Net.Security } #pragma warning restore IDE0060 - internal static int Encrypt( - SafeDeleteContext securityContext, - ReadOnlySpan buffer, - bool isConfidential, - bool isNtlm, - [NotNull] ref byte[]? output) - { - SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; - int resultSize; - - if (isNtlm && !isConfidential) - { - Interop.NetSecurityNative.GssBuffer micBuffer = default; - try - { - Interop.NetSecurityNative.Status minorStatus; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.GetMic( - out minorStatus, - gssContext, - buffer, - ref micBuffer); - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); - } - - resultSize = micBuffer.Span.Length + buffer.Length; - if (output == null || output.Length < resultSize + 4) - { - output = new byte[resultSize + 4]; - } - - micBuffer.Span.CopyTo(output.AsSpan(4)); - buffer.CopyTo(output.AsSpan(micBuffer.Span.Length + 4)); - BinaryPrimitives.WriteInt32LittleEndian(output, resultSize); - - return resultSize + 4; - } - finally - { - micBuffer.Dispose(); - } - } - - byte[] tempOutput = GssWrap(gssContext, ref isConfidential, buffer); - - // Create space for prefixing with the length - const int prefixLength = 4; - output = new byte[tempOutput.Length + prefixLength]; - Array.Copy(tempOutput, 0, output, prefixLength, tempOutput.Length); - resultSize = tempOutput.Length; - BinaryPrimitives.WriteInt32LittleEndian(output, resultSize); - - return resultSize + 4; - } - - internal static int Decrypt( - SafeDeleteContext securityContext, - Span buffer, - bool isConfidential, - bool isNtlm, - out int newOffset) - { - SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; - - if (isNtlm && !isConfidential) - { - const int NtlmSignatureLength = 16; - - if (buffer.Length < NtlmSignatureLength) - { - Debug.Fail("Argument 'count' out of range."); - throw new Interop.NetSecurityNative.GssApiException(Interop.NetSecurityNative.Status.GSS_S_DEFECTIVE_TOKEN, 0); - } - - Interop.NetSecurityNative.Status minorStatus; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.VerifyMic( - out minorStatus, - gssContext, - buffer.Slice(NtlmSignatureLength), - buffer.Slice(0, NtlmSignatureLength)); - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); - } - - newOffset = NtlmSignatureLength; - return buffer.Length - NtlmSignatureLength; - } - - newOffset = 0; - return GssUnwrap(gssContext, out _, buffer); - } - internal static NegotiateAuthenticationStatusCode Unwrap( SafeDeleteContext securityContext, ReadOnlySpan input, @@ -692,7 +598,52 @@ namespace System.Net.Security { encryptedBuffer.Dispose(); } + } + internal static bool VerifyMIC( + SafeDeleteContext securityContext, + bool isConfidential, + ReadOnlySpan message, + ReadOnlySpan signature) + { + _ = isConfidential; + SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.VerifyMic( + out _, + gssContext, + message, + signature); + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; + } + + internal static void GetMIC( + SafeDeleteContext securityContext, + bool isConfidential, + ReadOnlySpan message, + IBufferWriter signature) + { + _ = isConfidential; + SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; + Interop.NetSecurityNative.GssBuffer micBuffer = default; + try + { + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.GetMic( + out minorStatus, + gssContext, + message, + ref micBuffer); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + signature.Write(micBuffer.Span); + } + finally + { + micBuffer.Dispose(); + } } } } diff --git a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Windows.cs b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Windows.cs index 7fe7bbb..2b42c70 100644 --- a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Windows.cs +++ b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Windows.cs @@ -328,216 +328,120 @@ namespace System.Net.Security } } - internal static unsafe int Encrypt( + internal static unsafe bool VerifyMIC( SafeDeleteContext securityContext, - ReadOnlySpan buffer, bool isConfidential, - bool isNtlm, - [NotNull] ref byte[]? output) + ReadOnlySpan message, + ReadOnlySpan signature) { - SecPkgContext_Sizes sizes = default; - bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SIZES, ref sizes); - Debug.Assert(success); - - int maxCount = checked(int.MaxValue - 4 - sizes.cbBlockSize - sizes.cbSecurityTrailer); - if (buffer.Length > maxCount) - { - throw new ArgumentOutOfRangeException(nameof(buffer.Length), SR.Format(SR.net_io_out_range, maxCount)); - } + bool refAdded = false; - int resultSize = buffer.Length + sizes.cbSecurityTrailer + sizes.cbBlockSize; - if (output == null || output.Length < resultSize + 4) - { - output = new byte[resultSize + 4]; - } - - // Make a copy of user data for in-place encryption. - buffer.CopyTo(output.AsSpan(4 + sizes.cbSecurityTrailer)); - - fixed (byte* outputPtr = output) + try { - // Prepare buffers TOKEN(signature), DATA and Padding. - Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[3]; - Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; - Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; - Interop.SspiCli.SecBuffer* paddingBuffer = &unmanagedBuffer[2]; - tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; - tokenBuffer->pvBuffer = (IntPtr)(outputPtr + 4); - tokenBuffer->cbBuffer = sizes.cbSecurityTrailer; - dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; - dataBuffer->pvBuffer = (IntPtr)(outputPtr + 4 + sizes.cbSecurityTrailer); - dataBuffer->cbBuffer = buffer.Length; - paddingBuffer->BufferType = SecurityBufferType.SECBUFFER_PADDING; - paddingBuffer->pvBuffer = (IntPtr)(outputPtr + 4 + sizes.cbSecurityTrailer + buffer.Length); - paddingBuffer->cbBuffer = sizes.cbBlockSize; - - Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(3) - { - pBuffers = unmanagedBuffer - }; + securityContext.DangerousAddRef(ref refAdded); - if (isNtlm && !isConfidential) + fixed (byte* messagePtr = message) + fixed (byte* signaturePtr = signature) { - dataBuffer->BufferType |= SecurityBufferType.SECBUFFER_READONLY; - } + Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; + Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; + Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; + tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; + tokenBuffer->pvBuffer = (IntPtr)signaturePtr; + tokenBuffer->cbBuffer = signature.Length; + dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; + dataBuffer->pvBuffer = (IntPtr)messagePtr; + dataBuffer->cbBuffer = message.Length; + + Interop.SspiCli.SecBufferDesc sdcIn = new Interop.SspiCli.SecBufferDesc(2) + { + pBuffers = unmanagedBuffer + }; - uint qop = isConfidential ? 0 : Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; - int errorCode = GlobalSSPI.SSPIAuth.EncryptMessage(securityContext, ref sdcInOut, qop); + uint qop; + int errorCode = Interop.SspiCli.VerifySignature(ref securityContext._handle, in sdcIn, 0, &qop); - if (errorCode != 0) - { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw new Win32Exception(errorCode); - } + if (errorCode != 0) + { + Exception e = new Win32Exception(errorCode); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); + throw new Win32Exception(errorCode); + } - // Compacting the result. - resultSize = tokenBuffer->cbBuffer; - bool forceCopy = false; - if (resultSize != sizes.cbSecurityTrailer) - { - forceCopy = true; - output.AsSpan(4 + sizes.cbSecurityTrailer, dataBuffer->cbBuffer).CopyTo(output.AsSpan(4 + resultSize, dataBuffer->cbBuffer)); - } + if (isConfidential && qop == Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT) + { + Debug.Fail($"Expected qop = 0, returned value = {qop}"); + throw new InvalidOperationException(SR.net_auth_message_not_encrypted); + } - resultSize += dataBuffer->cbBuffer; - if (paddingBuffer->cbBuffer != 0 && (forceCopy || resultSize != (buffer.Length + sizes.cbSecurityTrailer))) - { - output.AsSpan(4 + sizes.cbSecurityTrailer + buffer.Length, paddingBuffer->cbBuffer).CopyTo(output.AsSpan(4 + resultSize, paddingBuffer->cbBuffer)); + return true; } - - resultSize += paddingBuffer->cbBuffer; - BinaryPrimitives.WriteInt32LittleEndian(output, resultSize); - - return resultSize + 4; - } - } - - internal static unsafe int Decrypt( - SafeDeleteContext securityContext, - Span buffer, - bool isConfidential, - bool isNtlm, - out int newOffset) - { - if (isNtlm) - { - return DecryptNtlm(securityContext, buffer, isConfidential, out newOffset); } - - // - // Kerberos and up - // - fixed (byte* bufferPtr = buffer) + finally { - Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; - Interop.SspiCli.SecBuffer* streamBuffer = &unmanagedBuffer[0]; - Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; - streamBuffer->BufferType = SecurityBufferType.SECBUFFER_STREAM; - streamBuffer->pvBuffer = (IntPtr)bufferPtr; - streamBuffer->cbBuffer = buffer.Length; - dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; - dataBuffer->pvBuffer = IntPtr.Zero; - dataBuffer->cbBuffer = 0; - - Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) - { - pBuffers = unmanagedBuffer - }; - - uint qop; - int errorCode = GlobalSSPI.SSPIAuth.DecryptMessage(securityContext, ref sdcInOut, out qop); - - if (errorCode != 0) + if (refAdded) { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw new Win32Exception(errorCode); + securityContext.DangerousRelease(); } - - if (qop == Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT && isConfidential) - { - Debug.Fail($"Expected qop = 0, returned value = {qop}"); - throw new InvalidOperationException(SR.net_auth_message_not_encrypted); - } - - if (dataBuffer->BufferType != SecurityBufferType.SECBUFFER_DATA) - { - throw new InternalException(dataBuffer->BufferType); - } - - Debug.Assert((nint)dataBuffer->pvBuffer >= (nint)bufferPtr); - Debug.Assert((nint)dataBuffer->pvBuffer + dataBuffer->cbBuffer <= (nint)bufferPtr + buffer.Length); - newOffset = (int)((byte*)dataBuffer->pvBuffer - bufferPtr); - return dataBuffer->cbBuffer; } } - private static unsafe int DecryptNtlm( + internal static unsafe void GetMIC( SafeDeleteContext securityContext, - Span buffer, bool isConfidential, - out int newOffset) + ReadOnlySpan message, + IBufferWriter signature) { - const int NtlmSignatureLength = 16; + bool refAdded = false; - // For the most part the arguments are verified in Decrypt(). - if (buffer.Length < NtlmSignatureLength) + try { - Debug.Fail("Argument 'count' out of range."); - throw new Win32Exception((int)Interop.SECURITY_STATUS.InvalidToken); - } + securityContext.DangerousAddRef(ref refAdded); - fixed (byte* bufferPtr = buffer) - { - SecurityBufferType realDataType = SecurityBufferType.SECBUFFER_DATA; - Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; - Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; - Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; - tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; - tokenBuffer->pvBuffer = (IntPtr)bufferPtr; - tokenBuffer->cbBuffer = NtlmSignatureLength; - dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; - dataBuffer->pvBuffer = (IntPtr)(bufferPtr + NtlmSignatureLength); - dataBuffer->cbBuffer = buffer.Length - NtlmSignatureLength; + SecPkgContext_Sizes sizes = default; + bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SIZES, ref sizes); + Debug.Assert(success); - Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) - { - pBuffers = unmanagedBuffer - }; - uint qop; - int errorCode; + Span signatureBuffer = signature.GetSpan(sizes.cbSecurityTrailer); - if (!isConfidential) + fixed (byte* messagePtr = message) + fixed (byte* signaturePtr = signatureBuffer) { - realDataType |= SecurityBufferType.SECBUFFER_READONLY; - dataBuffer->BufferType = realDataType; - } + // Prepare buffers TOKEN(signature), DATA. + Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; + Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; + Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; + tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; + tokenBuffer->pvBuffer = (IntPtr)signaturePtr; + tokenBuffer->cbBuffer = sizes.cbSecurityTrailer; + dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; + dataBuffer->pvBuffer = (IntPtr)messagePtr; + dataBuffer->cbBuffer = message.Length; + + Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) + { + pBuffers = unmanagedBuffer + }; - errorCode = GlobalSSPI.SSPIAuth.DecryptMessage(securityContext, ref sdcInOut, out qop); + uint qop = isConfidential ? 0 : Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; + int errorCode = Interop.SspiCli.MakeSignature(ref securityContext._handle, qop, ref sdcInOut, 0); - if (errorCode != 0) - { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw new Win32Exception(errorCode); - } + if (errorCode != 0) + { + Exception e = new Win32Exception(errorCode); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); + throw new Win32Exception(errorCode); + } - if (isConfidential && qop == Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT) - { - Debug.Fail($"Expected qop = 0, returned value = {qop}"); - throw new InvalidOperationException(SR.net_auth_message_not_encrypted); + signature.Advance(signatureBuffer.Length); } - - if (dataBuffer->BufferType != realDataType) + } + finally + { + if (refAdded) { - throw new InternalException(dataBuffer->BufferType); + securityContext.DangerousRelease(); } - - Debug.Assert((nint)dataBuffer->pvBuffer >= (nint)bufferPtr); - Debug.Assert((nint)dataBuffer->pvBuffer + dataBuffer->cbBuffer <= (nint)bufferPtr + buffer.Length); - newOffset = (int)((byte*)dataBuffer->pvBuffer - bufferPtr); - return dataBuffer->cbBuffer; } } } diff --git a/src/libraries/System.Net.Security/ref/System.Net.Security.cs b/src/libraries/System.Net.Security/ref/System.Net.Security.cs index 9fb9b37..87bdf17 100644 --- a/src/libraries/System.Net.Security/ref/System.Net.Security.cs +++ b/src/libraries/System.Net.Security/ref/System.Net.Security.cs @@ -98,7 +98,6 @@ namespace System.Net.Security TargetUnknown = 14, ImpersonationValidationFailed = 15, } - [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] public partial class NegotiateStream : System.Net.Security.AuthenticatedStream { public NegotiateStream(System.IO.Stream innerStream) : base (default(System.IO.Stream), default(bool)) { } diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index 2de2f39..bfedc0d 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -111,6 +111,8 @@ Link="Common\System\HexConverter.cs" /> + @@ -221,8 +223,6 @@ Link="Common\Interop\Windows\SChannel\Interop.Alerts.cs" /> - - + /// Input message to be wrapped. - /// Buffer writter where the wrapped message is written. + /// Buffer writer where the wrapped message is written. /// Specifies whether encryption is requested. /// Specifies whether encryption was applied in the wrapping. /// @@ -425,7 +426,7 @@ namespace System.Net.Security /// Unwrap an input message with signature or encryption applied by the other party. /// /// Input message to be unwrapped. - /// Buffer writter where the unwrapped message is written. + /// Buffer writer where the unwrapped message is written. /// /// On output specifies whether the wrapped message had encryption applied. /// @@ -476,6 +477,49 @@ namespace System.Net.Security return _ntAuthentication.UnwrapInPlace(input, out unwrappedOffset, out unwrappedLength, out wasEncrypted); } + /// + /// Computes the message integrity check of a given message. + /// + /// Input message for MIC calculation. + /// Buffer writer where the MIC is written. + /// + /// The method modifies the internal state and may update sequence numbers depending on the + /// selected algorithm. Two successive invocations thus don't produce the same result and + /// it's important to carefully pair GetMIC and VerifyMIC calls on the both sides of the + /// authenticated session. + /// + internal void GetMIC(ReadOnlySpan message, IBufferWriter signature) + { + if (!IsAuthenticated || _ntAuthentication == null) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + _ntAuthentication.GetMIC(message, signature); + } + + /// + /// Verifies the message integrity check of a given message. + /// + /// Input message for MIC calculation. + /// MIC to be verified. + /// For successfully verified MIC, the method returns true. + /// + /// The method modifies the internal state and may update sequence numbers depending on the + /// selected algorithm. Two successive invocations thus don't produce the same result and + /// it's important to carefully pair GetMIC and VerifyMIC calls on the both sides of the + /// authenticated session. + /// + internal bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) + { + if (!IsAuthenticated || _ntAuthentication == null) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + return _ntAuthentication.VerifyMIC(message, signature); + } + private bool CheckSpn() { Debug.Assert(_ntAuthentication != null); diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs index efed209..19022bd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Buffers.Binary; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -19,7 +21,6 @@ namespace System.Net.Security /// /// Provides a stream that uses the Negotiate security protocol to authenticate the client, and optionally the server, in client-server communication. /// - [UnsupportedOSPlatform("tvos")] public partial class NegotiateStream : AuthenticatedStream { /// Set as the _exception when the instance is disposed. @@ -34,12 +35,12 @@ namespace System.Net.Security private static readonly byte[] s_emptyMessage = new byte[0]; #pragma warning restore CA1825 + private readonly byte[] _writeHeader; private readonly byte[] _readHeader; - private IIdentity? _remoteIdentity; private byte[] _readBuffer; private int _readBufferOffset; private int _readBufferCount; - private byte[]? _writeBuffer; + private ArrayBufferWriter? _writeBuffer; private volatile int _writeInProgress; private volatile int _readInProgress; @@ -47,12 +48,14 @@ namespace System.Net.Security private ExceptionDispatchInfo? _exception; private StreamFramer? _framer; - private NTAuthentication? _context; + private NegotiateAuthentication? _context; private bool _canRetryAuthentication; private ProtectionLevel _expectedProtectionLevel; private TokenImpersonationLevel _expectedImpersonationLevel; private ExtendedProtectionPolicy? _extendedProtectionPolicy; + private bool isNtlm; + /// /// SSPI does not send a server ack on successful auth. /// This is a state variable used to gracefully handle auth confirmation. @@ -65,6 +68,7 @@ namespace System.Net.Security public NegotiateStream(Stream innerStream, bool leaveInnerStreamOpen) : base(innerStream, leaveInnerStreamOpen) { + _writeHeader = new byte[4]; _readHeader = new byte[4]; _readBuffer = Array.Empty(); } @@ -74,7 +78,7 @@ namespace System.Net.Security try { _exception = s_disposedSentinel; - _context?.CloseContext(); + _context?.Dispose(); } finally { @@ -87,7 +91,7 @@ namespace System.Net.Security try { _exception = s_disposedSentinel; - _context?.CloseContext(); + _context?.Dispose(); } finally { @@ -219,12 +223,12 @@ namespace System.Net.Security public override bool IsMutuallyAuthenticated => IsAuthenticatedCore && - !_context.IsNTLM && // suppressing for NTLM since SSPI does not return correct value in the context flags. - _context.IsMutualAuthFlag; + !string.Equals(_context.Package, NegotiationInfoClass.NTLM) && // suppressing for NTLM since SSPI does not return correct value in the context flags. + _context.IsMutuallyAuthenticated; - public override bool IsEncrypted => IsAuthenticatedCore && _context.IsConfidentialityFlag; + public override bool IsEncrypted => IsAuthenticatedCore && _context.IsEncrypted; - public override bool IsSigned => IsAuthenticatedCore && (_context.IsIntegrityFlag || _context.IsConfidentialityFlag); + public override bool IsSigned => IsAuthenticatedCore && (_context.IsSigned || _context.IsEncrypted); public override bool IsServer => _context != null && _context.IsServer; @@ -237,26 +241,18 @@ namespace System.Net.Security } } - private TokenImpersonationLevel PrivateImpersonationLevel => - _context!.IsDelegationFlag && _context.ProtocolName != NegotiationInfoClass.NTLM ? TokenImpersonationLevel.Delegation : // We should suppress the delegate flag in NTLM case. - _context.IsIdentifyFlag ? TokenImpersonationLevel.Identification : - TokenImpersonationLevel.Impersonation; + private TokenImpersonationLevel PrivateImpersonationLevel => _context!.ImpersonationLevel; - private bool HandshakeComplete => _context!.IsCompleted && _context.IsValidContext; + private bool HandshakeComplete => _context!.IsAuthenticated; - private bool CanGetSecureStream => _context!.IsConfidentialityFlag || _context.IsIntegrityFlag; + private bool CanGetSecureStream => _context!.IsEncrypted || _context.IsSigned; public virtual IIdentity RemoteIdentity { get { - IIdentity? identity = _remoteIdentity; - if (identity is null) - { - ThrowIfFailed(authSuccessCheck: true); - _remoteIdentity = identity = NegotiateStreamPal.GetIdentity(_context!); - } - return identity; + ThrowIfFailed(authSuccessCheck: true); + return _context!.RemoteIdentity; } } @@ -351,6 +347,8 @@ namespace System.Net.Security try { + ThrowIfFailed(authSuccessCheck: true); + if (_readBufferCount != 0) { int copyBytes = Math.Min(_readBufferCount, buffer.Length); @@ -372,7 +370,7 @@ namespace System.Net.Security } // Replace readBytes with the body size recovered from the header content. - readBytes = BitConverter.ToInt32(_readHeader, 0); + readBytes = BinaryPrimitives.ReadInt32LittleEndian(_readHeader); // The body carries 4 bytes for trailer size slot plus trailer, hence <= 4 frame size is always an error. // Additionally we'd like to restrict the read frame size to 64k. @@ -392,25 +390,49 @@ namespace System.Net.Security readBytes = await ReadAllAsync(InnerStream, new Memory(_readBuffer, 0, readBytes), allowZeroRead: false, cancellationToken).ConfigureAwait(false); - // Decrypt into internal buffer, change "readBytes" to count now _Decrypted Bytes_ - // Decrypted data start from zero offset, the size can be shrunk after decryption. - _readBufferCount = readBytes = _context.Decrypt(_readBuffer.AsSpan(0, readBytes), out _readBufferOffset); - if (readBytes == 0 && buffer.Length != 0) + // Decrypt into the same buffer (decrypted data size can be shrunk after decryption). + NegotiateAuthenticationStatusCode statusCode; + if (isNtlm && !_context.IsEncrypted) { - // Read again. - continue; + // Non-encrypted NTLM uses an encoding quirk + const int NtlmSignatureLength = 16; + + if (readBytes < NtlmSignatureLength || + !_context.VerifyMIC(_readBuffer.AsSpan(NtlmSignatureLength, readBytes - NtlmSignatureLength), _readBuffer.AsSpan(0, NtlmSignatureLength))) + { + statusCode = NegotiateAuthenticationStatusCode.InvalidToken; + } + else + { + _readBufferOffset = NtlmSignatureLength; + _readBufferCount = readBytes - NtlmSignatureLength; + statusCode = NegotiateAuthenticationStatusCode.Completed; + } + } + else + { + statusCode = _context.UnwrapInPlace(_readBuffer.AsSpan(0, readBytes), out _readBufferOffset, out _readBufferCount, out _); } - if (readBytes > buffer.Length) + if (statusCode != NegotiateAuthenticationStatusCode.Completed) { - readBytes = buffer.Length; + // TODO: Better exception + throw new IOException(SR.net_io_read); } - _readBuffer.AsMemory(_readBufferOffset, readBytes).CopyTo(buffer); - _readBufferOffset += readBytes; - _readBufferCount -= readBytes; + // Decrypted data can be shrunk after decryption. + if (_readBufferCount == 0 && buffer.Length != 0) + { + // Read again. + continue; + } - return readBytes; + int copyBytes = Math.Min(_readBufferCount, buffer.Length); + _readBuffer.AsMemory(_readBufferOffset, copyBytes).CopyTo(buffer); + _readBufferOffset += copyBytes; + _readBufferCount -= copyBytes; + + return copyBytes; } } catch (Exception e) when (!(e is IOException || e is OperationCanceledException)) @@ -482,6 +504,7 @@ namespace System.Net.Security where TIOAdapter : IReadWriteAdapter { Debug.Assert(_context is not null); + Debug.Assert(_writeBuffer is not null); if (Interlocked.Exchange(ref _writeInProgress, 1) == 1) { @@ -490,21 +513,40 @@ namespace System.Net.Security try { + ThrowIfFailed(authSuccessCheck: true); + while (!buffer.IsEmpty) { int chunkBytes = Math.Min(buffer.Length, MaxWriteDataSize); - int encryptedBytes; - try + + bool isEncrypted = _context.IsEncrypted; + NegotiateAuthenticationStatusCode statusCode; + ReadOnlyMemory bufferToWrap = buffer.Slice(0, chunkBytes); + + if (isNtlm && !isEncrypted) { - encryptedBytes = _context.Encrypt(buffer.Slice(0, chunkBytes).Span, ref _writeBuffer); + // Non-encrypted NTLM uses an encoding quirk + _context.GetMIC(bufferToWrap.Span, _writeBuffer); + _writeBuffer.Write(bufferToWrap.Span); + statusCode = NegotiateAuthenticationStatusCode.Completed; } - catch (Exception e) + else { - throw new IOException(SR.net_io_encrypt, e); + statusCode = _context.Wrap(bufferToWrap.Span, _writeBuffer, isEncrypted, out _); } - await TIOAdapter.WriteAsync(InnerStream, new ReadOnlyMemory(_writeBuffer, 0, encryptedBytes), cancellationToken).ConfigureAwait(false); + if (statusCode != NegotiateAuthenticationStatusCode.Completed) + { + // TODO: Trace the error + throw new IOException(SR.net_io_encrypt); + } + + BinaryPrimitives.WriteInt32LittleEndian(_writeHeader, _writeBuffer.WrittenCount); + await TIOAdapter.WriteAsync(InnerStream, _writeHeader, cancellationToken).ConfigureAwait(false); + + await TIOAdapter.WriteAsync(InnerStream, _writeBuffer.WrittenMemory, cancellationToken).ConfigureAwait(false); buffer = buffer.Slice(chunkBytes); + _writeBuffer.Clear(); } } catch (Exception e) when (!(e is IOException || e is OperationCanceledException)) @@ -513,6 +555,7 @@ namespace System.Net.Security } finally { + _writeBuffer.Clear(); _writeInProgress = 0; } } @@ -589,7 +632,7 @@ namespace System.Net.Security ThrowIfExceptional(); } - if (_context != null && _context.IsValidContext) + if (_context != null) { throw new InvalidOperationException(SR.net_auth_reauth); } @@ -598,7 +641,7 @@ namespace System.Net.Security ArgumentNullException.ThrowIfNull(servicePrincipalName); NegotiateStreamPal.ValidateImpersonationLevel(impersonationLevel); - if (_context != null && IsServer != isServer) + if (_context is not null && IsServer != isServer) { throw new InvalidOperationException(SR.net_auth_client_server); } @@ -608,67 +651,44 @@ namespace System.Net.Security _framer = new StreamFramer(); _framer.WriteHeader.MessageId = FrameHeader.HandshakeId; - _expectedProtectionLevel = protectionLevel; - _expectedImpersonationLevel = isServer ? impersonationLevel : TokenImpersonationLevel.None; - - ContextFlagsPal flags = ContextFlagsPal.Connection; + _canRetryAuthentication = false; // A workaround for the client when talking to Win9x on the server side. if (protectionLevel == ProtectionLevel.None && !isServer) { package = NegotiationInfoClass.NTLM; } - else if (protectionLevel == ProtectionLevel.EncryptAndSign) - { - flags |= ContextFlagsPal.Confidentiality; - } - else if (protectionLevel == ProtectionLevel.Sign) - { - // Assuming user expects NT4 SP4 and above. - flags |= ContextFlagsPal.ReplayDetect | ContextFlagsPal.SequenceDetect | ContextFlagsPal.InitIntegrity; - } if (isServer) { - if (_extendedProtectionPolicy!.PolicyEnforcement == PolicyEnforcement.WhenSupported) - { - flags |= ContextFlagsPal.AllowMissingBindings; - } - - if (_extendedProtectionPolicy.PolicyEnforcement != PolicyEnforcement.Never && - _extendedProtectionPolicy.ProtectionScenario == ProtectionScenario.TrustedProxy) - { - flags |= ContextFlagsPal.ProxyBindings; - } + _expectedProtectionLevel = protectionLevel; + _expectedImpersonationLevel = impersonationLevel; + _context = new NegotiateAuthentication( + new NegotiateAuthenticationServerOptions + { + Package = package, + Credential = credential, + Binding = channelBinding, + RequiredProtectionLevel = protectionLevel, + RequiredImpersonationLevel = impersonationLevel, + Policy = _extendedProtectionPolicy, + }); } else { - // Server side should not request any of these flags. - if (protectionLevel != ProtectionLevel.None) - { - flags |= ContextFlagsPal.MutualAuth; - } - - if (impersonationLevel == TokenImpersonationLevel.Identification) - { - flags |= ContextFlagsPal.InitIdentify; - } - - if (impersonationLevel == TokenImpersonationLevel.Delegation) - { - flags |= ContextFlagsPal.Delegate; - } - } - - _canRetryAuthentication = false; - - try - { - _context = new NTAuthentication(isServer, package, credential, servicePrincipalName, flags, channelBinding!); - } - catch (Win32Exception e) - { - throw new AuthenticationException(SR.net_auth_SSPI, e); + _expectedProtectionLevel = protectionLevel; + _expectedImpersonationLevel = TokenImpersonationLevel.None; + _context = new NegotiateAuthentication( + new NegotiateAuthenticationClientOptions + { + Package = package, + Credential = credential, + TargetName = servicePrincipalName, + Binding = channelBinding, + RequiredProtectionLevel = protectionLevel, + AllowedImpersonationLevel = impersonationLevel, + RequireMutualAuthentication = protectionLevel != ProtectionLevel.None + }); } } @@ -679,7 +699,7 @@ namespace System.Net.Security _exception = ExceptionDispatchInfo.Capture(e); } - _context?.CloseContext(); + _context?.Dispose(); } private void ThrowIfFailed(bool authSuccessCheck) @@ -720,97 +740,46 @@ namespace System.Net.Security } } - private bool CheckSpn() - { - Debug.Assert(_context != null); - - if (_context.IsKerberos || - _extendedProtectionPolicy!.PolicyEnforcement == PolicyEnforcement.Never || - _extendedProtectionPolicy.CustomServiceNames == null) - { - return true; - } - - string? clientSpn = _context.ClientSpecifiedSpn; - - if (string.IsNullOrEmpty(clientSpn)) - { - return _extendedProtectionPolicy.PolicyEnforcement == PolicyEnforcement.WhenSupported; - } - - return _extendedProtectionPolicy.CustomServiceNames.Contains(clientSpn); - } - // Client authentication starts here, but server also loops through this method. private async Task SendBlobAsync(byte[]? message, CancellationToken cancellationToken) where TIOAdapter : IReadWriteAdapter { Debug.Assert(_context != null); - Exception? exception = null; + NegotiateAuthenticationStatusCode statusCode = NegotiateAuthenticationStatusCode.Completed; if (message != s_emptyMessage) { - message = GetOutgoingBlob(message, ref exception); - } - - if (exception != null) - { - // Signal remote side on a failed attempt. - await SendAuthResetSignalAndThrowAsync(message!, exception, cancellationToken).ConfigureAwait(false); - Debug.Fail("Unreachable"); + message = _context.GetOutgoingBlob(message, out statusCode); } - if (HandshakeComplete) + if (statusCode is NegotiateAuthenticationStatusCode.BadBinding or + NegotiateAuthenticationStatusCode.TargetUnknown or + NegotiateAuthenticationStatusCode.ImpersonationValidationFailed or + NegotiateAuthenticationStatusCode.SecurityQosFailed) { - if (_context.IsServer && !CheckSpn()) + Exception exception = statusCode switch { - exception = new AuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch); - int statusCode = ERROR_TRUST_FAILURE; - message = new byte[sizeof(long)]; - - for (int i = message.Length - 1; i >= 0; --i) - { - message[i] = (byte)(statusCode & 0xFF); - statusCode = (int)((uint)statusCode >> 8); - } - - await SendAuthResetSignalAndThrowAsync(message, exception, cancellationToken).ConfigureAwait(false); - Debug.Fail("Unreachable"); - } - - if (PrivateImpersonationLevel < _expectedImpersonationLevel) - { - exception = new AuthenticationException(SR.Format(SR.net_auth_context_expectation, _expectedImpersonationLevel.ToString(), PrivateImpersonationLevel.ToString())); - int statusCode = ERROR_TRUST_FAILURE; - message = new byte[sizeof(long)]; - - for (int i = message.Length - 1; i >= 0; --i) - { - message[i] = (byte)(statusCode & 0xFF); - statusCode = (int)((uint)statusCode >> 8); - } - - await SendAuthResetSignalAndThrowAsync(message, exception, cancellationToken).ConfigureAwait(false); - Debug.Fail("Unreachable"); - } - - ProtectionLevel result = _context.IsConfidentialityFlag ? ProtectionLevel.EncryptAndSign : _context.IsIntegrityFlag ? ProtectionLevel.Sign : ProtectionLevel.None; + NegotiateAuthenticationStatusCode.BadBinding => + new AuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch), + NegotiateAuthenticationStatusCode.TargetUnknown => + new AuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch), + NegotiateAuthenticationStatusCode.ImpersonationValidationFailed => + new AuthenticationException(SR.Format(SR.net_auth_context_expectation, _expectedImpersonationLevel.ToString(), PrivateImpersonationLevel.ToString())), + _ => // NegotiateAuthenticationStatusCode.SecurityQosFailed + new AuthenticationException(SR.Format(SR.net_auth_context_expectation, _context.ProtectionLevel.ToString(), _expectedProtectionLevel.ToString())), + }; - if (result < _expectedProtectionLevel) - { - exception = new AuthenticationException(SR.Format(SR.net_auth_context_expectation, result.ToString(), _expectedProtectionLevel.ToString())); - int statusCode = ERROR_TRUST_FAILURE; - message = new byte[sizeof(long)]; + message = new byte[sizeof(long)]; + BinaryPrimitives.WriteInt64LittleEndian(message, ERROR_TRUST_FAILURE); - for (int i = message.Length - 1; i >= 0; --i) - { - message[i] = (byte)(statusCode & 0xFF); - statusCode = (int)((uint)statusCode >> 8); - } + await SendAuthResetSignalAndThrowAsync(message, exception, cancellationToken).ConfigureAwait(false); + Debug.Fail("Unreachable"); + } + else if (statusCode == NegotiateAuthenticationStatusCode.Completed) + { + _writeBuffer = new ArrayBufferWriter(); - await SendAuthResetSignalAndThrowAsync(message, exception, cancellationToken).ConfigureAwait(false); - Debug.Fail("Unreachable"); - } + isNtlm = string.Equals(_context.Package, NegotiationInfoClass.NTLM); // Signal remote party that we are done _framer!.WriteHeader.MessageId = FrameHeader.HandshakeDoneId; @@ -823,22 +792,58 @@ namespace System.Net.Security // Force signaling server OK to the client message ??= s_emptyMessage; } + + if (message != null) + { + //even if we are completed, there could be a blob for sending. + await _framer!.WriteMessageAsync(InnerStream, message, cancellationToken).ConfigureAwait(false); + } + + if (_remoteOk) + { + // We are done with success. + return; + } } - else if (message == null || message == s_emptyMessage) + else if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded) { - throw new InternalException(); - } + int errorCode = statusCode switch + { + NegotiateAuthenticationStatusCode.BadBinding => (int)Interop.SECURITY_STATUS.BadBinding, + NegotiateAuthenticationStatusCode.Unsupported => (int)Interop.SECURITY_STATUS.Unsupported, + NegotiateAuthenticationStatusCode.MessageAltered => (int)Interop.SECURITY_STATUS.MessageAltered, + NegotiateAuthenticationStatusCode.ContextExpired => (int)Interop.SECURITY_STATUS.ContextExpired, + NegotiateAuthenticationStatusCode.CredentialsExpired => (int)Interop.SECURITY_STATUS.CertExpired, + NegotiateAuthenticationStatusCode.InvalidCredentials => (int)Interop.SECURITY_STATUS.LogonDenied, + NegotiateAuthenticationStatusCode.InvalidToken => (int)Interop.SECURITY_STATUS.InvalidToken, + NegotiateAuthenticationStatusCode.UnknownCredentials => (int)Interop.SECURITY_STATUS.UnknownCredentials, + NegotiateAuthenticationStatusCode.QopNotSupported => (int)Interop.SECURITY_STATUS.QopNotSupported, + NegotiateAuthenticationStatusCode.OutOfSequence => (int)Interop.SECURITY_STATUS.OutOfSequence, + _ => (int)Interop.SECURITY_STATUS.InternalError + }; + Win32Exception win32Exception = new Win32Exception(errorCode); + Exception exception = statusCode switch + { + NegotiateAuthenticationStatusCode.InvalidCredentials => + new InvalidCredentialException(IsServer ? SR.net_auth_bad_client_creds : SR.net_auth_bad_client_creds_or_target_mismatch, win32Exception), + _ => new AuthenticationException(SR.net_auth_SSPI, win32Exception) + }; - if (message != null) - { - //even if we are completed, there could be a blob for sending. - await _framer!.WriteMessageAsync(InnerStream, message, cancellationToken).ConfigureAwait(false); - } + message = new byte[sizeof(long)]; + BinaryPrimitives.WriteInt64LittleEndian(message, errorCode); - if (HandshakeComplete && _remoteOk) + // Signal remote side on a failed attempt. + await SendAuthResetSignalAndThrowAsync(message!, exception, cancellationToken).ConfigureAwait(false); + Debug.Fail("Unreachable"); + } + else { - // We are done with success. - return; + if (message == null || message == s_emptyMessage) + { + throw new InternalException(); + } + + await _framer!.WriteMessageAsync(InnerStream, message, cancellationToken).ConfigureAwait(false); } await ReceiveBlobAsync(cancellationToken).ConfigureAwait(false); @@ -863,12 +868,7 @@ namespace System.Net.Security if (message.Length >= sizeof(long)) { // Try to recover remote win32 Exception. - long error = 0; - for (int i = 0; i < 8; ++i) - { - error = (error << 8) + message[i]; - } - + long error = BinaryPrimitives.ReadInt64LittleEndian(message); ThrowCredentialException(error); } @@ -906,65 +906,23 @@ namespace System.Net.Security { _framer!.WriteHeader.MessageId = FrameHeader.HandshakeErrId; - if (IsLogonDeniedException(exception)) - { - exception = new InvalidCredentialException(IsServer ? SR.net_auth_bad_client_creds : SR.net_auth_bad_client_creds_or_target_mismatch, exception); - } - - if (!(exception is AuthenticationException)) - { - exception = new AuthenticationException(SR.net_auth_SSPI, exception); - } - await _framer.WriteMessageAsync(InnerStream, message, cancellationToken).ConfigureAwait(false); _canRetryAuthentication = true; ExceptionDispatchInfo.Throw(exception); } - private static bool IsError(SecurityStatusPal status) => - (int)status.ErrorCode >= (int)SecurityStatusPalErrorCode.OutOfMemory; - - private unsafe byte[]? GetOutgoingBlob(byte[]? incomingBlob, ref Exception? e) - { - Debug.Assert(_context != null); - - byte[]? message = _context.GetOutgoingBlob(incomingBlob, false, out SecurityStatusPal statusCode); - - if (IsError(statusCode)) - { - e = NegotiateStreamPal.CreateExceptionFromError(statusCode); - uint error = (uint)e.HResult; - - message = new byte[sizeof(long)]; - for (int i = message.Length - 1; i >= 0; --i) - { - message[i] = (byte)(error & 0xFF); - error >>= 8; - } - } - - if (message != null && message.Length == 0) - { - message = s_emptyMessage; - } - - return message; - } - private static void ThrowCredentialException(long error) { var e = new Win32Exception((int)error); throw e.NativeErrorCode switch { + // Compatibility quirk: .NET Core and .NET 5/6 incorrectly report internal status code instead of Win32 error number (int)SecurityStatusPalErrorCode.LogonDenied => new InvalidCredentialException(SR.net_auth_bad_client_creds, e), + (int)Interop.SECURITY_STATUS.LogonDenied => new InvalidCredentialException(SR.net_auth_bad_client_creds, e), ERROR_TRUST_FAILURE => new AuthenticationException(SR.net_auth_context_expectation_remote, e), _ => new AuthenticationException(SR.net_auth_alert, e) }; } - - private static bool IsLogonDeniedException(Exception exception) => - exception is Win32Exception win32exception && - win32exception.NativeErrorCode == (int)SecurityStatusPalErrorCode.LogonDenied; } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.PlatformNotSupported.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Managed.cs similarity index 76% rename from src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.PlatformNotSupported.cs rename to src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Managed.cs index 4fde254..9734bdf 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.PlatformNotSupported.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Managed.cs @@ -12,7 +12,6 @@ namespace System.Net.Security // It encapsulates security context and does the real work in authentication and // user data encryption with NEGO SSPI package. // - [UnsupportedOSPlatform("tvos")] internal static partial class NegotiateStreamPal { #pragma warning disable IDE0060 @@ -23,12 +22,11 @@ namespace System.Net.Security internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) { - throw new PlatformNotSupportedException(); - } - - internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode) - { - throw new PlatformNotSupportedException(); + if (impersonationLevel != TokenImpersonationLevel.Identification) + { + throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), + SR.net_auth_supported_impl_levels); + } } #pragma warning restore IDE0060 } diff --git a/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml b/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml index e3cd7e6..c07a60b 100644 --- a/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml +++ b/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml @@ -879,6 +879,12 @@ CP0014 + T:System.Net.Security.NegotiateStream:[T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute] + net7.0/netstandard.dll + net8.0/netstandard.dll + + + CP0014 M:System.Console.get_WindowHeight:[T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute] net7.0/System.Console.dll net8.0/System.Console.dll @@ -1743,6 +1749,12 @@ CP0014 + T:System.Net.Security.NegotiateStream:[T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute] + net7.0/System.dll + net8.0/System.dll + + + CP0014 M:System.Linq.Queryable.Aggregate``1(System.Linq.IQueryable{``0},System.Linq.Expressions.Expression{System.Func{``0,``0,``0}}):[T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute] net7.0/System.Linq.Queryable.dll net8.0/System.Linq.Queryable.dll @@ -2552,6 +2564,12 @@ net8.0/System.Linq.Queryable.dll + CP0014 + T:System.Net.Security.NegotiateStream:[T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute] + net7.0/System.Net.Security.dll + net8.0/System.Net.Security.dll + + CP0015 M:System.Type.GetEnumValues:[T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute] net7.0/mscorlib.dll -- 2.7.4