File: System\Net\NegotiateAuthenticationPal.ManagedSpnego.cs
Web Access
Project: src\src\libraries\System.Net.Security\src\System.Net.Security.csproj (System.Net.Security)
// 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;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Formats.Asn1;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
 
namespace System.Net
{
    internal abstract partial class NegotiateAuthenticationPal
    {
        internal sealed class ManagedSpnegoNegotiateAuthenticationPal : NegotiateAuthenticationPal
        {
            // Input parameters
            private readonly NegotiateAuthenticationClientOptions _clientOptions;
 
            // State parameters
            private byte[]? _spnegoMechList;
            private bool _isAuthenticated;
            private bool _supportKerberos;
            private NegotiateAuthenticationPal? _optimisticMechanism;
            private NegotiateAuthenticationPal? _mechanism;
 
            private const string SpnegoOid = "1.3.6.1.5.5.2";
            private const string NtlmOid = "1.3.6.1.4.1.311.2.2.10";
            private const string KerberosOid = "1.2.840.113554.1.2.2";
 
            // rfc4178
            private enum NegotiationToken
            {
                NegTokenInit = 0,
                NegTokenResp = 1
            }
 
            private enum NegTokenInit
            {
                MechTypes = 0,
                ReqFlags = 1,
                MechToken = 2,
                MechListMIC = 3
            }
 
            private enum NegTokenResp
            {
                NegState = 0,
                SupportedMech = 1,
                ResponseToken = 2,
                MechListMIC = 3
            }
 
            private enum NegState
            {
                Unknown = -1,           // Internal. Not in RFC.
                AcceptCompleted = 0,
                AcceptIncomplete = 1,
                Reject = 2,
                RequestMic = 3
            }
 
            public override bool IsAuthenticated => _isAuthenticated && _mechanism?.IsAuthenticated == true;
            public override bool IsSigned => _mechanism?.IsSigned ?? false;
            public override bool IsEncrypted => _mechanism?.IsEncrypted ?? false;
            public override bool IsMutuallyAuthenticated => _mechanism?.IsMutuallyAuthenticated ?? false;
            public override string Package => _mechanism?.Package ?? NegotiationInfoClass.Negotiate;
            public override string? TargetName => _clientOptions.TargetName;
            public override IIdentity RemoteIdentity => _mechanism?.RemoteIdentity ?? throw new InvalidOperationException();
            public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel => _mechanism?.ImpersonationLevel ?? System.Security.Principal.TokenImpersonationLevel.Impersonation;
 
            public ManagedSpnegoNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions, bool supportKerberos = false)
            {
                Debug.Assert(clientOptions.Package == NegotiationInfoClass.Negotiate);
                _clientOptions = clientOptions;
                _supportKerberos = supportKerberos;
            }
 
            public override void Dispose()
            {
                _optimisticMechanism?.Dispose();
                _optimisticMechanism = null;
                _mechanism?.Dispose();
                _mechanism = null;
                _isAuthenticated = false;
            }
 
            public override unsafe byte[]? GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode)
            {
                //Console.WriteLine($"ManagedSpnegoNegotiateAuthenticationPal.GetOutgoingBlob > {Convert.ToBase64String(incomingBlob)}");
 
                byte[]? outgoingBlob;
                if (_spnegoMechList == null)
                {
                    outgoingBlob = CreateSpNegoNegotiateMessage(incomingBlob, out statusCode);
                }
                else
                {
                    outgoingBlob = ProcessSpNegoChallenge(incomingBlob, out statusCode);
                }
 
                //Console.WriteLine($"ManagedSpnegoNegotiateAuthenticationPal.GetOutgoingBlob < {(outgoingBlob == null ? "null" : Convert.ToBase64String(outgoingBlob))} {statusCode}");
 
                return outgoingBlob;
            }
 
            private NegotiateAuthenticationPal CreateMechanismForPackage(string packageName)
            {
                return NegotiateAuthenticationPal.Create(new NegotiateAuthenticationClientOptions
                {
                    Package = packageName,
                    Credential = _clientOptions.Credential,
                    TargetName = _clientOptions.TargetName,
                    Binding = _clientOptions.Binding,
                    RequiredProtectionLevel = _clientOptions.RequiredProtectionLevel,
                    RequireMutualAuthentication = _clientOptions.RequireMutualAuthentication,
                    AllowedImpersonationLevel = _clientOptions.AllowedImpersonationLevel,
                });
            }
 
            private IEnumerable<KeyValuePair<string, string>> EnumerateMechanisms()
            {
                if (_supportKerberos)
                {
                    yield return new KeyValuePair<string, string>(NegotiationInfoClass.Kerberos, KerberosOid);
                }
 
                yield return new KeyValuePair<string, string>(NegotiationInfoClass.NTLM, NtlmOid);
            }
 
