diff --git a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs index 292523ff7bdd5..ecb5adceb51b9 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs @@ -73,21 +73,21 @@ internal static partial Status ReleaseCred( ref IntPtr credHandle); [LibraryImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_InitSecContext")] - internal static partial Status InitSecContext( + private static partial Status InitSecContext( out Status minorStatus, SafeGssCredHandle initiatorCredHandle, ref SafeGssContextHandle contextHandle, [MarshalAs(UnmanagedType.Bool)] bool isNtlmOnly, SafeGssNameHandle? targetName, uint reqFlags, - byte[]? inputBytes, + ref byte inputBytes, int inputLength, ref GssBuffer token, out uint retFlags, [MarshalAs(UnmanagedType.Bool)] out bool isNtlmUsed); [LibraryImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_InitSecContextEx")] - internal static partial Status InitSecContext( + private static partial Status InitSecContext( out Status minorStatus, SafeGssCredHandle initiatorCredHandle, ref SafeGssContextHandle contextHandle, @@ -96,23 +96,99 @@ internal static partial Status InitSecContext( int cbtSize, SafeGssNameHandle? targetName, uint reqFlags, - byte[]? inputBytes, + ref byte inputBytes, int inputLength, ref GssBuffer token, out uint retFlags, [MarshalAs(UnmanagedType.Bool)] out bool isNtlmUsed); + internal static Status InitSecContext( + out Status minorStatus, + SafeGssCredHandle initiatorCredHandle, + ref SafeGssContextHandle contextHandle, + bool isNtlmOnly, + SafeGssNameHandle? targetName, + uint reqFlags, + ReadOnlySpan inputBytes, + ref GssBuffer token, + out uint retFlags, + out bool isNtlmUsed) + { + return InitSecContext( + out minorStatus, + initiatorCredHandle, + ref contextHandle, + isNtlmOnly, + targetName, + reqFlags, + ref MemoryMarshal.GetReference(inputBytes), + inputBytes.Length, + ref token, + out retFlags, + out isNtlmUsed); + } + + internal static Status InitSecContext( + out Status minorStatus, + SafeGssCredHandle initiatorCredHandle, + ref SafeGssContextHandle contextHandle, + bool isNtlmOnly, + IntPtr cbt, + int cbtSize, + SafeGssNameHandle? targetName, + uint reqFlags, + ReadOnlySpan inputBytes, + ref GssBuffer token, + out uint retFlags, + out bool isNtlmUsed) + { + return InitSecContext( + out minorStatus, + initiatorCredHandle, + ref contextHandle, + isNtlmOnly, + cbt, + cbtSize, + targetName, + reqFlags, + ref MemoryMarshal.GetReference(inputBytes), + inputBytes.Length, + ref token, + out retFlags, + out isNtlmUsed); + } + [LibraryImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_AcceptSecContext")] - internal static partial Status AcceptSecContext( + private static partial Status AcceptSecContext( out Status minorStatus, SafeGssCredHandle acceptorCredHandle, ref SafeGssContextHandle acceptContextHandle, - byte[]? inputBytes, + ref byte inputBytes, int inputLength, ref GssBuffer token, out uint retFlags, [MarshalAs(UnmanagedType.Bool)] out bool isNtlmUsed); + internal static Status AcceptSecContext( + out Status minorStatus, + SafeGssCredHandle acceptorCredHandle, + ref SafeGssContextHandle acceptContextHandle, + ReadOnlySpan inputBytes, + ref GssBuffer token, + out uint retFlags, + out bool isNtlmUsed) + { + return AcceptSecContext( + out minorStatus, + acceptorCredHandle, + ref acceptContextHandle, + ref MemoryMarshal.GetReference(inputBytes), + inputBytes.Length, + ref token, + out retFlags, + out isNtlmUsed); + } + [LibraryImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DeleteSecContext")] internal static partial Status DeleteSecContext( out Status minorStatus, diff --git a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.PlatformNotSupported.cs b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.PlatformNotSupported.cs deleted file mode 100644 index 20eeb6dfbf818..0000000000000 --- a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.PlatformNotSupported.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text; - -namespace Microsoft.Win32.SafeHandles -{ - [UnsupportedOSPlatform("tvos")] - internal sealed class SafeGssNameHandle : SafeHandle - { - public override bool IsInvalid - { - get { throw new PlatformNotSupportedException(); } - } - - protected override bool ReleaseHandle() => throw new PlatformNotSupportedException(); - - public SafeGssNameHandle() - : base(IntPtr.Zero, true) - { - } - } - - [UnsupportedOSPlatform("tvos")] - internal sealed class SafeGssCredHandle : SafeHandle - { - public SafeGssCredHandle() - : base(IntPtr.Zero, true) - { - } - - public override bool IsInvalid - { - get { throw new PlatformNotSupportedException(); } - } - - protected override bool ReleaseHandle() => throw new PlatformNotSupportedException(); - } - - [UnsupportedOSPlatform("tvos")] - internal sealed class SafeGssContextHandle : SafeHandle - { - public SafeGssContextHandle() - : base(IntPtr.Zero, true) - { - } - - public override bool IsInvalid - { - get { throw new PlatformNotSupportedException(); } - } - - protected override bool ReleaseHandle() => throw new PlatformNotSupportedException(); - } -} diff --git a/src/libraries/Common/src/System/Net/ContextFlagsAdapterPal.PlatformNotSupported.cs b/src/libraries/Common/src/System/Net/ContextFlagsAdapterPal.PlatformNotSupported.cs deleted file mode 100644 index ee4d9cb16dd4e..0000000000000 --- a/src/libraries/Common/src/System/Net/ContextFlagsAdapterPal.PlatformNotSupported.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.Versioning; - -namespace System.Net -{ - [UnsupportedOSPlatform("tvos")] - internal static class ContextFlagsAdapterPal - { - internal static ContextFlagsPal GetContextFlagsPalFromInterop(Interop.NetSecurityNative.GssFlags gssFlags, bool isServer) - { - throw new PlatformNotSupportedException(); - } - - internal static Interop.NetSecurityNative.GssFlags GetInteropFromContextFlagsPal(ContextFlagsPal flags, bool isServer) - { - throw new PlatformNotSupportedException(); - } - } -} diff --git a/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs b/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs index 7d733b9f4912d..cdb7103cdcd8d 100644 --- a/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs +++ b/src/libraries/Common/src/System/Net/NTAuthentication.Common.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.Security; @@ -10,7 +11,6 @@ namespace System.Net { - [UnsupportedOSPlatform("tvos")] internal sealed partial class NTAuthentication { private bool _isServer; @@ -81,12 +81,17 @@ internal bool IsKerberos { get { - if (_lastProtocolName == null) - { - _lastProtocolName = ProtocolName; - } + _lastProtocolName ??= ProtocolName; + return _lastProtocolName == NegotiationInfoClass.Kerberos; + } + } - return (object)_lastProtocolName == (object)NegotiationInfoClass.Kerberos; + internal bool IsNTLM + { + get + { + _lastProtocolName ??= ProtocolName; + return _lastProtocolName == NegotiationInfoClass.NTLM; } } @@ -150,6 +155,7 @@ internal void CloseContext() { _securityContext.Dispose(); } + _isCompleted = false; } internal int VerifySignature(byte[] buffer, int offset, int count) @@ -204,11 +210,16 @@ internal int MakeSignature(byte[] buffer, int offset, int count, [AllowNull] ref internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError) { - return GetOutgoingBlob(incomingBlob, throwOnError, out _); + return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out _); } // Accepts an incoming binary security blob and returns an outgoing binary security blob. internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) + { + return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out statusCode); + } + + internal byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) { byte[]? result = new byte[_tokenSize]; @@ -312,5 +323,29 @@ internal int MakeSignature(byte[] buffer, int offset, int count, [AllowNull] ref return spn; } + + internal int Encrypt(ReadOnlySpan buffer, [NotNull] ref byte[]? output, uint sequenceNumber) + { + return NegotiateStreamPal.Encrypt( + _securityContext!, + buffer, + (_contextFlags & ContextFlagsPal.Confidentiality) != 0, + IsNTLM, + ref output, + sequenceNumber); + } + + internal int Decrypt(byte[] payload, int offset, int count, out int newOffset, uint expectedSeqNumber) + { + return NegotiateStreamPal.Decrypt( + _securityContext!, + payload, + offset, + count, + (_contextFlags & ContextFlagsPal.Confidentiality) != 0, + IsNTLM, + out newOffset, + expectedSeqNumber); + } } } diff --git a/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs b/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs index 16e9ee17745e2..04b8bd8f2270b 100644 --- a/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs +++ b/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Formats.Asn1; -using System.Net.Http.Headers; using System.Net.Security; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -27,6 +26,7 @@ internal sealed partial class NTAuthentication private readonly NetworkCredential _credential; private readonly string? _spn; private readonly ChannelBinding? _channelBinding; + private readonly ContextFlagsPal _contextFlags; // State parameters private byte[]? _spnegoMechList; @@ -278,10 +278,11 @@ internal NTAuthentication(bool isServer, string package, NetworkCredential crede if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"package={package}, spn={spn}, requestedContextFlags={requestedContextFlags}"); - // TODO: requestedContextFlags _credential = credential; _spn = spn; _channelBinding = channelBinding; + _contextFlags = requestedContextFlags; + IsServer = isServer; } internal void CloseContext() @@ -296,7 +297,7 @@ internal void CloseContext() _serverSeal = null; _clientSequenceNumber = 0; _serverSequenceNumber = 0; - IsCompleted = true; + IsCompleted = false; } internal string? GetOutgoingBlob(string? incomingBlob) @@ -313,43 +314,65 @@ internal void CloseContext() { decodedIncomingBlob = Convert.FromBase64String(incomingBlob); } - byte[]? decodedOutgoingBlob; + byte[]? decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, throwOnError, out statusCode); + string? outgoingBlob = null; + if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) + { + outgoingBlob = Convert.ToBase64String(decodedOutgoingBlob); + } + + if (IsCompleted) + { + CloseContext(); + } + + return outgoingBlob; + } + + internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError) + { + return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out _); + } + + // Accepts an incoming binary security blob and returns an outgoing binary security blob. + internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) + { + return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out statusCode); + } + + internal unsafe byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) + { + byte[]? outgoingBlob; // TODO: Logging, validation if (_negotiateMessage == null) { - Debug.Assert(decodedIncomingBlob == null); + Debug.Assert(incomingBlob.IsEmpty); _negotiateMessage = new byte[sizeof(NegotiateMessage)]; CreateNtlmNegotiateMessage(_negotiateMessage); - decodedOutgoingBlob = _isSpNego ? CreateSpNegoNegotiateMessage(_negotiateMessage) : _negotiateMessage; + outgoingBlob = _isSpNego ? CreateSpNegoNegotiateMessage(_negotiateMessage) : _negotiateMessage; statusCode = SecurityStatusPalContinueNeeded; } else { - Debug.Assert(decodedIncomingBlob != null); + Debug.Assert(!incomingBlob.IsEmpty); if (!_isSpNego) { IsCompleted = true; - decodedOutgoingBlob = ProcessChallenge(decodedIncomingBlob, out statusCode); + outgoingBlob = ProcessChallenge(incomingBlob, out statusCode); } else { - decodedOutgoingBlob = ProcessSpNegoChallenge(decodedIncomingBlob, out statusCode); + outgoingBlob = ProcessSpNegoChallenge(incomingBlob, out statusCode); } } - string? outgoingBlob = null; - if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) - { - outgoingBlob = Convert.ToBase64String(decodedOutgoingBlob); - } - - if (IsCompleted) + if (statusCode.ErrorCode >= SecurityStatusPalErrorCode.OutOfMemory && throwOnError) { - CloseContext(); + throw new Win32Exception(NTE_FAIL, statusCode.ErrorCode.ToString()); } return outgoingBlob; @@ -613,23 +636,22 @@ private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlyS } // This gets decoded byte blob and returns response in binary form. - private unsafe byte[]? ProcessChallenge(byte[] blob, out SecurityStatusPal statusCode) + private unsafe byte[]? ProcessChallenge(ReadOnlySpan blob, out SecurityStatusPal statusCode) { // TODO: Validate size and offsets - ReadOnlySpan asBytes = new ReadOnlySpan(blob); - ref readonly ChallengeMessage challengeMessage = ref MemoryMarshal.AsRef(asBytes.Slice(0, sizeof(ChallengeMessage))); + ref readonly ChallengeMessage challengeMessage = ref MemoryMarshal.AsRef(blob.Slice(0, sizeof(ChallengeMessage))); // Verify message type and signature if (challengeMessage.Header.MessageType != MessageType.Challenge || - !NtlmHeader.SequenceEqual(asBytes.Slice(0, NtlmHeader.Length))) + !NtlmHeader.SequenceEqual(blob.Slice(0, NtlmHeader.Length))) { statusCode = SecurityStatusPalInvalidToken; return null; } Flags flags = BitConverter.IsLittleEndian ? challengeMessage.Flags : (Flags)BinaryPrimitives.ReverseEndianness((uint)challengeMessage.Flags); - ReadOnlySpan targetName = GetField(challengeMessage.TargetName, asBytes); + ReadOnlySpan targetName = GetField(challengeMessage.TargetName, blob); // Only NTLMv2 with MIC is supported // @@ -641,7 +663,7 @@ private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlyS return null; } - ReadOnlySpan targetInfo = GetField(challengeMessage.TargetInfo, asBytes); + ReadOnlySpan targetInfo = GetField(challengeMessage.TargetInfo, blob); byte[] targetInfoBuffer = ProcessTargetInfo(targetInfo, out DateTime time, out bool hasNbNames); // If NTLM v2 authentication is used and the CHALLENGE_MESSAGE does not contain both @@ -692,7 +714,7 @@ private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlyS payloadOffset += ChallengeResponseLength; // Create NTLM2 response - ReadOnlySpan serverChallenge = asBytes.Slice(24, 8); + ReadOnlySpan serverChallenge = blob.Slice(24, 8); makeNtlm2ChallengeResponse(time, ntlm2hash, serverChallenge, clientChallenge, targetInfoBuffer, ref response.NtChallengeResponse, payload, ref payloadOffset); Debug.Assert(payloadOffset == sizeof(AuthenticateMessage) + ChallengeResponseLength + sizeof(NtChallengeResponse) + targetInfoBuffer.Length); @@ -844,7 +866,7 @@ private unsafe byte[] CreateSpNegoNegotiateMessage(ReadOnlySpan ntlmNegoti return writer.Encode(); } - private unsafe byte[]? ProcessSpNegoChallenge(byte[] challenge, out SecurityStatusPal statusCode) + private unsafe byte[]? ProcessSpNegoChallenge(ReadOnlySpan challenge, out SecurityStatusPal statusCode) { NegState state = NegState.Unknown; string? mech = null; @@ -853,8 +875,8 @@ private unsafe byte[] CreateSpNegoNegotiateMessage(ReadOnlySpan ntlmNegoti try { - AsnReader reader = new AsnReader(challenge, AsnEncodingRules.DER); - AsnReader challengeReader = reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp)); + AsnValueReader reader = new AsnValueReader(challenge, AsnEncodingRules.DER); + AsnValueReader challengeReader = reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp)); reader.ThrowIfNotEmpty(); // NegTokenResp ::= SEQUENCE { @@ -876,28 +898,28 @@ private unsafe byte[] CreateSpNegoNegotiateMessage(ReadOnlySpan ntlmNegoti if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState))) { - AsnReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState)); + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState)); state = valueReader.ReadEnumeratedValue(); valueReader.ThrowIfNotEmpty(); } if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.SupportedMech))) { - AsnReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.SupportedMech)); + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.SupportedMech)); mech = valueReader.ReadObjectIdentifier(); valueReader.ThrowIfNotEmpty(); } if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken))) { - AsnReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken)); + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken)); blob = valueReader.ReadOctetString(); valueReader.ThrowIfNotEmpty(); } if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC))) { - AsnReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC)); + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC)); mechListMIC = valueReader.ReadOctetString(); valueReader.ThrowIfNotEmpty(); } @@ -979,5 +1001,29 @@ private unsafe byte[] CreateSpNegoNegotiateMessage(ReadOnlySpan ntlmNegoti return null; } + +#pragma warning disable CA1822 + internal int Encrypt(ReadOnlySpan buffer, [NotNull] ref byte[]? output, uint sequenceNumber) + { + throw new PlatformNotSupportedException(); + } + + internal int Decrypt(byte[] payload, int offset, int count, out int newOffset, uint expectedSeqNumber) + { + throw new PlatformNotSupportedException(); + } + + internal string ProtocolName => _isSpNego ? NegotiationInfoClass.Negotiate : NegotiationInfoClass.NTLM; + + internal bool IsNTLM => true; + + internal bool IsKerberos => false; + + internal bool IsServer { get; set; } + + internal bool IsValidContext => true; + + internal string? ClientSpecifiedSpn => _spn; +#pragma warning restore CA1822 } } diff --git a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.PlatformNotSupported.cs b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.PlatformNotSupported.cs deleted file mode 100644 index 00eb37b101b52..0000000000000 --- a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.PlatformNotSupported.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Security; -using System.Security.Authentication; -using System.Security.Authentication.ExtendedProtection; -using System.Security.Principal; -using System.Text; -using System.Threading; -using Microsoft.Win32.SafeHandles; - -namespace System.Net.Security -{ - // - // The class maintains the state of the authentication process and the security context. - // 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 - { - internal static string QueryContextClientSpecifiedSpn(SafeDeleteContext securityContext) - { - throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); - } - - internal static string QueryContextAuthenticationPackage(SafeDeleteContext securityContext) - { - throw new PlatformNotSupportedException(); - } - - internal static SecurityStatusPal InitializeSecurityContext( - ref SafeFreeCredentials credentialsHandle, - ref SafeDeleteContext? securityContext, - string? spn, - ContextFlagsPal requestedContextFlags, - byte[]? incomingBlob, - ChannelBinding? channelBinding, - ref byte[]? resultBlob, - ref ContextFlagsPal contextFlags) - { - throw new PlatformNotSupportedException(); - } - - internal static SecurityStatusPal AcceptSecurityContext( - SafeFreeCredentials? credentialsHandle, - ref SafeDeleteContext? securityContext, - ContextFlagsPal requestedContextFlags, - byte[]? incomingBlob, - ChannelBinding? channelBinding, - ref byte[] resultBlob, - ref ContextFlagsPal contextFlags) - { - throw new PlatformNotSupportedException(); - } - - internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode) - { - throw new PlatformNotSupportedException(); - } - - internal static int QueryMaxTokenSize(string package) - { - throw new PlatformNotSupportedException(); - } - - internal static SafeFreeCredentials AcquireDefaultCredential(string package, bool isServer) - { - throw new PlatformNotSupportedException(); - } - - internal static SafeFreeCredentials AcquireCredentialsHandle(string package, bool isServer, NetworkCredential credential) - { - throw new PlatformNotSupportedException(); - } - - internal static SecurityStatusPal CompleteAuthToken( - ref SafeDeleteContext? securityContext, - byte[]? incomingBlob) - { - throw new PlatformNotSupportedException(); - } - - internal static int Encrypt( - SafeDeleteContext securityContext, - ReadOnlySpan buffer, - bool isConfidential, - bool isNtlm, - [NotNull] ref byte[]? output, - uint sequenceNumber) - { - throw new PlatformNotSupportedException(); - } - - internal static int Decrypt( - SafeDeleteContext securityContext, - byte[]? buffer, - int offset, - int count, - bool isConfidential, - bool isNtlm, - out int newOffset, - uint sequenceNumber) - { - throw new PlatformNotSupportedException(); - } - - internal static int VerifySignature(SafeDeleteContext securityContext, byte[] buffer, int offset, int count) - { - throw new PlatformNotSupportedException(); - } - - internal static int MakeSignature(SafeDeleteContext securityContext, byte[] buffer, int offset, int count, [AllowNull] ref byte[] output) - { - throw new PlatformNotSupportedException(); - } - } -} 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 291795b418b38..47daaaf9872f6 100644 --- a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs @@ -97,7 +97,7 @@ private static bool GssInitSecurityContext( ChannelBinding? channelBinding, SafeGssNameHandle? targetName, Interop.NetSecurityNative.GssFlags inFlags, - byte[]? buffer, + ReadOnlySpan buffer, out byte[]? outputBuffer, out uint outFlags, out bool isNtlmUsed) @@ -139,7 +139,6 @@ private static bool GssInitSecurityContext( targetName, (uint)inFlags, buffer, - (buffer == null) ? 0 : buffer.Length, ref token, out outFlags, out isNtlmUsed); @@ -153,7 +152,6 @@ private static bool GssInitSecurityContext( targetName, (uint)inFlags, buffer, - (buffer == null) ? 0 : buffer.Length, ref token, out outFlags, out isNtlmUsed); @@ -183,7 +181,7 @@ private static bool GssInitSecurityContext( private static bool GssAcceptSecurityContext( ref SafeGssContextHandle? context, SafeGssCredHandle credential, - byte[]? buffer, + ReadOnlySpan buffer, out byte[] outputBuffer, out uint outFlags, out bool isNtlmUsed) @@ -207,7 +205,6 @@ private static bool GssAcceptSecurityContext( credential, ref context, buffer, - buffer?.Length ?? 0, ref token, out outFlags, out isNtlmUsed); @@ -276,7 +273,7 @@ private static SecurityStatusPal EstablishSecurityContext( ChannelBinding? channelBinding, string? targetName, ContextFlagsPal inFlags, - byte[]? incomingBlob, + ReadOnlySpan incomingBlob, ref byte[]? resultBuffer, ref ContextFlagsPal outFlags) { @@ -354,7 +351,7 @@ internal static SecurityStatusPal InitializeSecurityContext( ref SafeDeleteContext? securityContext, string? spn, ContextFlagsPal requestedContextFlags, - byte[]? incomingBlob, + ReadOnlySpan incomingBlob, ChannelBinding? channelBinding, ref byte[]? resultBlob, ref ContextFlagsPal contextFlags) @@ -383,7 +380,7 @@ internal static SecurityStatusPal AcceptSecurityContext( SafeFreeCredentials? credentialsHandle, ref SafeDeleteContext? securityContext, ContextFlagsPal requestedContextFlags, - byte[]? incomingBlob, + ReadOnlySpan incomingBlob, ChannelBinding? channelBinding, ref byte[] resultBlob, ref ContextFlagsPal contextFlags) @@ -517,6 +514,12 @@ internal static SafeFreeCredentials AcquireCredentialsHandle(string package, boo throw new PlatformNotSupportedException(SR.net_ntlm_not_possible_default_cred); } + if (!ntlmOnly && !string.Equals(package, NegotiationInfoClass.Negotiate)) + { + // Native shim currently supports only NTLM and Negotiate + throw new PlatformNotSupportedException(SR.net_securitypackagesupport); + } + try { return isEmptyCredential ? 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 a145951254a76..29603b58d2ce5 100644 --- a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Windows.cs +++ b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Windows.cs @@ -55,6 +55,11 @@ internal static SafeFreeCredentials AcquireCredentialsHandle(string package, boo } } + internal static string? QueryContextAssociatedName(SafeDeleteContext securityContext) + { + return SSPIWrapper.QueryStringContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_NAMES); + } + internal static string? QueryContextClientSpecifiedSpn(SafeDeleteContext securityContext) { return SSPIWrapper.QueryStringContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_CLIENT_SPECIFIED_TARGET); @@ -75,14 +80,14 @@ internal static SecurityStatusPal InitializeSecurityContext( ref SafeDeleteContext? securityContext, string? spn, ContextFlagsPal requestedContextFlags, - byte[]? incomingBlob, + ReadOnlySpan incomingBlob, ChannelBinding? channelBinding, ref byte[]? resultBlob, ref ContextFlagsPal contextFlags) { InputSecurityBuffers inputBuffers = default; - if (incomingBlob != null) + if (!incomingBlob.IsEmpty) { inputBuffers.SetNextBuffer(new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN)); } @@ -132,13 +137,13 @@ internal static SecurityStatusPal AcceptSecurityContext( SafeFreeCredentials? credentialsHandle, ref SafeDeleteContext? securityContext, ContextFlagsPal requestedContextFlags, - byte[]? incomingBlob, + ReadOnlySpan incomingBlob, ChannelBinding? channelBinding, ref byte[]? resultBlob, ref ContextFlagsPal contextFlags) { InputSecurityBuffers inputBuffers = default; - if (incomingBlob != null) + if (!incomingBlob.IsEmpty) { inputBuffers.SetNextBuffer(new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN)); } @@ -163,6 +168,14 @@ internal static SecurityStatusPal AcceptSecurityContext( ref outSecurityBuffer, ref outContextFlags); + // SSPI Workaround + // If a client sends up a blob on the initial request, Negotiate returns SEC_E_INVALID_HANDLE + // when it should return SEC_E_INVALID_TOKEN. + if (winStatus == Interop.SECURITY_STATUS.InvalidHandle && securityContext == null && !incomingBlob.IsEmpty) + { + winStatus = Interop.SECURITY_STATUS.InvalidToken; + } + resultBlob = outSecurityBuffer.token; securityContext = sslContext; contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop(outContextFlags); @@ -263,5 +276,195 @@ internal static int MakeSignature(SafeDeleteContext securityContext, byte[] buff // return signed size return securityBuffer[0].size + securityBuffer[1].size; } + + internal static int Encrypt( + SafeDeleteContext securityContext, + ReadOnlySpan buffer, + bool isConfidential, + bool isNtlm, + [NotNull] ref byte[]? output, + uint sequenceNumber) + { + 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)); + } + + 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)); + + // Prepare buffers TOKEN(signature), DATA and Padding. + ThreeSecurityBuffers buffers = default; + var securityBuffer = MemoryMarshal.CreateSpan(ref buffers._item0, 3); + securityBuffer[0] = new SecurityBuffer(output, 4, sizes.cbSecurityTrailer, SecurityBufferType.SECBUFFER_TOKEN); + securityBuffer[1] = new SecurityBuffer(output, 4 + sizes.cbSecurityTrailer, buffer.Length, SecurityBufferType.SECBUFFER_DATA); + securityBuffer[2] = new SecurityBuffer(output, 4 + sizes.cbSecurityTrailer + buffer.Length, sizes.cbBlockSize, SecurityBufferType.SECBUFFER_PADDING); + + int errorCode; + if (isConfidential) + { + errorCode = SSPIWrapper.EncryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + else + { + if (isNtlm) + { + securityBuffer[1].type |= SecurityBufferType.SECBUFFER_READONLY; + } + + errorCode = SSPIWrapper.MakeSignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, 0); + } + + if (errorCode != 0) + { + Exception e = new Win32Exception(errorCode); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); + throw e; + } + + // Compacting the result. + resultSize = securityBuffer[0].size; + bool forceCopy = false; + if (resultSize != sizes.cbSecurityTrailer) + { + forceCopy = true; + Buffer.BlockCopy(output, securityBuffer[1].offset, output, 4 + resultSize, securityBuffer[1].size); + } + + resultSize += securityBuffer[1].size; + if (securityBuffer[2].size != 0 && (forceCopy || resultSize != (buffer.Length + sizes.cbSecurityTrailer))) + { + Buffer.BlockCopy(output, securityBuffer[2].offset, output, 4 + resultSize, securityBuffer[2].size); + } + + resultSize += securityBuffer[2].size; + unchecked + { + output[0] = (byte)((resultSize) & 0xFF); + output[1] = (byte)(((resultSize) >> 8) & 0xFF); + output[2] = (byte)(((resultSize) >> 16) & 0xFF); + output[3] = (byte)(((resultSize) >> 24) & 0xFF); + } + + return resultSize + 4; + } + + internal static int Decrypt( + SafeDeleteContext securityContext, + byte[]? buffer, + int offset, + int count, + bool isConfidential, + bool isNtlm, + out int newOffset, + uint sequenceNumber) + { + if (offset < 0 || offset > (buffer == null ? 0 : buffer.Length)) + { + Debug.Fail("Argument 'offset' out of range."); + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0 || count > (buffer == null ? 0 : buffer.Length - offset)) + { + Debug.Fail("Argument 'count' out of range."); + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (isNtlm) + { + return DecryptNtlm(securityContext, buffer, offset, count, isConfidential, out newOffset, sequenceNumber); + } + + // + // Kerberos and up + // + TwoSecurityBuffers buffers = default; + var securityBuffer = MemoryMarshal.CreateSpan(ref buffers._item0, 2); + securityBuffer[0] = new SecurityBuffer(buffer, offset, count, SecurityBufferType.SECBUFFER_STREAM); + securityBuffer[1] = new SecurityBuffer(0, SecurityBufferType.SECBUFFER_DATA); + + int errorCode = isConfidential ? + SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber) : + SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + + if (errorCode != 0) + { + Exception e = new Win32Exception(errorCode); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); + throw e; + } + + if (securityBuffer[1].type != SecurityBufferType.SECBUFFER_DATA) + { + throw new InternalException(securityBuffer[1].type); + } + + newOffset = securityBuffer[1].offset; + return securityBuffer[1].size; + } + + private static int DecryptNtlm( + SafeDeleteContext securityContext, + byte[]? buffer, + int offset, + int count, + bool isConfidential, + out int newOffset, + uint sequenceNumber) + { + const int ntlmSignatureLength = 16; + // For the most part the arguments are verified in Decrypt(). + if (count < ntlmSignatureLength) + { + Debug.Fail("Argument 'count' out of range."); + throw new ArgumentOutOfRangeException(nameof(count)); + } + + TwoSecurityBuffers buffers = default; + var securityBuffer = MemoryMarshal.CreateSpan(ref buffers._item0, 2); + securityBuffer[0] = new SecurityBuffer(buffer, offset, ntlmSignatureLength, SecurityBufferType.SECBUFFER_TOKEN); + securityBuffer[1] = new SecurityBuffer(buffer, offset + ntlmSignatureLength, count - ntlmSignatureLength, SecurityBufferType.SECBUFFER_DATA); + + int errorCode; + SecurityBufferType realDataType = SecurityBufferType.SECBUFFER_DATA; + + if (isConfidential) + { + errorCode = SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + else + { + realDataType |= SecurityBufferType.SECBUFFER_READONLY; + securityBuffer[1].type = realDataType; + errorCode = SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + + if (errorCode != 0) + { + Exception e = new Win32Exception(errorCode); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); + throw new Win32Exception(errorCode); + } + + if (securityBuffer[1].type != realDataType) + { + throw new InternalException(securityBuffer[1].type); + } + + newOffset = securityBuffer[1].offset; + return securityBuffer[1].size; + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs b/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs index 67e9d869a68db..076a01d142d12 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs @@ -208,6 +208,13 @@ internal string ReadCharacterString(UniversalTagNumber encodingType, Asn1Tag? ex _span = _span.Slice(consumed); return ret; } + + internal TEnum ReadEnumeratedValue(Asn1Tag? expectedTag = null) where TEnum : Enum + { + TEnum ret = AsnDecoder.ReadEnumeratedValue(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } } internal static class AsnWriterExtensions diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index d5ff73663f820..20a30c23e9a9f 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -432,48 +432,12 @@ Protocol error: A received message contains a valid signature but it was not encrypted as required by the effective Protection Level. - - The requested security package is not supported. - - - '{0}' is not a supported handle type. - Authentication failed because the connection could not be reused. Authentication validation failed with error - {0}. - - Server implementation is not supported - - - Requested protection level is not supported with the GSSAPI implementation currently installed. - - - Insufficient buffer space. Required: {0} Actual: {1}. - - - GSSAPI operation failed with error - {0} ({1}). - - - GSSAPI operation failed with status: {0} (Minor status: {1}). - - - GSSAPI operation failed with error - {0}. - - - GSSAPI operation failed with status: {0}. - - - NTLM authentication requires the GSSAPI plugin 'gss-ntlmssp'. - - - NTLM authentication is not possible with default credentials on this platform. - - - Target name should be non empty if default credentials are passed. - Huffman-coded literal string failed to decode. diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 709ac8d11b022..1aabb7ad01e22 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -8,7 +8,6 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) - true SR.PlatformNotSupported_NetHttp $(DefineConstants);SYSNETHTTP_NO_OPENSSL $(DefineConstants);TARGET_MOBILE @@ -212,16 +211,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -468,56 +419,8 @@ Link="Common\System\Runtime\ExceptionServices\ExceptionStackTrace.cs" /> - - - - - - - - - - - - - - - - - - - - - - - - SendWithNtAuthAsync(HttpRequestMe NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}"); } - ContextFlagsPal contextFlags = ContextFlagsPal.Connection; + ProtectionLevel requiredProtectionLevel = ProtectionLevel.None; // When connecting to proxy server don't enforce the integrity to avoid // compatibility issues. The assumption is that the proxy server comes // from a trusted source. On macOS we always need to enforce the integrity @@ -162,56 +164,57 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe // tokens. if (!isProxyAuth || OperatingSystem.IsMacOS()) { - contextFlags |= ContextFlagsPal.InitIntegrity; + requiredProtectionLevel = ProtectionLevel.Sign; } - ChannelBinding? channelBinding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint); - NTAuthentication authContext = new NTAuthentication(isServer: false, challenge.SchemeName, challenge.Credential, spn, contextFlags, channelBinding); + NegotiateAuthenticationClientOptions authClientOptions = new NegotiateAuthenticationClientOptions + { + Package = challenge.SchemeName, + Credential = challenge.Credential, + TargetName = spn, + RequiredProtectionLevel = requiredProtectionLevel, + Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint) + }; + + using NegotiateAuthentication authContext = new NegotiateAuthentication(authClientOptions); string? challengeData = challenge.ChallengeData; - try + NegotiateAuthenticationStatusCode statusCode; + while (true) { - while (true) + string? challengeResponse = authContext.GetOutgoingBlob(challengeData, out statusCode); + if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded || challengeResponse == null) { - SecurityStatusPal statusCode; - string? challengeResponse = authContext.GetOutgoingBlob(challengeData, throwOnError: false, out statusCode); - if (statusCode.ErrorCode > SecurityStatusPalErrorCode.TryAgain || challengeResponse == null) - { - // Response indicated denial even after login, so stop processing and return current response. - break; - } + // Response indicated denial even after login, so stop processing and return current response. + break; + } - if (needDrain) - { - await connection.DrainResponseAsync(response!, cancellationToken).ConfigureAwait(false); - } + if (needDrain) + { + await connection.DrainResponseAsync(response!, cancellationToken).ConfigureAwait(false); + } - SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeName, challengeResponse), isProxyAuth); + SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeName, challengeResponse), isProxyAuth); - response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); - if (authContext.IsCompleted || !TryGetChallengeDataForScheme(challenge.SchemeName, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out challengeData)) - { - break; - } + response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); + if (authContext.IsAuthenticated || !TryGetChallengeDataForScheme(challenge.SchemeName, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out challengeData)) + { + break; + } - if (!IsAuthenticationChallenge(response, isProxyAuth)) + if (!IsAuthenticationChallenge(response, isProxyAuth)) + { + // Tail response for Negoatiate on successful authentication. Validate it before we proceed. + authContext.GetOutgoingBlob(challengeData, out statusCode); + if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded) { - // Tail response for Negoatiate on successful authentication. Validate it before we proceed. - authContext.GetOutgoingBlob(challengeData, throwOnError: false, out statusCode); - if (statusCode.ErrorCode != SecurityStatusPalErrorCode.OK) - { - isNewConnection = false; - connection.Dispose(); - throw new HttpRequestException(SR.Format(SR.net_http_authvalidationfailure, statusCode.ErrorCode), null, HttpStatusCode.Unauthorized); - } - break; + isNewConnection = false; + connection.Dispose(); + throw new HttpRequestException(SR.Format(SR.net_http_authvalidationfailure, statusCode), null, HttpStatusCode.Unauthorized); } - - needDrain = true; + break; } - } - finally - { - authContext.CloseContext(); + + needDrain = true; } } finally diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.tvOS.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.tvOS.cs deleted file mode 100644 index 2b5b5a4fe49f9..0000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.tvOS.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net.Http -{ - internal static partial class AuthenticationHelper - { - private static Task InnerSendAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) - { - return isProxyAuth ? - SendWithProxyAuthAsync(request, authUri, async, credentials, false, connectionPool, cancellationToken).AsTask() : - SendWithRequestAuthAsync(request, async, credentials, false, connectionPool, cancellationToken).AsTask(); - } - - public static Task SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) - { - return InnerSendAsync(request, proxyUri, async, proxyCredentials, isProxyAuth: true, connection, connectionPool, cancellationToken); - } - - public static Task SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) - { - Debug.Assert(request.RequestUri != null); - return InnerSendAsync(request, request.RequestUri, async, credentials, isProxyAuth: false, connection, connectionPool, cancellationToken); - } - } -} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.Windows.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.Windows.cs index 8bb4df5747efc..6a21f050015a3 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.Windows.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.Windows.cs @@ -31,7 +31,7 @@ internal static Task HandleNegotiateAuthenticationRequest(LoopbackServer.Connect internal static async Task HandleAuthenticationRequest(LoopbackServer.Connection connection, bool useNtlm, bool useNegotiate, bool closeConnection) { HttpRequestData request = await connection.ReadRequestDataAsync(); - NTAuthentication authContext = null; + NegotiateAuthentication authContext = null; string authHeader = null; foreach (HttpHeaderData header in request.Headers) @@ -64,7 +64,7 @@ internal static async Task HandleAuthenticationRequest(LoopbackServer.Connection request = await connection.ReadRequestDataAsync(); } - SecurityStatusPal statusCode; + NegotiateAuthenticationStatusCode statusCode; do { foreach (HttpHeaderData header in request.Headers) @@ -81,11 +81,11 @@ internal static async Task HandleAuthenticationRequest(LoopbackServer.Connection // Should be type and base64 encoded blob Assert.Equal(2, tokens.Length); - authContext ??= new NTAuthentication(isServer: true, tokens[0], CredentialCache.DefaultNetworkCredentials, null, ContextFlagsPal.Connection, null); + authContext ??= new NegotiateAuthentication(new NegotiateAuthenticationServerOptions { Package = tokens[0] }); - byte[]? outBlob = authContext.GetOutgoingBlob(Convert.FromBase64String(tokens[1]), throwOnError: false, out statusCode); + byte[]? outBlob = authContext.GetOutgoingBlob(Convert.FromBase64String(tokens[1]), out statusCode); - if (outBlob != null && statusCode.ErrorCode == SecurityStatusPalErrorCode.ContinueNeeded) + if (outBlob != null && statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded) { authHeader = $"WWW-Authenticate: {tokens[0]} {Convert.ToBase64String(outBlob)}\r\n"; await connection.SendResponseAsync(HttpStatusCode.Unauthorized, authHeader); @@ -94,15 +94,12 @@ internal static async Task HandleAuthenticationRequest(LoopbackServer.Connection request = await connection.ReadRequestDataAsync(); } } - while (statusCode.ErrorCode == SecurityStatusPalErrorCode.ContinueNeeded); + while (statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded); - if (statusCode.ErrorCode == SecurityStatusPalErrorCode.OK) + if (statusCode == NegotiateAuthenticationStatusCode.Completed) { // If authentication succeeded ask Windows about the identity and send it back as custom header. - SecurityContextTokenHandle? userContext = null; - using SafeDeleteContext securityContext = authContext.GetContext(out SecurityStatusPal statusCodeNew)!; - SSPIWrapper.QuerySecurityContextToken(GlobalSSPI.SSPIAuth, securityContext, out userContext); - using WindowsIdentity identity = new WindowsIdentity(userContext.DangerousGetHandle(), authContext.ProtocolName); + IIdentity identity = authContext.RemoteIdentity; authHeader = $"{UserHeaderName}: {identity.Name}\r\n"; if (closeConnection) @@ -111,7 +108,7 @@ internal static async Task HandleAuthenticationRequest(LoopbackServer.Connection } await connection.SendResponseAsync(HttpStatusCode.OK, authHeader, "foo"); - userContext.Dispose(); + authContext.Dispose(); } else { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index e5da76698df37..8d686b8343d4e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -225,108 +225,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/libraries/System.Net.HttpListener/src/Resources/Strings.resx b/src/libraries/System.Net.HttpListener/src/Resources/Strings.resx index c9c146f360c39..11a2453619c17 100644 --- a/src/libraries/System.Net.HttpListener/src/Resources/Strings.resx +++ b/src/libraries/System.Net.HttpListener/src/Resources/Strings.resx @@ -131,6 +131,9 @@ {0} can only be called once for each asynchronous operation. + + The byte count must not exceed {0} bytes for this stream type. + Custom channel bindings are not supported. diff --git a/src/libraries/System.Net.Mail/src/Resources/Strings.resx b/src/libraries/System.Net.Mail/src/Resources/Strings.resx index 8f077a3789f67..082461c79ccae 100644 --- a/src/libraries/System.Net.Mail/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Mail/src/Resources/Strings.resx @@ -64,6 +64,9 @@ {0} can only be called once for each asynchronous operation. + + The byte count must not exceed {0} bytes for this stream type. + This method is not implemented by this class. 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 76589add6b201..a5a9f3083a686 100644 --- a/src/libraries/System.Net.Security/ref/System.Net.Security.cs +++ b/src/libraries/System.Net.Security/ref/System.Net.Security.cs @@ -35,6 +35,56 @@ public enum EncryptionPolicy NoEncryption = 2, } public delegate System.Security.Cryptography.X509Certificates.X509Certificate LocalCertificateSelectionCallback(object sender, string targetHost, System.Security.Cryptography.X509Certificates.X509CertificateCollection localCertificates, System.Security.Cryptography.X509Certificates.X509Certificate? remoteCertificate, string[] acceptableIssuers); + public sealed partial class NegotiateAuthentication : System.IDisposable + { + public NegotiateAuthentication(System.Net.Security.NegotiateAuthenticationClientOptions clientOptions) { } + public NegotiateAuthentication(System.Net.Security.NegotiateAuthenticationServerOptions serverOptions) { } + public bool IsAuthenticated { get { throw null; } } + public bool IsEncrypted { get { throw null; } } + public bool IsMutuallyAuthenticated { get { throw null; } } + public bool IsServer { get { throw null; } } + public bool IsSigned { get { throw null; } } + public string Package { get { throw null; } } + public System.Net.Security.ProtectionLevel ProtectionLevel { get { throw null; } } + public System.Security.Principal.IIdentity RemoteIdentity { get { throw null; } } + public string? TargetName { get { throw null; } } + public void Dispose() { } + public byte[]? GetOutgoingBlob(System.ReadOnlySpan incomingBlob, out System.Net.Security.NegotiateAuthenticationStatusCode statusCode) { throw null; } + public string? GetOutgoingBlob(string? incomingBlob, out System.Net.Security.NegotiateAuthenticationStatusCode statusCode) { throw null; } + } + public partial class NegotiateAuthenticationClientOptions + { + public NegotiateAuthenticationClientOptions() { } + public System.Security.Authentication.ExtendedProtection.ChannelBinding? Binding { get { throw null; } set { } } + public System.Net.NetworkCredential Credential { get { throw null; } set { } } + public string Package { get { throw null; } set { } } + public System.Net.Security.ProtectionLevel RequiredProtectionLevel { get { throw null; } set { } } + public string? TargetName { get { throw null; } set { } } + } + public partial class NegotiateAuthenticationServerOptions + { + public NegotiateAuthenticationServerOptions() { } + public System.Security.Authentication.ExtendedProtection.ChannelBinding? Binding { get { throw null; } set { } } + public System.Net.NetworkCredential Credential { get { throw null; } set { } } + public string Package { get { throw null; } set { } } + public System.Net.Security.ProtectionLevel RequiredProtectionLevel { get { throw null; } set { } } + } + public enum NegotiateAuthenticationStatusCode + { + Completed = 0, + ContinueNeeded = 1, + GenericFailure = 2, + BadBinding = 3, + Unsupported = 4, + MessageAltered = 5, + ContextExpired = 6, + CredentialsExpired = 7, + InvalidCredentials = 8, + InvalidToken = 9, + UnknownCredentials = 10, + QopNotSupported = 11, + OutOfSequence = 12, + } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] public partial class NegotiateStream : System.Net.Security.AuthenticatedStream { diff --git a/src/libraries/System.Net.Security/src/Resources/Strings.resx b/src/libraries/System.Net.Security/src/Resources/Strings.resx index 0aef32953c3b0..2e4075f7d5e2a 100644 --- a/src/libraries/System.Net.Security/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Security/src/Resources/Strings.resx @@ -389,4 +389,7 @@ Sending trust in handshake is not supported on this platform. + + ASN1 corrupted data. + 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 50a94c18ae064..139ea7ce4ba06 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -12,9 +12,11 @@ $(DefineConstants);TARGET_WINDOWS true true + true $(DefineConstants);SYSNETSECURITY_NO_OPENSSL ReferenceAssemblyExclusions.txt + @@ -22,6 +24,10 @@ + + + + @@ -87,7 +93,8 @@ + Link="Common\System\Net\NTAuthentication.Common.cs" + Condition="'$(UseManagedNtlm)' != 'true'" /> - + - - - - + + + + + @@ -435,4 +444,8 @@ + + + + diff --git a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.cs b/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.cs index af10117c3adf1..f67274fe139bd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.cs @@ -9,99 +9,18 @@ namespace System.Net { - [UnsupportedOSPlatform("tvos")] internal sealed partial class NTAuthentication { - internal string? AssociatedName - { - get - { - if (!(IsValidContext && IsCompleted)) - { - throw new Win32Exception((int)SecurityStatusPalErrorCode.InvalidHandle); - } + internal bool IsConfidentialityFlag => (_contextFlags & ContextFlagsPal.Confidentiality) != 0; - string? name = NegotiateStreamPal.QueryContextAssociatedName(_securityContext!); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"NTAuthentication: The context is associated with [{name}]"); - return name; - } - } + internal bool IsIntegrityFlag => (_contextFlags & (IsServer ? ContextFlagsPal.AcceptIntegrity : ContextFlagsPal.InitIntegrity)) != 0; - internal bool IsConfidentialityFlag - { - get - { - return (_contextFlags & ContextFlagsPal.Confidentiality) != 0; - } - } + internal bool IsMutualAuthFlag => (_contextFlags & ContextFlagsPal.MutualAuth) != 0; - internal bool IsIntegrityFlag - { - get - { - return (_contextFlags & (_isServer ? ContextFlagsPal.AcceptIntegrity : ContextFlagsPal.InitIntegrity)) != 0; - } - } + internal bool IsDelegationFlag => (_contextFlags & ContextFlagsPal.Delegate) != 0; - internal bool IsMutualAuthFlag - { - get - { - return (_contextFlags & ContextFlagsPal.MutualAuth) != 0; - } - } + internal bool IsIdentifyFlag => (_contextFlags & (IsServer ? ContextFlagsPal.AcceptIdentify : ContextFlagsPal.InitIdentify)) != 0; - internal bool IsDelegationFlag - { - get - { - return (_contextFlags & ContextFlagsPal.Delegate) != 0; - } - } - - internal bool IsIdentifyFlag - { - get - { - return (_contextFlags & (_isServer ? ContextFlagsPal.AcceptIdentify : ContextFlagsPal.InitIdentify)) != 0; - } - } - - internal string? Spn - { - get - { - return _spn; - } - } - - internal bool IsNTLM - { - get - { - if (_lastProtocolName == null) - { - _lastProtocolName = ProtocolName; - } - - return (object)_lastProtocolName == (object)NegotiationInfoClass.NTLM; - } - } - - internal int Encrypt(ReadOnlySpan buffer, [NotNull] ref byte[]? output, uint sequenceNumber) - { - return NegotiateStreamPal.Encrypt( - _securityContext!, - buffer, - IsConfidentialityFlag, - IsNTLM, - ref output, - sequenceNumber); - } - - internal int Decrypt(byte[] payload, int offset, int count, out int newOffset, uint expectedSeqNumber) - { - return NegotiateStreamPal.Decrypt(_securityContext!, payload, offset, count, IsConfidentialityFlag, IsNTLM, out newOffset, expectedSeqNumber); - } + internal string? Spn => _spn; } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs new file mode 100644 index 0000000000000..1592c94ea65a1 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs @@ -0,0 +1,326 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Diagnostics; +using System.Security.Principal; + +namespace System.Net.Security +{ + /// + /// Represents a stateful authentication exchange that uses the Negotiate, NTLM or Kerberos security protocols + /// to authenticate the client or server, in client-server communication. + /// + public sealed class NegotiateAuthentication : IDisposable + { + private readonly NTAuthentication? _ntAuthentication; + private readonly string _requestedPackage; + private readonly bool _isServer; + private IIdentity? _remoteIdentity; + + /// + /// Initializes a new instance of the + /// for client-side authentication session. + /// + /// The property bag for the authentication options. + public NegotiateAuthentication(NegotiateAuthenticationClientOptions clientOptions) + { + ArgumentNullException.ThrowIfNull(clientOptions); + + ContextFlagsPal contextFlags = clientOptions.RequiredProtectionLevel switch + { + ProtectionLevel.Sign => ContextFlagsPal.InitIntegrity, + ProtectionLevel.EncryptAndSign => ContextFlagsPal.InitIntegrity | ContextFlagsPal.Confidentiality, + _ => 0 + } | ContextFlagsPal.Connection; + + _isServer = false; + _requestedPackage = clientOptions.Package; + try + { + _ntAuthentication = new NTAuthentication( + isServer: false, + clientOptions.Package, + clientOptions.Credential, + clientOptions.TargetName, + contextFlags, + clientOptions.Binding); + } + catch (PlatformNotSupportedException) // Managed implementation, Unix + { + } + catch (NotSupportedException) // Windows implementation + { + } + catch (Win32Exception) // Unix implementation in native layer + { + } + } + + /// + /// Initializes a new instance of the + /// for server-side authentication session. + /// + /// The property bag for the authentication options. + public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOptions) + { + ArgumentNullException.ThrowIfNull(serverOptions); + + ContextFlagsPal contextFlags = serverOptions.RequiredProtectionLevel switch + { + ProtectionLevel.Sign => ContextFlagsPal.AcceptIntegrity, + ProtectionLevel.EncryptAndSign => ContextFlagsPal.AcceptIntegrity | ContextFlagsPal.Confidentiality, + _ => 0 + } | ContextFlagsPal.Connection; + + _isServer = true; + _requestedPackage = serverOptions.Package; + try + { + _ntAuthentication = new NTAuthentication( + isServer: true, + serverOptions.Package, + serverOptions.Credential, + null, + contextFlags, + serverOptions.Binding); + } + catch (PlatformNotSupportedException) // Managed implementation, Unix + { + } + catch (NotSupportedException) // Windows implementation + { + } + catch (Win32Exception) // Unix implementation in native layer + { + } + } + + /// + /// Releases the unmanaged resources used by the + /// and optionally releases the managed resources. + /// + public void Dispose() + { + _ntAuthentication?.CloseContext(); + if (_remoteIdentity is IDisposable disposableRemoteIdentity) + { + disposableRemoteIdentity.Dispose(); + } + } + + /// + /// Indicates whether authentication was successfully completed and the session + /// was established. + /// + public bool IsAuthenticated => _ntAuthentication?.IsCompleted ?? false; + + /// + /// Indicates the negotiated level of protection. + /// + /// + /// The negotiated level of protection is only available when the session + /// authentication was finished (see ). The + /// protection level can be higher than the initially requested protection + /// level specified by or + /// . + /// + public ProtectionLevel ProtectionLevel => + !IsSigned ? ProtectionLevel.None : + !IsEncrypted ? ProtectionLevel.Sign : + ProtectionLevel.EncryptAndSign; + + /// + /// Indicates whether data signing was negotiated. + /// + public bool IsSigned => _ntAuthentication?.IsIntegrityFlag ?? false; + + /// + /// Indicates whether data encryption was negotiated. + /// + public bool IsEncrypted => _ntAuthentication?.IsConfidentialityFlag ?? false; + + /// + /// Indicates whether both server and client have been authenticated. + /// + public bool IsMutuallyAuthenticated => _ntAuthentication?.IsMutualAuthFlag ?? false; + + /// + /// Indicates whether the local side of the authentication is representing + /// the server. + /// + public bool IsServer => _isServer; + + /// + /// Name of the negotiated authentication package. + /// + /// + /// The negotiated authentication package is only available when the session + /// authentication was finished (see ). For + /// unfinished authentication sessions the value is undefined and usually + /// returns the initial authentication package name specified in + /// or + /// . + /// + /// If the Negotiate package was used for authentication the value of this + /// property will be Kerberos, NTLM, or any other specific protocol that was + /// negotiated between both sides of the authentication. + /// + public string Package => _ntAuthentication?.ProtocolName ?? _requestedPackage; + + /// + /// Gets target name (service principal name) of the server. + /// + /// + /// For server-side of the authentication the property returns the target name + /// specified by the client after successful authentication (see ). + /// + /// For client-side of the authentication the property returns the target name + /// specified in . + /// + public string? TargetName => IsServer ? _ntAuthentication?.ClientSpecifiedSpn : _ntAuthentication?.Spn; + + /// + /// Gets information about the identity of the remote party. + /// + /// + /// An object that describes the identity of the remote endpoint. + /// + /// Authentication failed or has not occurred. + /// System error occurred when trying to retrieve the identity. + public IIdentity RemoteIdentity + { + get + { + IIdentity? identity = _remoteIdentity; + if (identity is null) + { + if (!IsAuthenticated || _ntAuthentication == null) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + if (IsServer) + { + Debug.Assert(!OperatingSystem.IsTvOS(), "Server authentication is not supported on tvOS"); + _remoteIdentity = identity = NegotiateStreamPal.GetIdentity(_ntAuthentication); + } + else + { + return new GenericIdentity(TargetName ?? string.Empty, Package); + } + } + return identity; + } + } + + /// + /// Evaluates an authentication token sent by the other party and returns a token in response. + /// + /// Incoming authentication token, or empty value when initiating the authentication exchange. + /// Status code returned by the authentication provider. + /// Outgoing authentication token to be sent to the other party. + /// + /// When initiating the authentication exchange, one of the parties starts + /// with an empty incomingBlob parameter. + /// + /// Successful step of the authentication returns either + /// or status codes. + /// Any other status code indicates an unrecoverable error. + /// + /// When is returned the + /// return value is an authentication token to be transported to the other party. + /// + public byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + if (_ntAuthentication == null) + { + // Unsupported protocol + statusCode = NegotiateAuthenticationStatusCode.Unsupported; + return null; + } + + byte[]? blob = _ntAuthentication.GetOutgoingBlob(incomingBlob, false, out SecurityStatusPal securityStatus); + + // Map error codes + statusCode = securityStatus.ErrorCode switch + { + SecurityStatusPalErrorCode.OK => NegotiateAuthenticationStatusCode.Completed, + SecurityStatusPalErrorCode.ContinueNeeded => NegotiateAuthenticationStatusCode.ContinueNeeded, + + // These code should never be returned and they should be handled internally + SecurityStatusPalErrorCode.CompleteNeeded => NegotiateAuthenticationStatusCode.Completed, + SecurityStatusPalErrorCode.CompAndContinue => NegotiateAuthenticationStatusCode.ContinueNeeded, + + SecurityStatusPalErrorCode.ContextExpired => NegotiateAuthenticationStatusCode.ContextExpired, + SecurityStatusPalErrorCode.Unsupported => NegotiateAuthenticationStatusCode.Unsupported, + SecurityStatusPalErrorCode.PackageNotFound => NegotiateAuthenticationStatusCode.Unsupported, + SecurityStatusPalErrorCode.CannotInstall => NegotiateAuthenticationStatusCode.Unsupported, + SecurityStatusPalErrorCode.InvalidToken => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.QopNotSupported => NegotiateAuthenticationStatusCode.QopNotSupported, + SecurityStatusPalErrorCode.NoImpersonation => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.LogonDenied => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.UnknownCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.NoCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.MessageAltered => NegotiateAuthenticationStatusCode.MessageAltered, + SecurityStatusPalErrorCode.OutOfSequence => NegotiateAuthenticationStatusCode.OutOfSequence, + SecurityStatusPalErrorCode.NoAuthenticatingAuthority => NegotiateAuthenticationStatusCode.InvalidCredentials, + SecurityStatusPalErrorCode.IncompleteCredentials => NegotiateAuthenticationStatusCode.InvalidCredentials, + SecurityStatusPalErrorCode.IllegalMessage => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.CertExpired => NegotiateAuthenticationStatusCode.CredentialsExpired, + SecurityStatusPalErrorCode.SecurityQosFailed => NegotiateAuthenticationStatusCode.QopNotSupported, + SecurityStatusPalErrorCode.UnsupportedPreauth => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.BadBinding => NegotiateAuthenticationStatusCode.BadBinding, + SecurityStatusPalErrorCode.UntrustedRoot => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.SmartcardLogonRequired => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.WrongPrincipal => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.CannotPack => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.TimeSkew => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.AlgorithmMismatch => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.CertUnknown => NegotiateAuthenticationStatusCode.UnknownCredentials, + + // Processing partial inputs is not supported, so this is result of incorrect input + SecurityStatusPalErrorCode.IncompleteMessage => NegotiateAuthenticationStatusCode.InvalidToken, + + _ => NegotiateAuthenticationStatusCode.GenericFailure, + }; + + return blob; + } + + /// + /// Evaluates an authentication token sent by the other party and returns a token in response. + /// + /// Incoming authentication token, or empty value when initiating the authentication exchange. Encoded as base64. + /// Status code returned by the authentication provider. + /// Outgoing authentication token to be sent to the other party, encoded as base64. + /// + /// When initiating the authentication exchange, one of the parties starts + /// with an empty incomingBlob parameter. + /// + /// Successful step of the authentication returns either + /// or status codes. + /// Any other status code indicates an unrecoverable error. + /// + /// When is returned the + /// return value is an authentication token to be transported to the other party. + /// + public string? GetOutgoingBlob(string? incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + byte[]? decodedIncomingBlob = null; + if (!string.IsNullOrEmpty(incomingBlob)) + { + decodedIncomingBlob = Convert.FromBase64String(incomingBlob); + } + byte[]? decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, out statusCode); + + string? outgoingBlob = null; + if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) + { + outgoingBlob = Convert.ToBase64String(decodedOutgoingBlob); + } + + return outgoingBlob; + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationClientOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationClientOptions.cs new file mode 100644 index 0000000000000..e1c17698d8df1 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationClientOptions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Authentication.ExtendedProtection; + +namespace System.Net.Security +{ + /// + /// Represents a propery bag for client-side of an authentication exchange. + /// + public class NegotiateAuthenticationClientOptions + { + /// + /// Specifies the GSSAPI authentication package used for the authentication. + /// Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate. + /// + public string Package { get; set; } = NegotiationInfoClass.Negotiate; + + /// + /// The NetworkCredential that is used to establish the identity of the client. + /// Default value is CredentialCache.DefaultNetworkCredentials. + /// + public NetworkCredential Credential { get; set; } = CredentialCache.DefaultNetworkCredentials; + + /// + /// The Service Principal Name (SPN) that uniquely identifies the server to authenticate. + /// + public string? TargetName { get; set; } + + /// + /// Channel binding that is used for extended protection. + /// + public ChannelBinding? Binding { get; set; } + + /// + /// Indicates the required level of protection of the authentication exchange + /// and any further data exchange. Default value is None. + /// + public ProtectionLevel RequiredProtectionLevel { get; set; } = ProtectionLevel.None; + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationServerOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationServerOptions.cs new file mode 100644 index 0000000000000..6cb4919355802 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationServerOptions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Authentication.ExtendedProtection; + +namespace System.Net.Security +{ + /// + /// Represents a propery bag for server-side of an authentication exchange. + /// + public class NegotiateAuthenticationServerOptions + { + /// + /// Specifies the GSSAPI authentication package used for the authentication. + /// Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate. + /// + public string Package { get; set; } = NegotiationInfoClass.Negotiate; + + /// + /// The NetworkCredential that is used to establish the identity of the client. + /// Default value is CredentialCache.DefaultNetworkCredentials. + /// + public NetworkCredential Credential { get; set; } = CredentialCache.DefaultNetworkCredentials; + + /// + /// Channel binding that is used for extended protection. + /// + public ChannelBinding? Binding { get; set; } + + /// + /// Indicates the required level of protection of the authentication exchange + /// and any further data exchange. Default value is None. + /// + public ProtectionLevel RequiredProtectionLevel { get; set; } = ProtectionLevel.None; + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationStatusCode.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationStatusCode.cs new file mode 100644 index 0000000000000..62f8227932b81 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationStatusCode.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Security +{ + /// + /// Represents a status code for single step of an authentication exchange. + /// + public enum NegotiateAuthenticationStatusCode + { + /// Operation completed successfully. + /// Maps to GSS_S_COMPLETE status in GSSAPI. + Completed = 0, + + /// Operation completed successfully but more tokens are to be exchanged with the other party. + /// Maps to GSS_S_CONTINUE_NEEDED status in GSSAPI. + ContinueNeeded, + + /// Operation resulted in failure but not specific error code was given. + /// Maps to GSS_S_FAILURE status in GSSAPI. + GenericFailure, + + /// Channel binding mismatch between client and server. + /// Maps to GSS_S_BAD_BINDINGS status in GSSAPI. + BadBinding, + + /// Unsupported authentication package was requested. + /// Maps to GSS_S_BAD_MECH status in GSSAPI. + Unsupported, + + /// Message was altered and failed an integrity check validation. + /// Maps to GSS_S_BAD_SIG or GSS_S_BAD_MIC status in GSSAPI. + MessageAltered, + + /// Referenced authentication context has expired. + /// Maps to GSS_S_CONTEXT_EXPIRED status in GSSAPI. + ContextExpired, + + /// Authentication credentials have expired. + /// Maps to GSS_S_CREDENTIALS_EXPIRED status in GSSAPI. + CredentialsExpired, + + /// Consistency checks performed on the credential failed. + /// Maps to GSS_S_DEFECTIVE_CREDENTIAL status in GSSAPI. + InvalidCredentials, + + /// Checks performed on the authentication token failed. + /// Maps to GSS_S_DEFECTIVE_TOKEN status in GSSAPI. + InvalidToken, + + /// The supplied credentials were not valid for context acceptance, or the credential handle did not reference any credentials. + /// Maps to GSS_S_NO_CRED status in GSSAPI. + UnknownCredentials, + + /// Requested protection level is not supported. + /// Maps to GSS_S_BAD_QOP status in GSSAPI. + QopNotSupported, + + /// Authentication token was identfied as duplicate, old, or out of expected sequence. + /// Maps to GSS_S_DUPLICATE_TOKEN, GSS_S_OLD_TOKEN, GSS_S_UNSEQ_TOKEN, and GSS_S_GAP_TOKEN status bits in GSSAPI when failure was indicated. + OutOfSequence + } +} 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.PlatformNotSupported.cs index b41a28ccb9e10..841a3c59f25d9 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.PlatformNotSupported.cs @@ -20,12 +20,12 @@ internal static IIdentity GetIdentity(NTAuthentication context) throw new PlatformNotSupportedException(); } - internal static string QueryContextAssociatedName(SafeDeleteContext? securityContext) + internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) { - throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); + throw new PlatformNotSupportedException(); } - internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) + internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode) { throw new PlatformNotSupportedException(); } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Unix.cs index 720fae1a6d01f..b1288fce48655 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Unix.cs @@ -29,12 +29,6 @@ internal static IIdentity GetIdentity(NTAuthentication context) } return new GenericIdentity(name, protocol); - - } - - internal static string QueryContextAssociatedName(SafeDeleteContext? securityContext) - { - throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); } internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Windows.cs index 10ba2f5c772a7..1fb9995d90dd6 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Windows.cs @@ -21,7 +21,7 @@ internal static partial class NegotiateStreamPal internal static IIdentity GetIdentity(NTAuthentication context) { IIdentity? result; - string name = context.IsServer ? context.AssociatedName! : context.Spn!; + string? name = context.IsServer ? null : context.Spn; string protocol = context.ProtocolName; if (context.IsServer) @@ -29,13 +29,15 @@ internal static IIdentity GetIdentity(NTAuthentication context) SecurityContextTokenHandle? token = null; try { - SecurityStatusPal status; - SafeDeleteContext? securityContext = context.GetContext(out status); + SafeDeleteContext? securityContext = context.GetContext(out SecurityStatusPal status); if (status.ErrorCode != SecurityStatusPalErrorCode.OK) { throw new Win32Exception((int)SecurityStatusAdapterPal.GetInteropFromSecurityStatusPal(status)); } + name = QueryContextAssociatedName(securityContext!); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(context, $"NTAuthentication: The context is associated with [{name}]"); + // This will return a client token when conducted authentication on server side. // This token can be used for impersonation. We use it to create a WindowsIdentity and hand it out to the server app. Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.QuerySecurityContextToken( @@ -64,15 +66,10 @@ internal static IIdentity GetIdentity(NTAuthentication context) } // On the client we don't have access to the remote side identity. - result = new GenericIdentity(name, protocol); + result = new GenericIdentity(name ?? string.Empty, protocol); return result; } - internal static string? QueryContextAssociatedName(SafeDeleteContext securityContext) - { - return SSPIWrapper.QueryStringContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_NAMES); - } - internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) { if (impersonationLevel != TokenImpersonationLevel.Identification && @@ -82,201 +79,5 @@ internal static void ValidateImpersonationLevel(TokenImpersonationLevel imperson throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), SR.net_auth_supported_impl_levels); } } - - internal static int Encrypt( - SafeDeleteContext securityContext, - ReadOnlySpan buffer, - bool isConfidential, - bool isNtlm, - [NotNull] ref byte[]? output, - uint sequenceNumber) - { - 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)); - } - - 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)); - - // Prepare buffers TOKEN(signature), DATA and Padding. - ThreeSecurityBuffers buffers = default; - var securityBuffer = MemoryMarshal.CreateSpan(ref buffers._item0, 3); - securityBuffer[0] = new SecurityBuffer(output, 4, sizes.cbSecurityTrailer, SecurityBufferType.SECBUFFER_TOKEN); - securityBuffer[1] = new SecurityBuffer(output, 4 + sizes.cbSecurityTrailer, buffer.Length, SecurityBufferType.SECBUFFER_DATA); - securityBuffer[2] = new SecurityBuffer(output, 4 + sizes.cbSecurityTrailer + buffer.Length, sizes.cbBlockSize, SecurityBufferType.SECBUFFER_PADDING); - - int errorCode; - if (isConfidential) - { - errorCode = SSPIWrapper.EncryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); - } - else - { - if (isNtlm) - { - securityBuffer[1].type |= SecurityBufferType.SECBUFFER_READONLY; - } - - errorCode = SSPIWrapper.MakeSignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, 0); - } - - if (errorCode != 0) - { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw e; - } - - // Compacting the result. - resultSize = securityBuffer[0].size; - bool forceCopy = false; - if (resultSize != sizes.cbSecurityTrailer) - { - forceCopy = true; - Buffer.BlockCopy(output, securityBuffer[1].offset, output, 4 + resultSize, securityBuffer[1].size); - } - - resultSize += securityBuffer[1].size; - if (securityBuffer[2].size != 0 && (forceCopy || resultSize != (buffer.Length + sizes.cbSecurityTrailer))) - { - Buffer.BlockCopy(output, securityBuffer[2].offset, output, 4 + resultSize, securityBuffer[2].size); - } - - resultSize += securityBuffer[2].size; - unchecked - { - output[0] = (byte)((resultSize) & 0xFF); - output[1] = (byte)(((resultSize) >> 8) & 0xFF); - output[2] = (byte)(((resultSize) >> 16) & 0xFF); - output[3] = (byte)(((resultSize) >> 24) & 0xFF); - } - - return resultSize + 4; - } - - internal static int Decrypt( - SafeDeleteContext securityContext, - byte[]? buffer, - int offset, - int count, - bool isConfidential, - bool isNtlm, - out int newOffset, - uint sequenceNumber) - { - if (offset < 0 || offset > (buffer == null ? 0 : buffer.Length)) - { - Debug.Fail("Argument 'offset' out of range."); - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0 || count > (buffer == null ? 0 : buffer.Length - offset)) - { - Debug.Fail("Argument 'count' out of range."); - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (isNtlm) - { - return DecryptNtlm(securityContext, buffer, offset, count, isConfidential, out newOffset, sequenceNumber); - } - - // - // Kerberos and up - // - TwoSecurityBuffers buffers = default; - var securityBuffer = MemoryMarshal.CreateSpan(ref buffers._item0, 2); - securityBuffer[0] = new SecurityBuffer(buffer, offset, count, SecurityBufferType.SECBUFFER_STREAM); - securityBuffer[1] = new SecurityBuffer(0, SecurityBufferType.SECBUFFER_DATA); - - int errorCode; - if (isConfidential) - { - errorCode = SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); - } - else - { - errorCode = SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); - } - - if (errorCode != 0) - { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw e; - } - - if (securityBuffer[1].type != SecurityBufferType.SECBUFFER_DATA) - { - throw new InternalException(securityBuffer[1].type); - } - - newOffset = securityBuffer[1].offset; - return securityBuffer[1].size; - } - - private static int DecryptNtlm( - SafeDeleteContext securityContext, - byte[]? buffer, - int offset, - int count, - bool isConfidential, - out int newOffset, - uint sequenceNumber) - { - const int ntlmSignatureLength = 16; - // For the most part the arguments are verified in Decrypt(). - if (count < ntlmSignatureLength) - { - Debug.Fail("Argument 'count' out of range."); - throw new ArgumentOutOfRangeException(nameof(count)); - } - - TwoSecurityBuffers buffers = default; - var securityBuffer = MemoryMarshal.CreateSpan(ref buffers._item0, 2); - securityBuffer[0] = new SecurityBuffer(buffer, offset, ntlmSignatureLength, SecurityBufferType.SECBUFFER_TOKEN); - securityBuffer[1] = new SecurityBuffer(buffer, offset + ntlmSignatureLength, count - ntlmSignatureLength, SecurityBufferType.SECBUFFER_DATA); - - int errorCode; - SecurityBufferType realDataType = SecurityBufferType.SECBUFFER_DATA; - - if (isConfidential) - { - errorCode = SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); - } - else - { - realDataType |= SecurityBufferType.SECBUFFER_READONLY; - securityBuffer[1].type = realDataType; - errorCode = SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); - } - - if (errorCode != 0) - { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw new Win32Exception(errorCode); - } - - if (securityBuffer[1].type != realDataType) - { - throw new InternalException(securityBuffer[1].type); - } - - newOffset = securityBuffer[1].offset; - return securityBuffer[1].size; - } } } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs b/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs index 43cced820c7cf..0de9ec03fc18c 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs @@ -17,96 +17,8 @@ public class NTAuthenticationTests private static bool IsNtlmInstalled => Capability.IsNtlmInstalled(); private static NetworkCredential s_testCredentialRight = new NetworkCredential("rightusername", "rightpassword"); - private static NetworkCredential s_testCredentialWrong = new NetworkCredential("rightusername", "wrongpassword"); private static readonly byte[] s_Hello = "Hello"u8.ToArray(); - [Fact] - public void NtlmProtocolExampleTest() - { - // Mirrors the NTLMv2 example in the NTLM specification: - NetworkCredential credential = new NetworkCredential("User", "Password", "Domain"); - FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(credential); - fakeNtlmServer.SendTimestamp = false; - fakeNtlmServer.TargetIsServer = true; - fakeNtlmServer.PreferUnicode = false; - - // NEGOTIATE_MESSAGE - // Flags: - // NTLMSSP_NEGOTIATE_KEY_EXCH - // NTLMSSP_NEGOTIATE_56 - // NTLMSSP_NEGOTIATE_128 - // NTLMSSP_NEGOTIATE_VERSION - // NTLMSSP_NEGOTIATE_TARGET_INFO - // NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY - // NTLMSSP_TARGET_TYPE_SERVER - // NTLMSSP_NEGOTIATE_ALWAYS_SIGN - // NTLMSSP_NEGOTIATE_NTLM - // NTLMSSP_NEGOTIATE_SEAL - // NTLMSSP_NEGOTIATE_SIGN - // NTLMSSP_NEGOTIATE_OEM - // NTLMSSP_NEGOTIATE_UNICODE - // Domain: (empty) (should be "Domain" but the fake server doesn't check) - // Workstation: (empty) (should be "COMPUTER" but the fake server doesn't check) - // Version: 6.1.7600 / 15 - byte[] negotiateBlob = Convert.FromHexString("4e544c4d535350000100000033828ae2000000000000000000000000000000000601b01d0000000f"); - byte[]? challengeBlob = fakeNtlmServer.GetOutgoingBlob(negotiateBlob); - - // CHALLENGE_MESSAGE from 4.2.4.3 Messages - byte[] expectedChallengeBlob = Convert.FromHexString( - "4e544c4d53535000020000000c000c003800000033828ae20123456789abcdef" + - "00000000000000002400240044000000060070170000000f5300650072007600" + - "6500720002000c0044006f006d00610069006e0001000c005300650072007600" + - "6500720000000000"); - Assert.Equal(expectedChallengeBlob, challengeBlob); - - // AUTHENTICATE_MESSAGE from 4.2.4.3 Messages - byte[] authenticateBlob = Convert.FromHexString( - "4e544c4d5353500003000000180018006c00000054005400840000000c000c00" + - "480000000800080054000000100010005c00000010001000d8000000358288e2" + - "0501280a0000000f44006f006d00610069006e00550073006500720043004f00" + - "4d005000550054004500520086c35097ac9cec102554764a57cccc19aaaaaaaa" + - "aaaaaaaa68cd0ab851e51c96aabc927bebef6a1c010100000000000000000000" + - "00000000aaaaaaaaaaaaaaaa0000000002000c0044006f006d00610069006e00" + - "01000c005300650072007600650072000000000000000000c5dad2544fc97990" + - "94ce1ce90bc9d03e"); - byte[]? empty = fakeNtlmServer.GetOutgoingBlob(authenticateBlob); - Assert.Null(empty); - Assert.True(fakeNtlmServer.IsAuthenticated); - Assert.False(fakeNtlmServer.IsMICPresent); - } - - [ConditionalFact(nameof(IsNtlmInstalled))] - public void NtlmCorrectExchangeTest() - { - FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight); - NTAuthentication ntAuth = new NTAuthentication( - isServer: false, "NTLM", s_testCredentialRight, "HTTP/foo", - ContextFlagsPal.Connection | ContextFlagsPal.InitIntegrity, null); - - DoNtlmExchange(fakeNtlmServer, ntAuth); - - Assert.True(fakeNtlmServer.IsAuthenticated); - // NTLMSSP on Linux doesn't send the MIC and sends incorrect SPN (drops the service prefix) - if (!OperatingSystem.IsLinux()) - { - Assert.True(fakeNtlmServer.IsMICPresent); - Assert.Equal("HTTP/foo", fakeNtlmServer.ClientSpecifiedSpn); - } - } - - [ConditionalFact(nameof(IsNtlmInstalled))] - public void NtlmIncorrectExchangeTest() - { - FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight); - NTAuthentication ntAuth = new NTAuthentication( - isServer: false, "NTLM", s_testCredentialWrong, "HTTP/foo", - ContextFlagsPal.Connection | ContextFlagsPal.InitIntegrity, null); - - DoNtlmExchange(fakeNtlmServer, ntAuth); - - Assert.False(fakeNtlmServer.IsAuthenticated); - } - [ConditionalFact(nameof(IsNtlmInstalled))] [ActiveIssue("https://github.com/dotnet/runtime/issues/65678", TestPlatforms.OSX | TestPlatforms.iOS | TestPlatforms.MacCatalyst)] public void NtlmSignatureTest() @@ -153,50 +65,5 @@ private void DoNtlmExchange(FakeNtlmServer fakeNtlmServer, NTAuthentication ntAu byte[]? empty = fakeNtlmServer.GetOutgoingBlob(authenticateBlob); Assert.Null(empty); } - - [ConditionalTheory(nameof(IsNtlmInstalled))] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, false)] - public void NegotiateCorrectExchangeTest(bool requestMIC, bool requestConfidentiality) - { - // Older versions of gss-ntlmssp on Linux generate MIC at incorrect offset unless ForceNegotiateVersion is specified - FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight) { ForceNegotiateVersion = true }; - FakeNegotiateServer fakeNegotiateServer = new FakeNegotiateServer(fakeNtlmServer) { RequestMIC = requestMIC }; - NTAuthentication ntAuth = new NTAuthentication( - isServer: false, "Negotiate", s_testCredentialRight, "HTTP/foo", - ContextFlagsPal.Connection | ContextFlagsPal.InitIntegrity | - (requestConfidentiality ? ContextFlagsPal.Confidentiality : 0), null); - - byte[]? clientBlob = null; - byte[]? serverBlob = null; - do - { - clientBlob = ntAuth.GetOutgoingBlob(serverBlob, throwOnError: false, out SecurityStatusPal status); - if (clientBlob != null) - { - Assert.False(fakeNegotiateServer.IsAuthenticated); - // Send the client blob to the fake server - serverBlob = fakeNegotiateServer.GetOutgoingBlob(clientBlob); - } - - if (status.ErrorCode == SecurityStatusPalErrorCode.OK) - { - Assert.True(ntAuth.IsCompleted); - Assert.True(fakeNegotiateServer.IsAuthenticated); - Assert.True(fakeNtlmServer.IsAuthenticated); - } - else if (status.ErrorCode == SecurityStatusPalErrorCode.ContinueNeeded) - { - Assert.NotNull(clientBlob); - Assert.NotNull(serverBlob); - } - else - { - Assert.Fail(status.ErrorCode.ToString()); - } - } - while (!ntAuth.IsCompleted); - } } } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs new file mode 100644 index 0000000000000..4cc089db463b5 --- /dev/null +++ b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Net.Security; +using System.Text; +using System.Threading.Tasks; +using System.Net.Test.Common; +using Xunit; + +namespace System.Net.Security.Tests +{ + public class NegotiateAuthenticationTests + { + private static bool IsNtlmAvailable => Capability.IsNtlmInstalled() || OperatingSystem.IsAndroid() || OperatingSystem.IsTvOS(); + private static bool IsNtlmUnavailable => !IsNtlmAvailable; + + private static NetworkCredential s_testCredentialRight = new NetworkCredential("rightusername", "rightpassword"); + private static NetworkCredential s_testCredentialWrong = new NetworkCredential("rightusername", "wrongpassword"); + private static readonly byte[] s_Hello = "Hello"u8.ToArray(); + + [Fact] + public void Constructor_Overloads_Validation() + { + AssertExtensions.Throws("clientOptions", () => { new NegotiateAuthentication((NegotiateAuthenticationClientOptions)null); }); + AssertExtensions.Throws("serverOptions", () => { new NegotiateAuthentication((NegotiateAuthenticationServerOptions)null); }); + } + + [Fact] + public void RemoteIdentity_ThrowsOnUnauthenticated() + { + NegotiateAuthenticationClientOptions clientOptions = new NegotiateAuthenticationClientOptions { Credential = s_testCredentialRight, TargetName = "HTTP/foo" }; + NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication(clientOptions); + Assert.Throws(() => { _ = negotiateAuthentication.RemoteIdentity; }); + } + + [ConditionalFact(nameof(IsNtlmAvailable))] + public void RemoteIdentity_ThrowsOnDisposed() + { + FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight); + NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication( + new NegotiateAuthenticationClientOptions + { + Package = "NTLM", + Credential = s_testCredentialRight, + TargetName = "HTTP/foo", + RequiredProtectionLevel = ProtectionLevel.Sign + }); + + DoNtlmExchange(fakeNtlmServer, negotiateAuthentication); + + Assert.True(fakeNtlmServer.IsAuthenticated); + Assert.True(negotiateAuthentication.IsAuthenticated); + _ = negotiateAuthentication.RemoteIdentity; + + negotiateAuthentication.Dispose(); + Assert.Throws(() => { _ = negotiateAuthentication.RemoteIdentity; }); + } + + [Fact] + public void Package_Unsupported() + { + NegotiateAuthenticationClientOptions clientOptions = new NegotiateAuthenticationClientOptions { Package = "INVALID", Credential = s_testCredentialRight, TargetName = "HTTP/foo" }; + NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication(clientOptions); + NegotiateAuthenticationStatusCode statusCode; + negotiateAuthentication.GetOutgoingBlob((byte[]?)null, out statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.Unsupported, statusCode); + } + + [ConditionalFact(nameof(IsNtlmAvailable))] + public void Package_Supported_NTLM() + { + NegotiateAuthenticationClientOptions clientOptions = new NegotiateAuthenticationClientOptions { Package = "NTLM", Credential = s_testCredentialRight, TargetName = "HTTP/foo" }; + NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication(clientOptions); + NegotiateAuthenticationStatusCode statusCode; + negotiateAuthentication.GetOutgoingBlob((byte[]?)null, out statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + } + + [ConditionalFact(nameof(IsNtlmUnavailable))] + public void Package_Unsupported_NTLM() + { + NegotiateAuthenticationClientOptions clientOptions = new NegotiateAuthenticationClientOptions { Package = "NTLM", Credential = s_testCredentialRight, TargetName = "HTTP/foo" }; + NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication(clientOptions); + NegotiateAuthenticationStatusCode statusCode; + negotiateAuthentication.GetOutgoingBlob((byte[]?)null, out statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.Unsupported, statusCode); + } + + [Fact] + public void NtlmProtocolExampleTest() + { + // Mirrors the NTLMv2 example in the NTLM specification: + NetworkCredential credential = new NetworkCredential("User", "Password", "Domain"); + FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(credential); + fakeNtlmServer.SendTimestamp = false; + fakeNtlmServer.TargetIsServer = true; + fakeNtlmServer.PreferUnicode = false; + + // NEGOTIATE_MESSAGE + // Flags: + // NTLMSSP_NEGOTIATE_KEY_EXCH + // NTLMSSP_NEGOTIATE_56 + // NTLMSSP_NEGOTIATE_128 + // NTLMSSP_NEGOTIATE_VERSION + // NTLMSSP_NEGOTIATE_TARGET_INFO + // NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY + // NTLMSSP_TARGET_TYPE_SERVER + // NTLMSSP_NEGOTIATE_ALWAYS_SIGN + // NTLMSSP_NEGOTIATE_NTLM + // NTLMSSP_NEGOTIATE_SEAL + // NTLMSSP_NEGOTIATE_SIGN + // NTLMSSP_NEGOTIATE_OEM + // NTLMSSP_NEGOTIATE_UNICODE + // Domain: (empty) (should be "Domain" but the fake server doesn't check) + // Workstation: (empty) (should be "COMPUTER" but the fake server doesn't check) + // Version: 6.1.7600 / 15 + byte[] negotiateBlob = Convert.FromHexString("4e544c4d535350000100000033828ae2000000000000000000000000000000000601b01d0000000f"); + byte[]? challengeBlob = fakeNtlmServer.GetOutgoingBlob(negotiateBlob); + + // CHALLENGE_MESSAGE from 4.2.4.3 Messages + byte[] expectedChallengeBlob = Convert.FromHexString( + "4e544c4d53535000020000000c000c003800000033828ae20123456789abcdef" + + "00000000000000002400240044000000060070170000000f5300650072007600" + + "6500720002000c0044006f006d00610069006e0001000c005300650072007600" + + "6500720000000000"); + Assert.Equal(expectedChallengeBlob, challengeBlob); + + // AUTHENTICATE_MESSAGE from 4.2.4.3 Messages + byte[] authenticateBlob = Convert.FromHexString( + "4e544c4d5353500003000000180018006c00000054005400840000000c000c00" + + "480000000800080054000000100010005c00000010001000d8000000358288e2" + + "0501280a0000000f44006f006d00610069006e00550073006500720043004f00" + + "4d005000550054004500520086c35097ac9cec102554764a57cccc19aaaaaaaa" + + "aaaaaaaa68cd0ab851e51c96aabc927bebef6a1c010100000000000000000000" + + "00000000aaaaaaaaaaaaaaaa0000000002000c0044006f006d00610069006e00" + + "01000c005300650072007600650072000000000000000000c5dad2544fc97990" + + "94ce1ce90bc9d03e"); + byte[]? empty = fakeNtlmServer.GetOutgoingBlob(authenticateBlob); + Assert.Null(empty); + Assert.True(fakeNtlmServer.IsAuthenticated); + Assert.False(fakeNtlmServer.IsMICPresent); + } + + [ConditionalFact(nameof(IsNtlmAvailable))] + public void NtlmCorrectExchangeTest() + { + FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight); + NegotiateAuthentication ntAuth = new NegotiateAuthentication( + new NegotiateAuthenticationClientOptions + { + Package = "NTLM", + Credential = s_testCredentialRight, + TargetName = "HTTP/foo", + RequiredProtectionLevel = ProtectionLevel.Sign + }); + + DoNtlmExchange(fakeNtlmServer, ntAuth); + + Assert.True(fakeNtlmServer.IsAuthenticated); + // NTLMSSP on Linux doesn't send the MIC and sends incorrect SPN (drops the service prefix) + if (!OperatingSystem.IsLinux()) + { + Assert.True(fakeNtlmServer.IsMICPresent); + Assert.Equal("HTTP/foo", fakeNtlmServer.ClientSpecifiedSpn); + } + } + + [ConditionalFact(nameof(IsNtlmAvailable))] + public void NtlmIncorrectExchangeTest() + { + FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight); + NegotiateAuthentication ntAuth = new NegotiateAuthentication( + new NegotiateAuthenticationClientOptions + { + Package = "NTLM", + Credential = s_testCredentialWrong, + TargetName = "HTTP/foo", + RequiredProtectionLevel = ProtectionLevel.Sign + }); + + DoNtlmExchange(fakeNtlmServer, ntAuth); + + Assert.False(fakeNtlmServer.IsAuthenticated); + } + + private void DoNtlmExchange(FakeNtlmServer fakeNtlmServer, NegotiateAuthentication ntAuth) + { + NegotiateAuthenticationStatusCode statusCode; + byte[]? negotiateBlob = ntAuth.GetOutgoingBlob((byte[])null, out statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + Assert.NotNull(negotiateBlob); + byte[]? challengeBlob = fakeNtlmServer.GetOutgoingBlob(negotiateBlob); + Assert.NotNull(challengeBlob); + byte[]? authenticateBlob = ntAuth.GetOutgoingBlob(challengeBlob, out statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.Completed, statusCode); + Assert.NotNull(authenticateBlob); + byte[]? empty = fakeNtlmServer.GetOutgoingBlob(authenticateBlob); + Assert.Null(empty); + } + + [ConditionalTheory(nameof(IsNtlmAvailable))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void NegotiateCorrectExchangeTest(bool requestMIC, bool requestConfidentiality) + { + // Older versions of gss-ntlmssp on Linux generate MIC at incorrect offset unless ForceNegotiateVersion is specified + FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight) { ForceNegotiateVersion = true }; + FakeNegotiateServer fakeNegotiateServer = new FakeNegotiateServer(fakeNtlmServer) { RequestMIC = requestMIC }; + NegotiateAuthentication ntAuth = new NegotiateAuthentication( + new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = s_testCredentialRight, + TargetName = "HTTP/foo", + RequiredProtectionLevel = requestConfidentiality ? ProtectionLevel.EncryptAndSign : ProtectionLevel.Sign + }); + + byte[]? clientBlob = null; + byte[]? serverBlob = null; + NegotiateAuthenticationStatusCode statusCode; + do + { + clientBlob = ntAuth.GetOutgoingBlob(serverBlob, out statusCode); + if (clientBlob != null) + { + Assert.False(fakeNegotiateServer.IsAuthenticated); + // Send the client blob to the fake server + serverBlob = fakeNegotiateServer.GetOutgoingBlob(clientBlob); + } + + if (statusCode == NegotiateAuthenticationStatusCode.Completed) + { + Assert.True(ntAuth.IsAuthenticated); + Assert.True(fakeNegotiateServer.IsAuthenticated); + Assert.True(fakeNtlmServer.IsAuthenticated); + } + else if (statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded) + { + Assert.NotNull(clientBlob); + Assert.NotNull(serverBlob); + } + else + { + Assert.Fail(statusCode.ToString()); + } + } + while (!ntAuth.IsAuthenticated); + } + } +} diff --git a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj index 3f61524d58c4c..771a712cc57bb 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj @@ -27,6 +27,7 @@ +