/// <summary> /// Queries the security package's expections regarding message/token/signature/padding buffer sizes. /// </summary> /// <returns></returns> private SecPkgContext_Sizes QueryBufferSizes() { SecPkgContext_Sizes sizes = new SecPkgContext_Sizes(); SecurityStatus status = SecurityStatus.InternalError; bool gotRef = false; RuntimeHelpers.PrepareConstrainedRegions(); try { this.ContextHandle.DangerousAddRef(ref gotRef); } catch (Exception) { if (gotRef) { this.ContextHandle.DangerousRelease(); gotRef = false; } throw; } finally { if (gotRef) { status = ContextNativeMethods.QueryContextAttributes_Sizes( ref this.ContextHandle.rawHandle, ContextQueryAttrib.Sizes, ref sizes ); this.ContextHandle.DangerousRelease(); } } if (status != SecurityStatus.OK) { throw new SSPIException("Failed to query context buffer size attributes", status); } return(sizes); }
/// <summary> /// Safely invokes the native DecryptMessage function, making sure that handle ref counting is /// performed in a proper CER. /// </summary> /// <param name="handle"></param> /// <param name="qualityOfProtection"></param> /// <param name="bufferAdapter"></param> /// <param name="sequenceNumber"></param> /// <returns></returns> internal static SecurityStatus SafeDecryptMessage( SafeContextHandle handle, int qualityOfProtection, SecureBufferAdapter bufferAdapter, int sequenceNumber) { SecurityStatus status = SecurityStatus.InvalidHandle; bool gotRef = false; RuntimeHelpers.PrepareConstrainedRegions(); try { handle.DangerousAddRef(ref gotRef); } catch (Exception) { if (gotRef) { handle.DangerousRelease(); gotRef = false; } throw; } finally { if (gotRef) { status = ContextNativeMethods.DecryptMessage( ref handle.rawHandle, bufferAdapter.Handle, sequenceNumber, qualityOfProtection ); handle.DangerousRelease(); } } return(status); }
/// <summary> /// Called by the ImpersonationHandle when it is released, either by disposale or finalization. /// </summary> internal void RevertImpersonate() { bool gotRef = false; if (impersonating == false || this.Disposed) { return; } RuntimeHelpers.PrepareConstrainedRegions(); try { this.ContextHandle.DangerousAddRef(ref gotRef); } catch (Exception) { if (gotRef) { this.ContextHandle.DangerousRelease(); gotRef = false; } throw; } finally { if (gotRef) { ContextNativeMethods.RevertSecurityContext( ref this.ContextHandle.rawHandle ); this.ContextHandle.DangerousRelease(); this.impersonating = false; } } }
internal static SecurityStatus SafeQueryContextAttribute( SafeContextHandle handle, ContextQueryAttrib attribute, ref byte[] buffer ) { bool gotRef = false; SecurityStatus status = SecurityStatus.InternalError; RuntimeHelpers.PrepareConstrainedRegions(); int pointerSize = System.Environment.Is64BitOperatingSystem ? 8 : 4; //NOTE: update this when 128 bit processors exist IntPtr alloc_buffer = Marshal.AllocHGlobal(sizeof(uint) + pointerSize); //NOTE: this is at most 4 + sizeof(void*) bytes //see struct SecPkgContext_SessionKey // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380096(v=vs.85).aspx try { handle.DangerousAddRef(ref gotRef); } catch (Exception) { if (gotRef) { handle.DangerousRelease(); gotRef = false; buffer = null; } throw; } finally { if (gotRef) { status = ContextNativeMethods.QueryContextAttributes( ref handle.rawHandle, attribute, alloc_buffer ); if (status == SecurityStatus.OK) { KeyStruct key = new KeyStruct(); Marshal.PtrToStructure(alloc_buffer, key); // fit to the proper size, read a byte[] byte[] sizedBuffer = new byte[key.size]; for (int i = 0; i < key.size; i++) { sizedBuffer[i] = Marshal.ReadByte(key.data, i); } buffer = sizedBuffer; } handle.DangerousRelease(); } } Marshal.FreeHGlobal(alloc_buffer); return(status); }
/// <summary> /// Performs and continues the authentication cycle. /// </summary> /// <remarks> /// This method is performed iteratively to continue and end the authentication cycle with the /// client. Each stage works by acquiring a token from one side, presenting it to the other side /// which in turn may generate a new token. /// /// The cycle typically starts and ends with the client. On the first invocation on the client, /// no server token exists, and null is provided in its place. The client returns its status, providing /// its output token for the server. The server accepts the clients token as input and provides a /// token as output to send back to the client. This cycle continues until the server and client /// both indicate, typically, a SecurityStatus of 'OK'. /// </remarks> /// <param name="clientToken">The most recently received token from the client.</param> /// <param name="nextToken">The servers next authentication token in the cycle, that must /// be sent to the client.</param> /// <returns>A status message indicating the progression of the authentication cycle. /// A status of 'OK' indicates that the cycle is complete, from the servers's perspective. If the nextToken /// is not null, it must be sent to the client. /// A status of 'Continue' indicates that the output token should be sent to the client and /// a response should be anticipated.</returns> public SecurityStatus AcceptToken(byte[] clientToken, out byte[] nextToken) { SecureBuffer clientBuffer; SecureBuffer outBuffer; SecurityStatus status; TimeStamp rawExpiry = new TimeStamp(); SecureBufferAdapter clientAdapter; SecureBufferAdapter outAdapter; if (this.Disposed) { throw new ObjectDisposedException("ServerContext"); } else if (this.Initialized) { throw new InvalidOperationException( "Attempted to continue initialization of a ServerContext after initialization had completed." ); } clientBuffer = new SecureBuffer(clientToken, BufferType.Token); outBuffer = new SecureBuffer( new byte[this.Credential.PackageInfo.MaxTokenLength], BufferType.Token ); using (clientAdapter = new SecureBufferAdapter(clientBuffer)) { using (outAdapter = new SecureBufferAdapter(outBuffer)) { if (this.ContextHandle.IsInvalid) { status = ContextNativeMethods.AcceptSecurityContext_1( ref this.Credential.Handle.rawHandle, IntPtr.Zero, clientAdapter.Handle, requestedAttribs, SecureBufferDataRep.Network, ref this.ContextHandle.rawHandle, outAdapter.Handle, ref this.finalAttribs, ref rawExpiry ); } else { status = ContextNativeMethods.AcceptSecurityContext_2( ref this.Credential.Handle.rawHandle, ref this.ContextHandle.rawHandle, clientAdapter.Handle, requestedAttribs, SecureBufferDataRep.Network, ref this.ContextHandle.rawHandle, outAdapter.Handle, ref this.finalAttribs, ref rawExpiry ); } } } if (status == SecurityStatus.OK) { nextToken = null; base.Initialize(rawExpiry.ToDateTime()); if (outBuffer.Length != 0) { nextToken = new byte[outBuffer.Length]; Array.Copy(outBuffer.Buffer, nextToken, nextToken.Length); } else { nextToken = null; } } else if (status == SecurityStatus.ContinueNeeded) { nextToken = new byte[outBuffer.Length]; Array.Copy(outBuffer.Buffer, nextToken, nextToken.Length); } else { throw new SSPIException("Failed to call AcceptSecurityContext", status); } return(status); }
/// <summary> /// Changes the current thread's security context to impersonate the user of the client. /// </summary> /// <remarks> /// Requires that the security package provided with the server's credentials, as well as the /// client's credentials, support impersonation. /// /// Currently, only one thread may initiate impersonation per security context. Impersonation may /// follow threads created by the initial impersonation thread, however. /// </remarks> /// <returns>A handle to capture the lifetime of the impersonation. Dispose the handle to revert /// impersonation. If the handle is leaked, the impersonation will automatically revert at a /// non-deterministic time when the handle is finalized by the Garbage Collector.</returns> public ImpersonationHandle ImpersonateClient() { ImpersonationHandle handle; SecurityStatus status = SecurityStatus.InternalError; bool gotRef = false; if (this.Disposed) { throw new ObjectDisposedException("ServerContext"); } else if (this.Initialized == false) { throw new InvalidOperationException( "The server context has not been completely initialized." ); } else if (impersonating) { throw new InvalidOperationException("Cannot impersonate again while already impersonating."); } else if (this.SupportsImpersonate == false) { throw new InvalidOperationException( "The ServerContext is using a security package that does not support impersonation." ); } handle = new ImpersonationHandle(this); RuntimeHelpers.PrepareConstrainedRegions(); try { this.ContextHandle.DangerousAddRef(ref gotRef); } catch (Exception) { if (gotRef) { this.ContextHandle.DangerousRelease(); gotRef = false; } throw; } finally { if (gotRef) { status = ContextNativeMethods.ImpersonateSecurityContext( ref this.ContextHandle.rawHandle ); this.ContextHandle.DangerousRelease(); this.impersonating = status == SecurityStatus.OK; } } if (status == SecurityStatus.NoImpersonation) { throw new SSPIException("Impersonation could not be performed.", status); } else if (status == SecurityStatus.Unsupported) { throw new SSPIException("Impersonation is not supported by the security context's Security Support Provider.", status); } else if (status != SecurityStatus.OK) { throw new SSPIException("Failed to impersonate the client", status); } if (this.impersonating && this.impersonationSetsThreadPrinciple) { SetThreadPrinciple(); } return(handle); }
/// <summary> /// Performs and continues the authentication cycle. /// </summary> /// <remarks> /// This method is performed iteratively to start, continue, and end the authentication cycle with the /// server. Each stage works by acquiring a token from one side, presenting it to the other side /// which in turn may generate a new token. /// /// The cycle typically starts and ends with the client. On the first invocation on the client, /// no server token exists, and null is provided in its place. The client returns its status, providing /// its output token for the server. The server accepts the clients token as input and provides a /// token as output to send back to the client. This cycle continues until the server and client /// both indicate, typically, a SecurityStatus of 'OK'. /// </remarks> /// <param name="serverToken">The most recently received token from the server, or null if beginning /// the authentication cycle.</param> /// <param name="outToken">The clients next authentication token in the authentication cycle.</param> /// <returns>A status message indicating the progression of the authentication cycle. /// A status of 'OK' indicates that the cycle is complete, from the client's perspective. If the outToken /// is not null, it must be sent to the server. /// A status of 'Continue' indicates that the output token should be sent to the server and /// a response should be anticipated.</returns> public SecurityStatus Init(byte[] serverToken, out byte[] outToken) { TimeStamp rawExpiry = new TimeStamp(); SecurityStatus status; SecureBuffer outTokenBuffer; SecureBufferAdapter outAdapter; SecureBuffer serverBuffer; SecureBufferAdapter serverAdapter; if (this.Disposed) { throw new ObjectDisposedException("ClientContext"); } else if ((serverToken != null) && (this.ContextHandle.IsInvalid)) { throw new InvalidOperationException("Out-of-order usage detected - have a server token, but no previous client token had been created."); } else if ((serverToken == null) && (this.ContextHandle.IsInvalid == false)) { throw new InvalidOperationException("Must provide the server's response when continuing the init process."); } // The security package tells us how big its biggest token will be. We'll allocate a buffer // that size, and it'll tell us how much it used. outTokenBuffer = new SecureBuffer( new byte[this.Credential.PackageInfo.MaxTokenLength], BufferType.Token ); serverBuffer = null; if (serverToken != null) { serverBuffer = new SecureBuffer(serverToken, BufferType.Token); } // Some notes on handles and invoking InitializeSecurityContext // - The first time around, the phContext parameter (the 'old' handle) is a null pointer to what // would be an RawSspiHandle, to indicate this is the first time it's being called. // The phNewContext is a pointer (reference) to an RawSspiHandle struct of where to write the // new handle's values. // - The next time you invoke ISC, it takes a pointer to the handle it gave you last time in phContext, // and takes a pointer to where it should write the new handle's values in phNewContext. // - After the first time, you can provide the same handle to both parameters. From MSDN: // "On the second call, phNewContext can be the same as the handle specified in the phContext // parameter." // It will overwrite the handle you gave it with the new handle value. // - All handle structures themselves are actually *two* pointer variables, eg, 64 bits on 32-bit // Windows, 128 bits on 64-bit Windows. // - So in the end, on a 64-bit machine, we're passing a 64-bit value (the pointer to the struct) that // points to 128 bits of memory (the struct itself) for where to write the handle numbers. using (outAdapter = new SecureBufferAdapter(outTokenBuffer)) { if (this.ContextHandle.IsInvalid) { status = ContextNativeMethods.InitializeSecurityContext_1( ref this.Credential.Handle.rawHandle, IntPtr.Zero, this.serverPrinc, this.requestedAttribs, 0, SecureBufferDataRep.Network, IntPtr.Zero, 0, ref this.ContextHandle.rawHandle, outAdapter.Handle, ref this.finalAttribs, ref rawExpiry ); } else { using (serverAdapter = new SecureBufferAdapter(serverBuffer)) { status = ContextNativeMethods.InitializeSecurityContext_2( ref this.Credential.Handle.rawHandle, ref this.ContextHandle.rawHandle, this.serverPrinc, this.requestedAttribs, 0, SecureBufferDataRep.Network, serverAdapter.Handle, 0, ref this.ContextHandle.rawHandle, outAdapter.Handle, ref this.finalAttribs, ref rawExpiry ); } } } if (status.IsError() == false) { if (status == SecurityStatus.OK) { base.Initialize(rawExpiry.ToDateTime()); } outToken = null; if (outTokenBuffer.Length != 0) { outToken = new byte[outTokenBuffer.Length]; Array.Copy(outTokenBuffer.Buffer, outToken, outToken.Length); } } else { throw new SSPIException("Failed to invoke InitializeSecurityContext for a client", status); } return(status); }
/// <summary> /// Verifies the signature of a signed message /// </summary> /// <remarks> /// The expected structure of the signed message buffer is as follows: /// - 4 bytes, unsigned integer in big endian format indicating the length of the plaintext message /// - 2 bytes, unsigned integer in big endian format indicating the length of the signture /// - The plaintext message /// - The message's signature. /// </remarks> /// <param name="signedMessage">The packed signed message.</param> /// <param name="origMessage">The extracted original message.</param> /// <returns>True if the message has a valid signature, false otherwise.</returns> public bool VerifySignature(byte[] signedMessage, out byte[] origMessage) { SecurityStatus status = SecurityStatus.InternalError; SecPkgContext_Sizes sizes; SecureBuffer dataBuffer; SecureBuffer signatureBuffer; SecureBufferAdapter adapter; CheckLifecycle(); sizes = QueryBufferSizes(); if (signedMessage.Length < 2 + 4 + sizes.MaxSignature) { throw new ArgumentException("Input message is too small to possibly fit a valid message"); } int position = 0; int messageLen; int sigLen; messageLen = ByteWriter.ReadInt32_BE(signedMessage, 0); position += 4; sigLen = ByteWriter.ReadInt16_BE(signedMessage, position); position += 2; if (messageLen + sigLen + 2 + 4 > signedMessage.Length) { throw new ArgumentException("The buffer contains invalid data - the embedded length data does not add up."); } dataBuffer = new SecureBuffer(new byte[messageLen], BufferType.Data); Array.Copy(signedMessage, position, dataBuffer.Buffer, 0, messageLen); position += messageLen; signatureBuffer = new SecureBuffer(new byte[sigLen], BufferType.Token); Array.Copy(signedMessage, position, signatureBuffer.Buffer, 0, sigLen); position += sigLen; using (adapter = new SecureBufferAdapter(new[] { dataBuffer, signatureBuffer })) { status = ContextNativeMethods.SafeVerifySignature( this.ContextHandle, 0, adapter, 0 ); } if (status == SecurityStatus.OK) { origMessage = dataBuffer.Buffer; return(true); } else if (status == SecurityStatus.MessageAltered || status == SecurityStatus.OutOfSequence) { origMessage = null; return(false); } else { throw new SSPIException("Failed to determine the veracity of a signed message.", status); } }
/// <summary> /// Decrypts a previously encrypted message. /// </summary> /// <remarks> /// The expected format of the buffer is as follows: /// - 2 bytes, an unsigned big-endian integer indicating the length of the trailer buffer size /// - 4 bytes, an unsigned big-endian integer indicating the length of the message buffer size. /// - 2 bytes, an unsigned big-endian integer indicating the length of the encryption padding buffer size. /// - The trailer buffer /// - The message buffer /// - The padding buffer. /// </remarks> /// <param name="input">The packed and encrypted data.</param> /// <returns>The original plaintext message.</returns> public byte[] Decrypt(byte[] input) { SecPkgContext_Sizes sizes; SecureBuffer trailerBuffer; SecureBuffer dataBuffer; SecureBuffer paddingBuffer; SecureBufferAdapter adapter; SecurityStatus status; byte[] result = null; int remaining; int position; int trailerLength; int dataLength; int paddingLength; CheckLifecycle(); sizes = QueryBufferSizes(); // This check is required, but not sufficient. We could be stricter. if (input.Length < 2 + 4 + 2 + sizes.SecurityTrailer) { throw new ArgumentException("Buffer is too small to possibly contain an encrypted message"); } position = 0; trailerLength = ByteWriter.ReadInt16_BE(input, position); position += 2; dataLength = ByteWriter.ReadInt32_BE(input, position); position += 4; paddingLength = ByteWriter.ReadInt16_BE(input, position); position += 2; if (trailerLength + dataLength + paddingLength + 2 + 4 + 2 > input.Length) { throw new ArgumentException("The buffer contains invalid data - the embedded length data does not add up."); } trailerBuffer = new SecureBuffer(new byte[trailerLength], BufferType.Token); dataBuffer = new SecureBuffer(new byte[dataLength], BufferType.Data); paddingBuffer = new SecureBuffer(new byte[paddingLength], BufferType.Padding); remaining = input.Length - position; if (trailerBuffer.Length <= remaining) { Array.Copy(input, position, trailerBuffer.Buffer, 0, trailerBuffer.Length); position += trailerBuffer.Length; remaining -= trailerBuffer.Length; } else { throw new ArgumentException("Input is missing data - it is not long enough to contain a fully encrypted message"); } if (dataBuffer.Length <= remaining) { Array.Copy(input, position, dataBuffer.Buffer, 0, dataBuffer.Length); position += dataBuffer.Length; remaining -= dataBuffer.Length; } else { throw new ArgumentException("Input is missing data - it is not long enough to contain a fully encrypted message"); } if (paddingBuffer.Length <= remaining) { Array.Copy(input, position, paddingBuffer.Buffer, 0, paddingBuffer.Length); } // else there was no padding. using (adapter = new SecureBufferAdapter(new[] { trailerBuffer, dataBuffer, paddingBuffer })) { status = ContextNativeMethods.SafeDecryptMessage( this.ContextHandle, 0, adapter, 0 ); } if (status != SecurityStatus.OK) { throw new SSPIException("Failed to encrypt message", status); } result = new byte[dataBuffer.Length]; Array.Copy(dataBuffer.Buffer, 0, result, 0, dataBuffer.Length); return(result); }
/// <summary> /// Encrypts the byte array using the context's session key. /// </summary> /// <remarks> /// The structure of the returned data is as follows: /// - 2 bytes, an unsigned big-endian integer indicating the length of the trailer buffer size /// - 4 bytes, an unsigned big-endian integer indicating the length of the message buffer size. /// - 2 bytes, an unsigned big-endian integer indicating the length of the encryption padding buffer size. /// - The trailer buffer /// - The message buffer /// - The padding buffer. /// </remarks> /// <param name="input">The raw message to encrypt.</param> /// <returns>The packed and encrypted message.</returns> public byte[] Encrypt(byte[] input) { // The message is encrypted in place in the buffer we provide to Win32 EncryptMessage SecPkgContext_Sizes sizes; SecureBuffer trailerBuffer; SecureBuffer dataBuffer; SecureBuffer paddingBuffer; SecureBufferAdapter adapter; SecurityStatus status = SecurityStatus.InvalidHandle; byte[] result; CheckLifecycle(); sizes = QueryBufferSizes(); trailerBuffer = new SecureBuffer(new byte[sizes.SecurityTrailer], BufferType.Token); dataBuffer = new SecureBuffer(new byte[input.Length], BufferType.Data); paddingBuffer = new SecureBuffer(new byte[sizes.BlockSize], BufferType.Padding); Array.Copy(input, dataBuffer.Buffer, input.Length); using (adapter = new SecureBufferAdapter(new[] { trailerBuffer, dataBuffer, paddingBuffer })) { status = ContextNativeMethods.SafeEncryptMessage( this.ContextHandle, 0, adapter, 0 ); } if (status != SecurityStatus.OK) { throw new SSPIException("Failed to encrypt message", status); } int position = 0; // Enough room to fit: // -- 2 bytes for the trailer buffer size // -- 4 bytes for the message size // -- 2 bytes for the padding size. // -- The encrypted message result = new byte[2 + 4 + 2 + trailerBuffer.Length + dataBuffer.Length + paddingBuffer.Length]; ByteWriter.WriteInt16_BE((short)trailerBuffer.Length, result, position); position += 2; ByteWriter.WriteInt32_BE(dataBuffer.Length, result, position); position += 4; ByteWriter.WriteInt16_BE((short)paddingBuffer.Length, result, position); position += 2; Array.Copy(trailerBuffer.Buffer, 0, result, position, trailerBuffer.Length); position += trailerBuffer.Length; Array.Copy(dataBuffer.Buffer, 0, result, position, dataBuffer.Length); position += dataBuffer.Length; Array.Copy(paddingBuffer.Buffer, 0, result, position, paddingBuffer.Length); position += paddingBuffer.Length; return(result); }