            private byte[]? CreateSpNegoNegotiateMessage(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode)
            {
                AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
                using (writer.PushSequence(new Asn1Tag(TagClass.Application, 0)))
                {
                    writer.WriteObjectIdentifier(SpnegoOid);
 
                    // NegTokenInit::= SEQUENCE {
                    //    mechTypes[0] MechTypeList,
                    //    reqFlags[1] ContextFlags OPTIONAL,
                    //       --inherited from RFC 2478 for backward compatibility,
                    //      --RECOMMENDED to be left out
                    //    mechToken[2] OCTET STRING  OPTIONAL,
                    //    mechListMIC[3] OCTET STRING  OPTIONAL,
                    //    ...
                    // }
                    using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenInit)))
                    {
                        using (writer.PushSequence())
                        {
                            byte[]? mechBlob = null;
 
                            // MechType::= OBJECT IDENTIFIER
                            //    -- OID represents each security mechanism as suggested by
                            //   --[RFC2743]
                            //
                            // MechTypeList::= SEQUENCE OF MechType
                            using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenInit.MechTypes)))
                            {
                                AsnWriter mechListWriter = new AsnWriter(AsnEncodingRules.DER);
 
                                using (mechListWriter.PushSequence())
                                {
                                    foreach (KeyValuePair<string, string> packageAndOid in EnumerateMechanisms())
                                    {
                                        if (_optimisticMechanism == null)
                                        {
                                            _optimisticMechanism = CreateMechanismForPackage(packageAndOid.Key);
                                            mechBlob = _optimisticMechanism.GetOutgoingBlob(incomingBlob, out statusCode);
                                            if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded &&
                                                statusCode != NegotiateAuthenticationStatusCode.Completed)
                                            {
                                                mechBlob = null;
                                                _optimisticMechanism?.Dispose();
                                                _optimisticMechanism = null;
                                                if (statusCode != NegotiateAuthenticationStatusCode.Unsupported)
                                                {
                                                    return null;
                                                }
                                                continue;
                                            }
                                        }
 
                                        mechListWriter.WriteObjectIdentifier(packageAndOid.Value);
                                    }
                                }
 
                                _spnegoMechList = mechListWriter.Encode();
                                mechListWriter.CopyTo(writer);
                            }
 
                            if (mechBlob != null)
                            {
                                using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenInit.MechToken)))
                                {
                                    writer.WriteOctetString(mechBlob);
                                }
                            }
                        }
                    }
                }
 
                statusCode = NegotiateAuthenticationStatusCode.ContinueNeeded;
                return writer.Encode();
            }
 
            private byte[]? ProcessSpNegoChallenge(ReadOnlySpan<byte> challenge, out NegotiateAuthenticationStatusCode statusCode)
            {
                NegState state = NegState.Unknown;
                string? mech = null;
                byte[]? blob = null;
                byte[]? mechListMIC = null;
 
                try
                {
                    AsnValueReader reader = new AsnValueReader(challenge, AsnEncodingRules.DER);
                    AsnValueReader challengeReader = reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp));
                    reader.ThrowIfNotEmpty();
 
                    // NegTokenResp ::= SEQUENCE {
                    //    negState[0] ENUMERATED {
                    //        accept - completed(0),
                    //        accept - incomplete(1),
                    //        reject(2),
                    //        request - mic(3)
                    //    } OPTIONAL,
                    // --REQUIRED in the first reply from the target
                    //    supportedMech[1] MechType OPTIONAL,
                    // --present only in the first reply from the target
                    // responseToken[2] OCTET STRING  OPTIONAL,
                    // mechListMIC[3] OCTET STRING  OPTIONAL,
                    // ...
                    // }
 
                    challengeReader = challengeReader.ReadSequence();
 
                    if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState)))
                    {
                        AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState));
                        state = valueReader.ReadEnumeratedValue<NegState>();
                        valueReader.ThrowIfNotEmpty();
                    }
 
                    if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(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)))
                    {
                        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)))
                    {
                        AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC));
                        mechListMIC = valueReader.ReadOctetString();
                        valueReader.ThrowIfNotEmpty();
                    }
 
                    challengeReader.ThrowIfNotEmpty();
                }
                catch (AsnContentException)
                {
                    statusCode = NegotiateAuthenticationStatusCode.InvalidToken;
                    return null;
                }
 
                // Validate and choose the mechanism if necessary
                string? requestedPackage = mech switch
                {
                    NtlmOid => NegotiationInfoClass.NTLM,
                    KerberosOid => NegotiationInfoClass.Kerberos,
                    _ => null
                };
 
                if (_mechanism is null)
                {
                    if (requestedPackage is null)
                    {
                        statusCode = NegotiateAuthenticationStatusCode.Unsupported;
                        return null;
                    }
 
                    if (requestedPackage == _optimisticMechanism?.Package)
                    {
                        _mechanism = _optimisticMechanism;
                    }
                    else
                    {
                        // Abandon the optimistic path and restart with a new mechanism
                        _optimisticMechanism?.Dispose();
                        _mechanism = CreateMechanismForPackage(requestedPackage);
                    }
 
                    _optimisticMechanism = null;
                }
                else
                {
                    if (requestedPackage != null &&
                        _mechanism.Package != requestedPackage)
                    {
                        statusCode = NegotiateAuthenticationStatusCode.InvalidToken;
                        return null;
                    }
                }
 
                if (blob?.Length > 0)
                {
                    // Process decoded blob.
                    byte[]? response = _mechanism.GetOutgoingBlob(blob, out statusCode);
 
                    if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded &&
                        statusCode != NegotiateAuthenticationStatusCode.Completed)
                    {
                        return null;
                    }
 
                    if (response?.Length > 0)
                    {
                        AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
 
                        using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp)))
                        {
                            using (writer.PushSequence())
                            {
                                using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken)))
                                {
                                    writer.WriteOctetString(response);
                                }
 
                                if (statusCode == NegotiateAuthenticationStatusCode.Completed)
                                {
                                    using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC)))
                                    {
                                        ArrayBufferWriter<byte> micBuffer = new ArrayBufferWriter<byte>();
                                        _mechanism.GetMIC(_spnegoMechList, micBuffer);
                                        writer.WriteOctetString(micBuffer.WrittenSpan);
                                    }
                                }
                            }
                        }
 
                        statusCode = state == NegState.RequestMic ? NegotiateAuthenticationStatusCode.ContinueNeeded : NegotiateAuthenticationStatusCode.Completed;
                        _isAuthenticated = statusCode == NegotiateAuthenticationStatusCode.Completed;
                        return writer.Encode();
                    }
                }
 
                // Process MIC if the server sent it.
                //
                // We workaround broken servers that send the mechanism token in the mechListMIC
                // field. This is the same workaround that exists in MIT KRB5 and it's attributed to
                // Windows 2000 bug. It was reported in a .NET issue and tracked down as a bug in
                // IBM Websphere 8.5.5.19 on Java 1.8.
                //
                // References:
                // - https://github.com/krb5/krb5/blame/master/src/lib/gssapi/spnego/spnego_mech.c#L3521-L3525
                // - https://github.com/dotnet/runtime/issues/88874
                // - https://krbdev.mit.edu/rt/Ticket/Display.html?id=6726
                // - https://www.ibm.com/support/pages/apar/IV74044
                if (mechListMIC != null &&
                    !mechListMIC.AsSpan().SequenceEqual(blob.AsSpan()))
                {
                    if (_spnegoMechList == null || state != NegState.AcceptCompleted)
                    {
                        statusCode = NegotiateAuthenticationStatusCode.GenericFailure;
                        return null;
                    }
 
                    if (!_mechanism.VerifyMIC(_spnegoMechList, mechListMIC))
                    {
                        statusCode = NegotiateAuthenticationStatusCode.MessageAltered;
                        return null;
                    }
 
                    (_mechanism as ManagedNtlmNegotiateAuthenticationPal)?.ResetKeys();
                }
 
                _isAuthenticated = state == NegState.AcceptCompleted || state == NegState.Reject;
                statusCode = state switch {
                    NegState.AcceptCompleted => NegotiateAuthenticationStatusCode.Completed,
                    NegState.AcceptIncomplete => NegotiateAuthenticationStatusCode.ContinueNeeded,
                    NegState.Reject => NegotiateAuthenticationStatusCode.UnknownCredentials,
                    _ => NegotiateAuthenticationStatusCode.GenericFailure
                };
 
                return null;
            }
 
            public override NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan<byte> input, IBufferWriter<byte> outputWriter, bool requestEncryption, out bool isEncrypted)
            {
                if (_mechanism is null || !_isAuthenticated)
                {
                    throw new InvalidOperationException(SR.net_auth_noauth);
                }
 
                return _mechanism.Wrap(input, outputWriter, requestEncryption, out isEncrypted);
            }
 
            public override NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan<byte> input, IBufferWriter<byte> outputWriter, out bool wasEncrypted)
            {
                if (_mechanism is null || !_isAuthenticated)
                {
                    throw new InvalidOperationException(SR.net_auth_noauth);
                }
 
                return _mechanism.Unwrap(input, outputWriter, out wasEncrypted);
            }
 
            public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span<byte> input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted)
            {
                if (_mechanism is null || !_isAuthenticated)
                {
                    throw new InvalidOperationException(SR.net_auth_noauth);
                }
 
                return _mechanism.UnwrapInPlace(input, out unwrappedOffset, out unwrappedLength, out wasEncrypted);
            }
 
            public override bool VerifyMIC(ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature)
            {
                if (_mechanism is null || !_isAuthenticated)
                {
                    throw new InvalidOperationException(SR.net_auth_noauth);
                }
 
                return _mechanism.VerifyMIC(message, signature);
            }
 
            public override void GetMIC(ReadOnlySpan<byte> message, IBufferWriter<byte> signature)
            {
                if (_mechanism is null || !_isAuthenticated)
                {
                    throw new InvalidOperationException(SR.net_auth_noauth);
                }
 
                _mechanism.GetMIC(message, signature);
            }
        }
    }
}