| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System; |
| | | 5 | | using System.Buffers; |
| | | 6 | | using System.ComponentModel; |
| | | 7 | | using System.IO; |
| | | 8 | | using System.IO.Pipelines; |
| | | 9 | | using System.Linq; |
| | | 10 | | using System.Reflection; |
| | | 11 | | using System.Security.Authentication.ExtendedProtection; |
| | | 12 | | using System.Security.Principal; |
| | | 13 | | |
| | | 14 | | namespace CoreWCF.Security.NegotiateInternal |
| | | 15 | | { |
| | | 16 | | internal class NTAuthenticationNet8 : INTAuthenticationFacade |
| | | 17 | | { |
| | | 18 | | // value should match the Windows sspicli NTE_FAIL value |
| | | 19 | | // defined in winerror.h |
| | | 20 | | private const int NTE_FAIL = unchecked((int)0x80090020); |
| | | 21 | | |
| | | 22 | | private static readonly Type s_negotiateAuthenticationType; |
| | | 23 | | private static readonly Type s_negotiateAuthenticationStatusCodeType; |
| | | 24 | | private static readonly MethodInfo s_getOutgoingBlob; |
| | | 25 | | private static readonly Delegate s_getOutgoingBlobInvoker; |
| | | 26 | | private static readonly Type s_serverOptionsType; |
| | | 27 | | |
| | | 28 | | static NTAuthenticationNet8() |
| | | 29 | | { |
| | 1 | 30 | | var securityAssembly = typeof(System.Net.Security.NegotiateStream).Assembly; |
| | 1 | 31 | | s_serverOptionsType = securityAssembly.GetType("System.Net.Security.NegotiateAuthenticationServerOptions", t |
| | | 32 | | |
| | 1 | 33 | | s_negotiateAuthenticationType = securityAssembly.GetType("System.Net.Security.NegotiateAuthentication", true |
| | | 34 | | |
| | 1 | 35 | | s_negotiateAuthenticationStatusCodeType = securityAssembly.GetType("System.Net.Security.NegotiateAuthenticat |
| | | 36 | | |
| | 1 | 37 | | s_getOutgoingBlob = s_negotiateAuthenticationType.GetMethods().Single(m => |
| | 23 | 38 | | "GetOutgoingBlob".Equals(m.Name, StringComparison.Ordinal) && |
| | 23 | 39 | | typeof(byte[]).Equals(m.ReturnType)); |
| | | 40 | | |
| | 1 | 41 | | s_getOutgoingBlobInvoker = LambdaExpressionBuilder.BuildFor( |
| | 1 | 42 | | s_negotiateAuthenticationType, |
| | 1 | 43 | | s_getOutgoingBlob).Compile(); |
| | 1 | 44 | | } |
| | | 45 | | |
| | | 46 | | |
| | | 47 | | private static IDisposable CreateNegotiateAuthentication(object serverOptions) |
| | | 48 | | { |
| | 0 | 49 | | object[] parameters = new object[] { serverOptions }; |
| | 0 | 50 | | return (IDisposable)Activator.CreateInstance(s_negotiateAuthenticationType, parameters); |
| | | 51 | | } |
| | | 52 | | |
| | | 53 | | private IDisposable _negotiateAuthentication; |
| | | 54 | | private ChannelBinding _channelBinding; |
| | | 55 | | private ExtendedProtectionPolicy _protectionPolicy; |
| | | 56 | | |
| | 1 | 57 | | public NTAuthenticationNet8() |
| | | 58 | | { |
| | | 59 | | //_negotiateAuthentication = NewNegotiateAuthentication(); |
| | 1 | 60 | | } |
| | | 61 | | |
| | | 62 | | private object CreateNegotiateAuthenticationServerOptions() |
| | | 63 | | { |
| | 0 | 64 | | dynamic serverOptions = Activator.CreateInstance(s_serverOptionsType); |
| | 0 | 65 | | if (_channelBinding != null) serverOptions.ChannelBinding = _channelBinding; |
| | 0 | 66 | | if (_protectionPolicy != null) serverOptions.ExtendedProtectionPolicy = _protectionPolicy; |
| | | 67 | | |
| | 0 | 68 | | return serverOptions; |
| | | 69 | | } |
| | | 70 | | |
| | 0 | 71 | | private dynamic NegotiateAuthentication => _negotiateAuthentication ??= CreateNegotiateAuthentication(CreateNego |
| | | 72 | | |
| | | 73 | | public void SetChannelBinding(ChannelBinding channelBinding) |
| | | 74 | | { |
| | 0 | 75 | | if (channelBinding == null) return; |
| | 0 | 76 | | if (_negotiateAuthentication != null) throw new InvalidOperationException("Channel binding must be set befor |
| | 0 | 77 | | if (_channelBinding != null && channelBinding != null && _channelBinding != channelBinding) throw new Invali |
| | | 78 | | |
| | 0 | 79 | | _channelBinding = channelBinding; |
| | 0 | 80 | | } |
| | | 81 | | |
| | | 82 | | public void SetExtendedProtectionPolicy(ExtendedProtectionPolicy protectionPolicy) |
| | | 83 | | { |
| | 0 | 84 | | if (protectionPolicy == null) return; |
| | 0 | 85 | | if (_negotiateAuthentication != null) throw new InvalidOperationException("Extended Protection Policy must b |
| | 0 | 86 | | if (_protectionPolicy != null && protectionPolicy != null && _protectionPolicy != protectionPolicy) throw ne |
| | | 87 | | |
| | 0 | 88 | | _protectionPolicy = protectionPolicy; |
| | 0 | 89 | | } |
| | | 90 | | |
| | | 91 | | // https://learn.microsoft.com/en-us/dotnet/api/system.net.security.negotiateauthentication.isauthenticated?view |
| | 0 | 92 | | public bool IsCompleted => ((dynamic)NegotiateAuthentication).IsAuthenticated; |
| | | 93 | | |
| | | 94 | | // https://learn.microsoft.com/en-us/dotnet/api/system.net.security.negotiateauthentication.package?view=net-8.0 |
| | 0 | 95 | | public string Protocol => ((dynamic)NegotiateAuthentication).Package; |
| | | 96 | | |
| | 0 | 97 | | public bool IsValidContext { get; private set; } = false; |
| | | 98 | | |
| | | 99 | | public byte[] Encrypt(byte[] input) |
| | | 100 | | { |
| | | 101 | | // https://learn.microsoft.com/en-us/dotnet/api/system.net.security.negotiateauthentication.wrap?view=net-8. |
| | | 102 | | // System.Net.Security.NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan<byte> input, System.Buffers.IBuff |
| | | 103 | | // |
| | | 104 | | // SECURITY: requestEncryption MUST be true. Encrypt() backs ISspiNegotiation.Encrypt which |
| | | 105 | | // SspiNegotiationTokenAuthenticator.IssueServiceToken uses to wrap the SecurityContextToken |
| | | 106 | | // proof key into the RequestedProofToken returned in the WS-Trust RSTR. Wrapping with |
| | | 107 | | // requestEncryption=false produces an integrity-only (MIC) token under platform GSS, which |
| | | 108 | | // would expose the symmetric proof key in cleartext to any passive network observer when |
| | | 109 | | // the binding is not protected by TLS. After the call we also assert isEncrypted is true |
| | | 110 | | // so that we fail closed if the negotiated package cannot provide confidentiality (rather |
| | | 111 | | // than silently leaking the key). This matches the behaviour of the legacy .NET Framework |
| | | 112 | | // WindowsSspiNegotiation.Encrypt path which called SSPI EncryptMessage with sealing. |
| | | 113 | | // |
| | | 114 | | // Create the memory stream with an initial capacity twice the size of the input, as encryption may increase |
| | | 115 | | // This is a heuristic and will be more than enough for typical cases, but it allows us to avoid resizing th |
| | | 116 | | // If we're wrong (e.g. the encryption overhead is larger than the input size), MemoryStream will grow if ne |
| | 0 | 117 | | var memoryStream = new MemoryStream(input.Length * 2); |
| | 0 | 118 | | PipeWriter pipeWriter = PipeWriter.Create(memoryStream); |
| | 0 | 119 | | var statusCode = (int)(((dynamic)_negotiateAuthentication).Wrap(input, (IBufferWriter<byte>)pipeWriter, true |
| | | 120 | | // Safe to call GetAwaiter().GetResult() as PipeWriter is on top of a MemoryStream which does all writes syn |
| | 0 | 121 | | pipeWriter.FlushAsync().GetAwaiter().GetResult(); |
| | | 122 | | |
| | 0 | 123 | | var errorCode = ToErrorCode(statusCode); |
| | 0 | 124 | | if (errorCode != NegotiateInternalSecurityStatusErrorCode.OK) |
| | | 125 | | { |
| | 0 | 126 | | throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( |
| | 0 | 127 | | new SecurityNegotiationException(SR.Format(SR.SspiWrapFailed, errorCode))); |
| | | 128 | | } |
| | | 129 | | |
| | 0 | 130 | | if (!isEncrypted) |
| | | 131 | | { |
| | 0 | 132 | | throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( |
| | 0 | 133 | | new SecurityNegotiationException(SR.SspiWrapDidNotEncrypt)); |
| | | 134 | | } |
| | | 135 | | |
| | 0 | 136 | | var output = memoryStream.ToArray(); |
| | 0 | 137 | | return output; |
| | | 138 | | } |
| | | 139 | | |
| | | 140 | | public IIdentity GetIdentity() |
| | | 141 | | { |
| | | 142 | | // https://learn.microsoft.com/en-us/dotnet/api/system.net.security.negotiateauthentication.remoteidentity?v |
| | 0 | 143 | | return ((dynamic)NegotiateAuthentication).RemoteIdentity; |
| | | 144 | | } |
| | | 145 | | |
| | | 146 | | public byte[] GetOutgoingBlob(byte[] incomingBlob, out NegotiateInternalSecurityStatusPal status) |
| | | 147 | | { |
| | | 148 | | // https://learn.microsoft.com/en-us/dotnet/api/system.net.security.negotiateauthentication.getoutgoingblob? |
| | | 149 | | // byte[]? GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out System.Net.Security.NegotiateAuthenticationS |
| | 0 | 150 | | object statusCode = Activator.CreateInstance(s_negotiateAuthenticationStatusCodeType); |
| | | 151 | | |
| | 0 | 152 | | object[] parameters = new object[] { NegotiateAuthentication, incomingBlob, statusCode }; |
| | 0 | 153 | | var result = (byte[]) s_getOutgoingBlobInvoker.DynamicInvoke(parameters); |
| | 0 | 154 | | statusCode = parameters[2]; |
| | | 155 | | |
| | 0 | 156 | | var internalStatusCode = ToErrorCode((int)statusCode); |
| | | 157 | | |
| | 0 | 158 | | IsValidContext = internalStatusCode is NegotiateInternalSecurityStatusErrorCode.OK |
| | 0 | 159 | | or NegotiateInternalSecurityStatusErrorCode.ContinueNeeded |
| | 0 | 160 | | or NegotiateInternalSecurityStatusErrorCode.CompleteNeeded; |
| | | 161 | | |
| | 0 | 162 | | Exception error = null; |
| | | 163 | | |
| | 0 | 164 | | if (!IsValidContext) |
| | | 165 | | { |
| | 0 | 166 | | error = new Win32Exception(NTE_FAIL, statusCode.ToString()); |
| | | 167 | | } |
| | | 168 | | |
| | 0 | 169 | | status = new NegotiateInternalSecurityStatusPal(internalStatusCode, error); |
| | | 170 | | |
| | 0 | 171 | | return result; |
| | | 172 | | } |
| | | 173 | | |
| | | 174 | | public void Dispose() |
| | | 175 | | { |
| | 0 | 176 | | _negotiateAuthentication?.Dispose(); |
| | 0 | 177 | | } |
| | | 178 | | |
| | | 179 | | /// <summary> |
| | | 180 | | /// Convert the NegotiateAuthenticationStatusCode int value into the (likely corresponding) |
| | | 181 | | /// NegotiateInternalSecurityStatusErrorCode value |
| | | 182 | | /// </summary> |
| | | 183 | | private static NegotiateInternalSecurityStatusErrorCode ToErrorCode(int inputValue) |
| | | 184 | | { |
| | | 185 | | // https://learn.microsoft.com/en-us/dotnet/api/system.net.security.negotiateauthenticationstatuscode?view=n |
| | 0 | 186 | | return inputValue switch |
| | 0 | 187 | | { |
| | 0 | 188 | | // Completed - Operation completed successfully |
| | 0 | 189 | | 0 => NegotiateInternalSecurityStatusErrorCode.OK, |
| | 0 | 190 | | // ContinueNeeded |
| | 0 | 191 | | 1 => NegotiateInternalSecurityStatusErrorCode.ContinueNeeded, |
| | 0 | 192 | | // GenericFailure |
| | 0 | 193 | | 2 => NegotiateInternalSecurityStatusErrorCode.InternalError, |
| | 0 | 194 | | // BadBinding |
| | 0 | 195 | | 3 => NegotiateInternalSecurityStatusErrorCode.BadBinding, |
| | 0 | 196 | | // Unsupported |
| | 0 | 197 | | 4 => NegotiateInternalSecurityStatusErrorCode.Unsupported, |
| | 0 | 198 | | // MessageAltered |
| | 0 | 199 | | 5 => NegotiateInternalSecurityStatusErrorCode.MessageAltered, |
| | 0 | 200 | | // ContextExpired |
| | 0 | 201 | | 6 => NegotiateInternalSecurityStatusErrorCode.ContextExpired, |
| | 0 | 202 | | // CredentialsExpired (Closest match) |
| | 0 | 203 | | 7 => NegotiateInternalSecurityStatusErrorCode.UnknownCredentials, |
| | 0 | 204 | | // InvalidCredentials (Closest match) |
| | 0 | 205 | | 8 => NegotiateInternalSecurityStatusErrorCode.UnknownCredentials, |
| | 0 | 206 | | // InvalidToken |
| | 0 | 207 | | 9 => NegotiateInternalSecurityStatusErrorCode.InvalidToken, |
| | 0 | 208 | | // UnknownCredentials |
| | 0 | 209 | | 10 => NegotiateInternalSecurityStatusErrorCode.UnknownCredentials, |
| | 0 | 210 | | // QopNotSupported |
| | 0 | 211 | | 11 => NegotiateInternalSecurityStatusErrorCode.QopNotSupported, |
| | 0 | 212 | | // OutOfSequence |
| | 0 | 213 | | 12 => NegotiateInternalSecurityStatusErrorCode.OutOfSequence, |
| | 0 | 214 | | // SecurityQosFailed |
| | 0 | 215 | | 13 => NegotiateInternalSecurityStatusErrorCode.SecurityQosFailed, |
| | 0 | 216 | | // TargetUnknown |
| | 0 | 217 | | 14 => NegotiateInternalSecurityStatusErrorCode.TargetUnknown, |
| | 0 | 218 | | // ImpersonationValidationFailed (Closest match) |
| | 0 | 219 | | 15 => NegotiateInternalSecurityStatusErrorCode.NoImpersonation, |
| | 0 | 220 | | _ => NegotiateInternalSecurityStatusErrorCode.NotSet, |
| | 0 | 221 | | }; |
| | | 222 | | } |
| | | 223 | | } |
| | | 224 | | } |