I find the .Net NegotiateStream class useful for internal services as it provides single sign on, authentication and basic encryption. However, as a python programmer, I couldn’t find a client library.
TL;DR
For the impatient the repo can be found on GitHub.
The Protocol
I won’t go into detail regarding the protocol, but it has two key features: a handshake (during which credentials are exchanged), and then the encryption/decryption of data sent/received.
The handshake stage is widely used by Windows intranet servers to avoid the need for entering credentials, and involves the generation and consumption of tokens which are base64 encoded and passed as HTTP headers. I’ve done this before using the pyspnego package for the SSPI authentication layer of a Python web server. The SPNEGO client generates a token that is passed to the server, which responds with a new token, and so on until authentication is complete.
The wire protocol uses the same tokens, but has different “headers”. This was revealed by inspecting the code from the net.tcp-proxy project. During the handshake the header takes the form:
The state can be one of “done” (0x14), “error” (0x15) and “in progress” (0x16). The major.minor version is 1.0, and the payload size is the length of the generated token. When the handshake is complete the header changes to simply the payload size, but as an unsigned int.
Let’s look at some code! First I needed to handle the handshake header record.
@classmethod defunpack(cls, buf: bytes) -> HandshakeRecord: (state, major, minor, payload_size) = struct.unpack(cls.FORMAT, buf) return HandshakeRecord(state, major, minor, payload_size) With that, and the help of pyspnego I can implement reading and writing.
Note the different headers. When the handshake is “in progress” the handshake style of header is used. After authentication the shorter form is used, and the client has to encrypt and decrypt the data.
using System; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Text;
namespaceNegotiateStreamServer { internalclassProgram { staticvoidMain(string[] args) { var listener = new TcpListener(IPAddress.Any, 8181); listener.Start();
while (true) { Console.WriteLine("Listening ..."); var client = listener.AcceptTcpClient();
try { Console.WriteLine("... Client connected.");
Console.WriteLine("Authenticating..."); var stream = new NegotiateStream(client.GetStream(), false); stream.AuthenticateAsServer();
Console.WriteLine( "... {0} authenticated using {1}", stream.RemoteIdentity.Name, stream.RemoteIdentity.AuthenticationType);
var buf = newbyte[4096]; for (var i = 0; i < 4; ++i) { var bytesRead = stream.Read(buf, 0, buf.Length); var message = Encoding.UTF8.GetString(buf, 0, bytesRead); Console.WriteLine(message); stream.Write(buf, 0, bytesRead); } stream.Close(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } } } }
What a lot of code the server is. If you run the server, then the client, you should see the handshake, and the messages passing to and fro, encrypted.
As most of my Python servers are async, I made a couple of async versions. One is a straight async version of the synchronous socket client. The second is more”asyncio” style. I won’t show the code here, but here is a demo program using it:
for data in (b'first line', b'second line', b'third line'): writer.write(data) await writer.drain() response = await reader.read() print("Received: ", response)
writer.close() await writer.wait_closed()
if __name__ == '__main__': asyncio.run(main())
By using the same pattern as asyncio.open_connection the negotiation gets tidied away inside open_negotiate_stream providing a clean consistent interface.
If you find any bugs, or wish to add features, please post issues and pull requests to the repo.