Thanks for this question. It pointed me into the right direction, that there is a bug in .Net, which I do not very often think about.
I bumped into this problem during writing of my implementation of FTPS server and Filezilla (or GnuTLS probably) client was complaining "GnuTLS error -110 in gnutls_record_recv: The TLS connection was non-properly terminated". I think it is a quite significant drawback in SslStream implementation.
So I ended up with writing a wrapper which sends this alert before closing the stream:
public class FixedSslStream : SslStream {
public FixedSslStream(Stream innerStream)
: base(innerStream) {
}
public FixedSslStream(Stream innerStream, bool leaveInnerStreamOpen)
: base(innerStream, leaveInnerStreamOpen) {
}
public FixedSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback)
: base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback) {
}
public FixedSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback, LocalCertificateSelectionCallback userCertificateSelectionCallback)
: base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback, userCertificateSelectionCallback) {
}
public FixedSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback, LocalCertificateSelectionCallback userCertificateSelectionCallback, EncryptionPolicy encryptionPolicy)
: base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback, userCertificateSelectionCallback, encryptionPolicy) {
}
public override void Close() {
try {
SslDirectCall.CloseNotify(this);
} finally {
base.Close();
}
}
}
And the following code is to make it working (this code requires assembly to be 'unsafe'):
public unsafe static class SslDirectCall {
public static void CloseNotify(SslStream sslStream) {
if (sslStream.IsAuthenticated) {
bool isServer = sslStream.IsServer;
byte[] result;
int resultSz;
var asmbSystem = typeof(System.Net.Authorization).Assembly;
int SCHANNEL_SHUTDOWN = 1;
var workArray = BitConverter.GetBytes(SCHANNEL_SHUTDOWN);
var sslstate = ReflectUtil.GetField(sslStream, "_SslState");
var context = ReflectUtil.GetProperty(sslstate, "Context");
var securityContext = ReflectUtil.GetField(context, "m_SecurityContext");
var securityContextHandleOriginal = ReflectUtil.GetField(securityContext, "_handle");
NativeApi.SSPIHandle securityContextHandle = default(NativeApi.SSPIHandle);
securityContextHandle.HandleHi = (IntPtr)ReflectUtil.GetField(securityContextHandleOriginal, "HandleHi");
securityContextHandle.HandleLo = (IntPtr)ReflectUtil.GetField(securityContextHandleOriginal, "HandleLo");
var credentialsHandle = ReflectUtil.GetField(context, "m_CredentialsHandle");
var credentialsHandleHandleOriginal = ReflectUtil.GetField(credentialsHandle, "_handle");
NativeApi.SSPIHandle credentialsHandleHandle = default(NativeApi.SSPIHandle);
credentialsHandleHandle.HandleHi = (IntPtr)ReflectUtil.GetField(credentialsHandleHandleOriginal, "HandleHi");
credentialsHandleHandle.HandleLo = (IntPtr)ReflectUtil.GetField(credentialsHandleHandleOriginal, "HandleLo");
int bufferSize = 1;
NativeApi.SecurityBufferDescriptor securityBufferDescriptor = new NativeApi.SecurityBufferDescriptor(bufferSize);
NativeApi.SecurityBufferStruct[] unmanagedBuffer = new NativeApi.SecurityBufferStruct[bufferSize];
fixed (NativeApi.SecurityBufferStruct* ptr = unmanagedBuffer)
fixed (void* workArrayPtr = workArray) {
securityBufferDescriptor.UnmanagedPointer = (void*)ptr;
unmanagedBuffer[0].token = (IntPtr)workArrayPtr;
unmanagedBuffer[0].count = workArray.Length;
unmanagedBuffer[0].type = NativeApi.BufferType.Token;
NativeApi.SecurityStatus status;
status = (NativeApi.SecurityStatus)NativeApi.ApplyControlToken(ref securityContextHandle, securityBufferDescriptor);
if (status == NativeApi.SecurityStatus.OK) {
unmanagedBuffer[0].token = IntPtr.Zero;
unmanagedBuffer[0].count = 0;
unmanagedBuffer[0].type = NativeApi.BufferType.Token;
NativeApi.SSPIHandle contextHandleOut = default(NativeApi.SSPIHandle);
NativeApi.ContextFlags outflags = NativeApi.ContextFlags.Zero;
long ts = 0;
var inflags = NativeApi.ContextFlags.SequenceDetect |
NativeApi.ContextFlags.ReplayDetect |
NativeApi.ContextFlags.Confidentiality |
NativeApi.ContextFlags.AcceptExtendedError |
NativeApi.ContextFlags.AllocateMemory |
NativeApi.ContextFlags.InitStream;
if (isServer) {
status = (NativeApi.SecurityStatus)NativeApi.AcceptSecurityContext(ref credentialsHandleHandle, ref securityContextHandle, null,
inflags, NativeApi.Endianness.Native, ref contextHandleOut, securityBufferDescriptor, ref outflags, out ts);
} else {
status = (NativeApi.SecurityStatus)NativeApi.InitializeSecurityContextW(ref credentialsHandleHandle, ref securityContextHandle, null,
inflags, 0, NativeApi.Endianness.Native, null, 0, ref contextHandleOut, securityBufferDescriptor, ref outflags, out ts);
}
if (status == NativeApi.SecurityStatus.OK) {
byte[] resultArr = new byte[unmanagedBuffer[0].count];
Marshal.Copy(unmanagedBuffer[0].token, resultArr, 0, resultArr.Length);
Marshal.FreeCoTaskMem(unmanagedBuffer[0].token);
result = resultArr;
resultSz = resultArr.Length;
} else {
throw new InvalidOperationException(string.Format("AcceptSecurityContext/InitializeSecurityContextW returned [{0}] during CloseNotify.", status));
}
} else {
throw new InvalidOperationException(string.Format("ApplyControlToken returned [{0}] during CloseNotify.", status));
}
}
var innerStream = (Stream)ReflectUtil.GetProperty(sslstate, "InnerStream");
innerStream.Write(result, 0, resultSz);
}
}
}
Windows API used:
public unsafe static class NativeApi {
internal enum BufferType {
Empty,
Data,
Token,
Parameters,
Missing,
Extra,
Trailer,
Header,
Padding = 9,
Stream,
ChannelBindings = 14,
TargetHost = 16,
ReadOnlyFlag = -2147483648,
ReadOnlyWithChecksum = 268435456
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct SSPIHandle {
public IntPtr HandleHi;
public IntPtr HandleLo;
public bool IsZero {
get {
return this.HandleHi == IntPtr.Zero && this.HandleLo == IntPtr.Zero;
}
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
internal void SetToInvalid() {
this.HandleHi = IntPtr.Zero;
this.HandleLo = IntPtr.Zero;
}
public override string ToString() {
return this.HandleHi.ToString("x") + ":" + this.HandleLo.ToString("x");
}
}
[StructLayout(LayoutKind.Sequential)]
internal class SecurityBufferDescriptor {
public readonly int Version;
public readonly int Count;
public unsafe void* UnmanagedPointer;
public SecurityBufferDescriptor(int count) {
this.Version = 0;
this.Count = count;
this.UnmanagedPointer = null;
}
}
[StructLayout(LayoutKind.Sequential)]
internal struct SecurityBufferStruct {
public int count;
public BufferType type;
public IntPtr token;
public static readonly int Size = sizeof(SecurityBufferStruct);
}
internal enum SecurityStatus {
OK,
ContinueNeeded = 590610,
CompleteNeeded,
CompAndContinue,
ContextExpired = 590615,
CredentialsNeeded = 590624,
Renegotiate,
OutOfMemory = -2146893056,
InvalidHandle,
Unsupported,
TargetUnknown,
InternalError,
PackageNotFound,
NotOwner,
CannotInstall,
InvalidToken,
CannotPack,
QopNotSupported,
NoImpersonation,
LogonDenied,
UnknownCredentials,
NoCredentials,
MessageAltered,
OutOfSequence,
NoAuthenticatingAuthority,
IncompleteMessage = -2146893032,
IncompleteCredentials = -2146893024,
BufferNotEnough,
WrongPrincipal,
TimeSkew = -2146893020,
UntrustedRoot,
IllegalMessage,
CertUnknown,
CertExpired,
AlgorithmMismatch = -2146893007,
SecurityQosFailed,
SmartcardLogonRequired = -2146892994,
UnsupportedPreauth = -2146892989,
BadBinding = -2146892986
}
[Flags]
internal enum ContextFlags {
Zero = 0,
Delegate = 1,
MutualAuth = 2,
ReplayDetect = 4,
SequenceDetect = 8,
Confidentiality = 16,
UseSessionKey = 32,
AllocateMemory = 256,
Connection = 2048,
InitExtendedError = 16384,
AcceptExtendedError = 32768,
InitStream = 32768,
AcceptStream = 65536,
InitIntegrity = 65536,
AcceptIntegrity = 131072,
InitManualCredValidation = 524288,
InitUseSuppliedCreds = 128,
InitIdentify = 131072,
AcceptIdentify = 524288,
ProxyBindings = 67108864,
AllowMissingBindings = 268435456,
UnverifiedTargetName = 536870912
}
internal enum Endianness {
Network,
Native = 16
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[DllImport("secur32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern int ApplyControlToken(ref SSPIHandle contextHandle, [In] [Out] SecurityBufferDescriptor outputBuffer);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[DllImport("secur32.dll", ExactSpelling = true, SetLastError = true)]
internal unsafe static extern int AcceptSecurityContext(ref SSPIHandle credentialHandle, ref SSPIHandle contextHandle, [In] SecurityBufferDescriptor inputBuffer, [In] ContextFlags inFlags, [In] Endianness endianness, ref SSPIHandle outContextPtr, [In] [Out] SecurityBufferDescriptor outputBuffer, [In] [Out] ref ContextFlags attributes, out long timeStamp);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[DllImport("secur32.dll", ExactSpelling = true, SetLastError = true)]
internal unsafe static extern int InitializeSecurityContextW(ref SSPIHandle credentialHandle, ref SSPIHandle contextHandle, [In] byte* targetName, [In] ContextFlags inFlags, [In] int reservedI, [In] Endianness endianness, [In] SecurityBufferDescriptor inputBuffer, [In] int reservedII, ref SSPIHandle outContextPtr, [In] [Out] SecurityBufferDescriptor outputBuffer, [In] [Out] ref ContextFlags attributes, out long timeStamp);
}
Reflection utilities:
public static class ReflectUtil {
public static object GetField(object obj, string fieldName) {
var tp = obj.GetType();
var info = GetAllFields(tp)
.Where(f => f.Name == fieldName).Single();
return info.GetValue(obj);
}
public static void SetField(object obj, string fieldName, object value) {
var tp = obj.GetType();
var info = GetAllFields(tp)
.Where(f => f.Name == fieldName).Single();
info.SetValue(obj, value);
}
public static object GetStaticField(Assembly assembly, string typeName, string fieldName) {
var tp = assembly.GetType(typeName);
var info = GetAllFields(tp)
.Where(f => f.IsStatic)
.Where(f => f.Name == fieldName).Single();
return info.GetValue(null);
}
public static object GetProperty(object obj, string propertyName) {
var tp = obj.GetType();
var info = GetAllProperties(tp)
.Where(f => f.Name == propertyName).Single();
return info.GetValue(obj, null);
}
public static object CallMethod(object obj, string methodName, params object[] prm) {
var tp = obj.GetType();
var info = GetAllMethods(tp)
.Where(f => f.Name == methodName && f.GetParameters().Length == prm.Length).Single();
object rez = info.Invoke(obj, prm);
return rez;
}
public static object NewInstance(Assembly assembly, string typeName, params object[] prm) {
var tp = assembly.GetType(typeName);
var info = tp.GetConstructors()
.Where(f => f.GetParameters().Length == prm.Length).Single();
object rez = info.Invoke(prm);
return rez;
}
public static object InvokeStaticMethod(Assembly assembly, string typeName, string methodName, params object[] prm) {
var tp = assembly.GetType(typeName);
var info = GetAllMethods(tp)
.Where(f => f.IsStatic)
.Where(f => f.Name == methodName && f.GetParameters().Length == prm.Length).Single();
object rez = info.Invoke(null, prm);
return rez;
}
public static object GetEnumValue(Assembly assembly, string typeName, int value) {
var tp = assembly.GetType(typeName);
object rez = Enum.ToObject(tp, value);
return rez;
}
private static IEnumerable<FieldInfo> GetAllFields(Type t) {
if (t == null)
return Enumerable.Empty<FieldInfo>();
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance |
BindingFlags.DeclaredOnly;
return t.GetFields(flags).Concat(GetAllFields(t.BaseType));
}
private static IEnumerable<PropertyInfo> GetAllProperties(Type t) {
if (t == null)
return Enumerable.Empty<PropertyInfo>();
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance |
BindingFlags.DeclaredOnly;
return t.GetProperties(flags).Concat(GetAllProperties(t.BaseType));
}
private static IEnumerable<MethodInfo> GetAllMethods(Type t) {
if (t == null)
return Enumerable.Empty<MethodInfo>();
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance |
BindingFlags.DeclaredOnly;
return t.GetMethods(flags).Concat(GetAllMethods(t.BaseType));
}
}
I am not experienced in writing reliable interaction with unmanaged environment, so I hope somebody can have a look and fix issues (and maybe make it 'safe').
It's a bug in .NET's usage of the underlying security API. Note another question by me about being unable to select a specific cypher suite - they really botched up this API wrapper...
For the record, SslStream at least in .NET 2.0 also doesn't appear to response to a close_notify from the other side. This means that calling OpenSSL's SSL_Shutdown() properly, i.e. twice - once to initiate the shutdown and again to wait for the response - will hang on the second call.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With