- Introduction
- TLS/SSL Protocol Overview
- TLS/SSL and the .NET Framework
- Using the SslStream class
- CipherSuites
INTRODUCTION
Since its debut in 1994, when Netscape Communication designed version 1.0 of the Secure Socket Layer specification, SSL began to emerge as the most popular protocol for the secure exchange of digital data between remote systems. Its success was so remarkable that the Internet Engineering Task Force (IETF) decided, in 1996, to adopt it as an Internet Standard under the name of Transport Layer Security (TLS).
Even though it was originally intended for protecting electronic e-commerce transactions, the growth over time of the world wide web both in terms of the number of people using it every day and the number of available services, forced the TLS/SSL protocol to be applied to very different scenarios from those envisaged at the outset. Think about technologies like FTPS, secure e-mail delivery using TLS channels, SSL VPN, and so on.
That’s not all. The growing complexity of “digital communication” doesn’t involve only the underlying technologies, but it has social impacts too. For example, Web 2.0 has enabled users to become involved in website content generation; they can upload documents and share photos or videos, they can blog or post comments on other people’s blogs, and so on. This “opening up to users” has dramatically increased information exchange, including personal and sensitive data, and it has revealed a “dark side”… sadly, more data around also means more risk that it may be used in a fraudulent manner. Protecting the data has therefore become an important requirement. At present, the HTTPS protocol, based on TLS/SSL protocol and digital certificates, is probably the most widely-adopted solution.
High level solutions for developing client/server communication systems that use the TSL/SSL protocol to protect data are available, Windows Communication Foundation (WCF) being probably the most important. However, analyzing at low level how TLS/SSL protocol works can be very useful to better understand what happens behind the scenes, whatever solution you are working on. In this article, I would like to investigate, in-depth, how the SslStream
class of the .NET Framework operates, and how to use it to develop a simple client/server communication system that uses the TLS/SSL protocol to protect data exchange.
TLS/SSL PROTOCOL OVERVIEW
Why has the TLS/SSL protocol been so widely adopted? Like me, many of you have probably studied the TCP/IP protocols family and learnt that, even if an official standard protocol exists (the ISO/OSI model), TCP/IP became the “de facto” standard for all network communications due to its simplicity and its ability to work well in every situation. In my opinion, the success of the TLS/SSL protocol relies on the fact that it was designed to perfectly match the TCP/IP protocol architecture. It maintains the TCP/IP’s layer-based design principles, thus inheriting all their advantages. It maybe for this reason that protocols like S-HTTP have not had the same success.
Figure 1 shows how TLS/SSL protocol interacts with the TCP/IP protocol.
TLS/SSL adds a new layer to the TCP/IP layers stack. As with all the other layers in the stack, the TLS/SSL protocols are independent of the protocols above and below, but the layer “speaks the same language” as the same layer on the other side of the communications channel. This design not only ensures full compatibility with all the network technologies based on TCP/IP, but it also enables them to easily “switch” to their secure versions without having to reinvent them from the ground up. So, HTTP became HTTPS without having to modify its specifications, FTP became FTPS, and so on.
To keep the data secure during the communication, TLS/SSL uses cryptographic techniques. Among the four goals of cryptography (confidentiality, integrity, authentication, and non-repudiation) TLS/SSL is able to guarantee confidentiality, integrity, and authentication. This is done principally in two steps:
- Authentication of the entities involved in the data exchange and negotiation of the cryptographic parameters to be used during the communication. This step uses asymmetric cryptography and X509 digital certificates.
- Symmetric encryption of exchanged data, and message authentication code (MAC) calculation and verification of each packet transmitted. The first assures confidentiality, the second integrity.
Referring to the TLS 1.2 specification defined in RFC 5246, the first step occurs by using a series of messages that the two communicating entities (client and server) exchange to start the secure communication. The protocol that specifies the kind of messages to be exchanged and their relative order is the Handshake Protocol
. During the handshake, the client and the server agree on the TLS/SSL protocol version to adopt, they decide on the cryptographic algorithms and the relative parameters that they will use, they (optionally) authenticate each other, and finally they exchange shared secrets to use during the second step.
Client Message: ClientHello => |
|
Exchanged data: |
· Max SSL Protocol version allowed · SessionID · Set of Cipher Suites · Set of Compression Methods · Random number |
The client sends the maximum number of the SSL protocol version it understands, a session id to be used to resume the communication if needed, a set of cipher suites and compression methods that it is able to use, and a random generated number to be used during the key exchange. |
|
Server Message: <= ServerHello |
|
Exchanged data: |
· SSL Protocol Selected · SessionID · Selected Cipher Suite · Selected Compression Method · Random Number |
The server sends the SSL protocol version selected (not exceeding the maximum supported by the client), a session id, the cipher suite and compression method selected from those suggested by the client, and a random number to be used during the key exchange. |
|
Server Message: <= ServerCertificate |
|
Exchanged data: |
· Server’s X509 Certificate(s) |
The server sends its X509 certificate (or X509 certificates chain) to use during the key exchange to encrypt key materials and to authenticate itself. |
|
Server Message: <= ServerKeyExchange |
|
Exchanged data: |
· Public key to use for the key exchange algorithm (optional) |
If the server’s certificate is not able to perform encryption, the server sends a public key to be used to encrypt key materials. |
|
Server Message: <= CertificateRequest |
|
Exchanged data: |
· None |
(Optional) If client authentication is required, the server sends the CertificateRequest message to the client |
|
Server Message: <= ServerHelloDone |
|
Exchanged data: |
· None |
End of the ServerHello. |
|
Client Message: Certificate => |
|
Exchanged data: |
· Client’s X509 Certificate (optional) |
If client authentication is required, the client sends to the server its X509 certificate (or X509 certificates chain). |
|
Client Message: ClientKeyExchange => |
|
Exchanged data: |
· Key material encrypted with the server’s certificate |
The client sends all the information needed to agree with the server the symmetric key to use during the communication. The type of information depends on the key exchange algorithm selected. |
|
Client Message: CertificateVerify => |
|
Exchanged data: |
· Signed handshake messages sent or received until now (optional) |
If client authentication is required, the client send all the handshake messages exchanged until now signed with its private key. The server verifies the client’s identity using the public key received. |
|
Client Message: ChangeCipherSpec + Finished => |
|
Exchanged data: |
· None |
End of the negotiation. |
|
Server Message: <= ChangeCipherSpec + Finished |
|
Exchanged data: |
· None |
End of the negotiation. |
In the second step, the data exchange takes place. In the same way as all the other TCP/IP layers, TLS/SSL maintains the fragment-based nature of the communication. Information is divided into small data units, and each unit is encapsulated with a header that contains the information needed by the other side of the communication channel to reconstruct the original message. A MAC is appended to each data fragment, and the overall content is encrypted using the symmetric algorithm and the key materials negotiated in first step. Figure 3 illustrates the composition of a TLS/SSL data unit.
TLS/SSL AND THE .NET FRAMEWORK
To allow us to implement a secure communication using the TLS/SSL protocol, .NET Framework comes with the SslStream
class. You can find it under the System.Net.Security
namespace.
As the name suggests, SslStream
is a class that derives from the Stream class. This implies that to use it, we need to apply almost the same logic as when we work with the other stream-based classes defined inside the .NET Framework. Its derivation tree is given by:
SslStream
- â
AuthenticatedStream
- â
Stream
… where AuthenticatedStream
is an abstract class that defines common properties when working with streams that need authentication of peers before starting the communication.
There are essentially two steps involved with the authenticated stream:
- Perform the authentication. In this step the identity of the remote peers is checked in order to assure that data will be delivered to a trusted entity. Eventually, the remote peers can request the sender authentication too.
- Exchange data using the stream. After the authentication, data can be transmitted from one entity to the other by using the common methods defined for all the classes that derive from the base Stream class.
The type of authentication depends on the type of system we are considering. As we have seen, when a client and a server exchange information using the TLS/SSL protocol, authentication occurs during the handshake using the X509 digital certificate.
To perform the handshake, the SslStream
class implements the AuthenticateAsServer(...)
and AuthenticateAsClient(...)
methods. As their names suggest, the former method occurs server side, while the latter occurs client side.
When the client invokes the AuthenticateAsClient(...)
method, the handshake begins and the server uses the AuthenticateAsServer(...)
method to respond to the client in order to identify itself and to negotiate the cryptographic algorithms and parameters that will be used. As developers, we can interact with the negotiation by setting the arguments required by the methods. These are summarized in Tables 2 and 3.
AuthenticateAsServer |
||
Type |
Name |
Description |
X509Certificate |
serverCertificate |
X509 certificate of the server. |
Bool |
clientCertificateRequired |
If true, the server imposes the client authentication. |
SslProtocols |
enabledSslProtocols |
List of TLS/SSL protocol accepted by the server. |
Bool |
checkCertificateRevocation |
If true, the server checks whether the client’s certificate, if required, has been revoked. |
AuthenticateAsClient |
||
Type |
Name |
Description |
String |
targetHost |
Server address to which the client connects. |
X509CertificateCollection |
clientCertificates |
If client authentication is required, the list of the X509 certificates that are able to identify the client and that the server recognizes. |
SslProtocols |
enabledSslProtocols |
Enum for the TLS/SSL protocol to use. |
Bool |
checkCertificateRevocation |
If true, the client checks whether the server’s certificate has been revoked. |
If the handshake succeeds, the SslStream
is ready to exchange data.
All the cryptographic parameters can be checked by querying the value of some properties implemented in the SslStream
class’s object model. Table 4 shows the principal properties.
SslStream Principal Properties |
||
Type |
Name |
Description |
SslProtocols |
SslProtocol |
TLS/SSL protocol negotiated during the handshake. |
ExchangeAlgortithmType |
KeyExchangeAlgorithm |
The key exchange algorithm adopted to exchange secret values to use during the symmetric encryption of data to be transmitted. |
Int |
KeyExchangeAlgorthmStrength |
Length, in bytes, of the asymmetric key pairs needed by the key exchange algorithm. |
CipherAlgorithmType |
CipherAlgorithm |
Negotiated symmetric algorithm. |
Int |
CipherStrength |
Length, in bytes, of the symmetric key to use for the symmetric encryption of data. |
HashAlgortihmType |
HashAlgorithm |
Hash algorithm to use for the MAC calculation. |
Int |
HashStrength |
MAC length, in bytes. |
USING THE SSLSTREAM CLASS
We are now ready to see the SslStream
class in action. We will use a simple example designed for this purpose.
We start our example by defining a simple communication system able to transfer data in a client/server scenario. Our system must have the following characteristic:
- It must allow us to interact directly with the communication stream.
- The data transport must be based on the TCP protocol (Table 1) in order to assure a reliable and a connection-oriented communication. This enables us to establish a session between the client and the server. When a session has been established, the TLS 1.2 specification permits resumption of an interrupted communication maintaining the parameters negotiated during the initial handshake.
On the basis of what we have just said, the TcpListener
and the TcpClient
classes of the .NET Framework are the right choice for our system. We create two console applications that emulate a simple TCP server and a simple TCP client using the code in Listings 1 and 2.
SERVER SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/// <summary> /// Start the server /// </summary> private static void RunServer() { TcpListener listener = null ; try { listener = new TcpListener (IPAddress .Any, 443); listener.Start(); Console .WriteLine("Ready ...\n" ); while (String .Compare(_message, "close" , StringComparison .CurrentCultureIgnoreCase ) != 0) { TcpClient client = listener.AcceptTcpClient(); ManageClientRequest(client); } } catch (Exception ex) { Console .WriteLine("Error detected: " + ex.Message); } finally { Console .WriteLine("\nStopped ..." ); } } |
CLIENT SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/// <summary> /// Start the client /// </summary> private static void RunClient() { while (true ) { Console .Write("\nInput message: " ); //read a string from the console. _message = Console .ReadLine(); if (String .Compare(_message, "close" , StringComparison .CurrentCultureIgnoreCase ) == 0) break ; if (!String .IsNullOrEmpty(_message)) { //create the client instance. It will perform calls to the server's //endpoint (192.168.1.21:443) TcpClient client = new TcpClient ("192.168.1.21" , 443); SendMessageToServer(client); } } |
In Listings 1 and 2, the _message string variable is defined at class level.
On the server side, the RunServer()
method performs the following tasks:
- It creates a
TcpListener
class by using, as communication endpoint, the IP address of the machine that hosts the server and port 443. Then it startsTcpListener
. - It waits for a client’s call, and for each client call, it manages the request using the
ManageClientRequest
(…) method. - When it receives the Close message, it exits.
On the client side, the RunClient()
method performs the following tasks:
- It reads a message from the console.
- It creates a
TcpClient
in order to send the message to the server. - It send the message to the server using the
SendMessageToServer()
method. - It repeats steps 1 and 2 until the Close message is received from the console, at which point it exits.
With the server up and running and the client ready to operate, we can now analyze how to transfer messages from the client to the server using TLS/SSL protocol. As we have seen, to accomplish this, the SendMessageToServer(...)
and ManageClientRequest(...)
methods have been implemented. The body of each method is shown in Listings 3 and 4.
SERVER SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/// /// <summary> /// Manage a client request /// </summary> private static void ManageClientRequest(TcpClient client) { try { bool leaveInnerStreamOpen = true ; RemoteCertificateValidationCallback validationCallback = new RemoteCertificateValidationCallback (ClientValidationCallback); LocalCertificateSelectionCallback selectionCallback = new LocalCertificateSelectionCallback (ServerCertificateSelectionCallback); EncryptionPolicy encryptionPolicy = EncryptionPolicy .AllowNoEncryption; //create the SSL stream starting from the NetworkStream associated //with the TcpClient instance _sslStream = new SslStream (client.GetStream(), leaveInnerStreamOpen, validationCallback, selectionCallback, encryptionPolicy); //1. when the client requests it, the handshake begins ServerSideHandshake(); //2. read client's data using the encrypted stream ReadClientData(); } catch (Exception ex) { Console .WriteLine("\nError detected: " + ex.Message); } finally { if (_sslStream != null ) _sslStream.Close(); client.Close(); } } |
CLIENT SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/// <summary> /// Send a message to the server using TLS/SSL /// </summary> private static void SendMessageToServer(TcpClient client) { try { bool leaveInnerStreamOpen = false ; RemoteCertificateValidationCallback validationCallback = new RemoteCertificateValidationCallback (ServerValidationCallback); LocalCertificateSelectionCallback selectionCallback = new LocalCertificateSelectionCallback (ClientCertificateSelectionCallback); EncryptionPolicy encryptionPolicy = EncryptionPolicy .RequireEncryption; //create the SSL stream starting from the NetworkStream associated //with the TcpClient instance _sslStream = new SslStream (client.GetStream(), leaveInnerStreamOpen, validationCallback, selectionCallback, encryptionPolicy); //1. start the authentication process. If it doesn't succeed //an AuthenticationException is thrown ClienSideHandshake(); //2. send the input message to the server SendDataToServer(); } catch (AuthenticationException ex) { Console .WriteLine("\nAuthentication Exception: " + ex.Message); } catch (Exception ex) { Console .WriteLine("\nError detected: " + ex.Message); } finally { if (_sslStream != null ) _sslStream.Close(); client.Close(); } } |
In Listings 3 and 4, the _SslStream
is defined at class level.
Let’s start by considering the server side method. In this, an SslStream
instance is created by using, as underlying stream, the NetworkStream
stream associated with the TcpClient
‘s object. This object is created when theAcceptTcpClient()
method of the TcpListener
object informs the server that a client request has arrived.
The SslStream
constructor uses four additional arguments:
bool leaveInnerStreamOpen
: This allows the status of the inner stream to be set (open or closed).RemoteCertificateValidationCallback validationCallback
: This method is invoked on the server after the client’s certificate validation. It permits all the exception conditions that eventually occur to be managed, and allows further checks to be made prior to continuing with the handshake. The method has a return value of type Boolean. If true is returned, the handshake continues; otherwise, an AuthenticationException is thrown.LocalCertificateSelectionCallback selectionCallback
: This method is invoked when the server’s certificate is selected. With it, we can perform some checks about its nature and state. The method has a return value of type X509Certificate2. We need to return to the caller the certificate selected. If null is returned, a NotSupportedException is thrown and the communication ends.EncryptionPolicy encryptionPolicy
: This is an enumeration that allows us to specify whether the server requires, allows, or does not allow encryption.
The same situation occurs at client side. In this case, we create an object of type TcpClient
that to communicate with the server. Its associated NetworkStream
object is used to create the SslStream
instance. We use, as arguments of its constructor, the same arguments with almost the same meaning as those used for the server’s code. In this case, the validationCallback
method is related to the server’s certificate validation; theselectCallback
method is related to the client’s certificate selection; and the encryptionPolicy
value is used to specify whether the stream will be encrypted or not. The encryptionPolicy
setting must be compatible with the analogue server’s setting, otherwise, an exception is thrown: if the server requires encryption, the client must require it too; if the server allows encryption, the client can use encryption or not; if the server doesn’t allow encryption, the client must not use it.
After the SslStream
instance generation, the two methods continue performing the handshake. This is done by invoking the ServerSideHandshake()
and ClientSideHandshake()
methods. If it doesn’t succeed, an AuthenticationException
is thrown and the communication ends. If it succeeds, the client sends data to the server using the SendDataToServer()
method, while the server reads it using theReadClientData()
method.
THE HANDSHAKE
In our example, we want to implement a TLS/SSL server with the following characteristics:
- It must allow both TLS 1.0 that SSL 3.0 protocols. This requirement is imposed by the SslProtocols enumeration. Table 5 lists all its possible values.
- It must require client authentication. The client needs a valid X509 certificate to be able to perform the data exchange.
- It must require that the client’s certificate is valid and hasn’t been revoked. This implies that the
validationCallback
delegate defined in theManageClientRequest()
method (Listing 3) will be invoked.
Value |
Description |
TLS |
The TLS 1.0 protocol as defined in the RFC 2246. |
SSL3 |
The SSL protocol, defined by Netscape Communication, from which the TLS 1.0 is derived. |
SSL2 |
The first public available version of the SSL protocol created by Netscape (SSL 1.0 has never been published). |
Default |
Both TLS and SSL3 are acceptable. TLS has higher priority than SSL3. |
None |
Empty bit flag. |
The client must:
- Require the TLS protocol.
- Require that the server’s certificate is valid and hasn’t been revoked. This implies that the
validationCallback
delegate defined in theSendMessageToServer(...)
method (Listing 4) will be invoked.
We need to implement the ServerSideHandshake()
and ClientSideHandshake()
methods as shown in Listings 5 and 6.
SERVER SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/// <summary> /// Perform the server handshake /// </summary> private static void ServerSideHandshake() { X509Certificate2 certificate = GetServerCertificate("ssl_server" ); bool requireClientCertificate = true ; SslProtocols enabledSslProtocols = SslProtocols .Ssl3 | SslProtocols .Tls; bool checkCertificateRevocation = true ; _sslStream.AuthenticateAsServer (certificate,requireClientCertificate, enabledSslProtocols, checkCertificateRevocation); } |
CLIENT SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/// <summary> /// Perform the client handshake /// </summary> private static void ClientSideHandshake() { Console .WriteLine("\nStart authentication ... " ); X509CertificateCollection clientCertificates = GetClientCertificates("ssl_client" ); string targetHost = "192.168.1.21" ; SslProtocols sslProtocol = SslProtocols .Tls; bool checkCertificateRevocation = true ; //Start the handshake _sslStream.AuthenticateAsClient (targetHost, clientCertificates, sslProtocol, checkCertificateRevocation); } |
In Listings 5 and 6, the GetServerCertificate(...)
and GetClientCertificates(...)
methods are helper methods that allow us to load, from the certificate store, the server and client certificates respectively. Note that, while the AuthenticateAsServer(...)
method requires an X509Certificate2 object as an argument, the AuthenticateAsClient(...)
method requires a X509Certificate2Collection object. This is because, at this stage, the client can specify more than one certificate to authenticate itself. The certificate used will be selected a second time, and it must be a certificate that the server is able to recognize. We will see how this is done in section 4.3.
VALIDATION
During the handshake, both client and server are able to validate the information they exchange. As seen in Listings 3 and 4, the validation results are carried out with the aid of the ServerValidationCallback(...)
and ClientValidationCallback(...
) methods. They are implemented as shown in Listings 7 and 8.
SERVER SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/// <summary> /// Callback for the verification of the client's certificate /// </summary> private static bool ClientValidationCallback ( object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { switch (sslPolicyErrors) { case SslPolicyErrors .RemoteCertificateNameMismatch: Console .WriteLine( "Client's name mismatch. End communication ...\n" ); return false ; case SslPolicyErrors .RemoteCertificateNotAvailable: Console .WriteLine( "Client's certificate not available. End communication ...\n" ); return false ; case SslPolicyErrors .RemoteCertificateChainErrors: Console .WriteLine( "Client's certificate validation failed. End communication ...\n" ); return false ; } //Perform others checks using the "certificate" and "chain" objects ... // ... // ... Console .WriteLine( "Client's authentication succeeded ...\n" ); return true ; } |
CLIENT SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/// <summary> /// Callback for the verification of the server certificate /// </summary> private static bool ServerValidationCallback ( object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { switch (sslPolicyErrors) { case SslPolicyErrors .RemoteCertificateNameMismatch: Console .WriteLine( "Server name mismatch. End communication ...\n" ); return false ; case SslPolicyErrors .RemoteCertificateNotAvailable: Console .WriteLine( "Server's certificate not available. End communication ...\n" ); return false ; case SslPolicyErrors .RemoteCertificateChainErrors: Console .WriteLine( "Server's certificate validation failed. End communication ...\n" ); return false ; } //Perform others checks using the "certificate" and "chain" objects ... // ... // ... Console .WriteLine( "Server's authentication succeeded ...\n" ); return true ; } |
The two methods in Listings 7 and 8 are quite similar. They get, as method arguments, the certificate being validated, the certificate chain (given by the collection of all the certification authorities’ and sub-certification authorities’ certificates involved when it was issued, plus information about how they relate to each other) and an SslPolicyErrors
enumeration value that states which kind of exception eventually occurs during the validation. For this last, three values are possible:
RemoteCertificateNameMismatch
: occurs when the server certificate’s common name is not identical to the server name.RemoteCertificateNotAvailable
: occurs when a valid certificate to use for the communication has not been found.RemoteCertificateChainErrors
: occurs when the certificate cannot be validated by using its certificate chain; or when the certificate has been revoked; or when the certificate revocation list cannot be found and the server or the client needs to check the certificate’s revocation status (because the checkCertificateRevocation argument in Listings 5 and 6 is true).
When one of these exceptions occurs, or when we are not satisfied by further checks that we can perform by using the certificate or the certificate chain, we can end the communication by forcing the method to return the “false” value. If all goes well or we want to proceed anyway, we only need to return the “true” value.
CERTIFICATES SELECTION
SslServer’s class allows us to interact with the certificate selection both client side and server side. We have seen that when we create the SslStream
‘s class instances, we can optionally specify a callback to use for this purpose. In our example, we have implemented the ServerCertificateSelectionCallback(...)
and ClientCertificateSelectionCallback(...)
methods. The method body for each is shown in Listings 9 and 10.
Server Side
1 2 3 4 5 6 7 8 9 10 11 |
/// <summary> /// Certificate selection callback. /// </summary> public static X509Certificate ServerCertificateSelectionCallback ( object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string [] acceptableIssuers) { //perform some checks on the certificate... // ... // ... //return the selected certificate. If null is returned a // NotSupported exception is thrown. return localCertificates[0]; } |
CLIENT SIDE
1 2 3 4 5 6 7 8 9 10 11 |
/// <summary> /// Certificate selection callback. /// </summary> public static X509Certificate ClientCertificateSelectionCallback ( object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string [] acceptableIssuers) { //perform some checks on the certificate ... // ... // ... //return the selected certificate. If null is returned, the client's authentication does //not take place. return localCertificates[0]; } |
They are almost identical. They receive the following parameters as input:
sender
: A reference to theSslStream
object used for the communication.targetHost
: The address of the remote host.localCertificates
: The collection of certificates that was used as the argument for theSslStream
constructor when it was instantiated, plus all the certificates that belong to their chain.remoteCertificate
: The certificate of the remote entity involved in the secure communication.acceptableIssuers
: An array of all the certificate issuer’s distinguished names that are allowed.
The two methods must return an X509Certificate2 object, which is the certificate that will be used during the handshake.
My tests have brought me to the following conclusions:
- The server side method is not useful at all. In each situation, only the sender and the
localCertificates argument
has a value different from null (or empty array). When the method returns null, aNotSupportedException
exception is thrown. Maybe the callback has been implemented only for the client side. Perhaps, rather than implementing two differentSslStream
classes, (one for the server, and a different one for the client, with theLocalCertificateSelectionCallback(...)
implementation), the .NET Framework teams decided to implement a single class for both, giving a dummy meaning to the server side’sLocalCertificateSelectionCallback(...)
delegate. - During the handshake, the client side method is invoked twice. In the first invocation, the situation is very similar to the above. If we return a bad certificate (maybe a revoked one), the handshake proceeds without errors. In the second call, the
acceptableIssuers
array has values too, as does thelocalCertificates
collection. The former contains the distinguished name of all the issuers that the server is able to recognize; the latter is filled with all the certificates that we used as argument for theAuthenticateAsClient(...)
method. If we return a bad certificate, aRemoteCertificateChainErrors
is thrown. If we return null, the handshake will proceed anyway but without the client validation (we can check this condition by checking the value of theIsMutuallyAuthenticated
property of theSslStream
class). If we take a look at Table 1, it is reasonable to assume that the two calls are executed during the Certificate message and theCertificateVerify
message. If the first doesn’t need a certificate validation, in the second case, it is required.
DATA EXCHANGE
If the handshake ends properly, data exchange can take place. For this, we only need to use the common methods related to the Stream base class. We can use the code shown in Listings 11 and 12:
SERVER SIDE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// <summary> /// Read client's data /// </summary> private static void ReadClientData() { byte [] buffer = new byte [1024]; int n = _sslStream.Read(buffer, 0, 1024); Array .Resize< byte >( ref buffer, n); _message = Encoding .UTF8.GetString(buffer); Console .WriteLine( "Client said: " + _message); } |
CLIENT SIDE
1 2 3 4 5 6 7 8 |
/// <summary> /// Send data to server /// </summary> private static void SendDataToServer() { byte [] buffer = Encoding .UTF8.GetBytes(_message); _sslStream.Write(buffer, 0, buffer.Length); } |
The client uses the Write(…) method of the SslStream
class instance to write each message acquired by the console into the SSL stream. The server reads it by using the Read(…) method of its SslStream
class instance.
CIPHERSUITES
We are now able to use our example to investigate how the SslStream
class behaves in relation to the different settings that we are allowed to set. The SslStream
class defined inside the .NET Framework allows us to:
§ Vary the TLS/SSL protocol to use.
§ Vary the characteristics of the certificates to use in term of keys lengths, asymmetric algorithms, and hash algorithms.
All the others parameters (key exchange algorithm, symmetric algorithm to use for the encryption of the stream’s data, keys strength etc…) are determined accordingly.
We would like to search for the CipherSuite
that the SslStream
class negotiates. CipherSuites
are defined in each TLS or SSL protocol version, and they are stated with some combination of (SignatureAlgorithm)-KeyExhangeAlgortihm-SymmetricAlgorithm-HashAlgorithm. The SslStream
class doesn’t allow us to select a CipherSuite
, so we would like to check which is used.
We perform our analysis by obtaining a set of the so called SSL certificates for the server and SSL certificates for the client. Table 6 shows the principal characteristics they need to have in order to be acceptable for our purpose.
Characteristic |
Ssl Server |
Ssl Client |
Common Name |
Server’s DNS name or IP |
Any |
KeyUsage |
Digital Signature, Key Encipherment |
Digital Signature |
EnhancedKeyUsage |
Server Authentication (1.3.6.1.5.5.7.3.1) |
Client Authentication (1.3.6.1.5.5.7.3.2) |
We need to have the certificate revocation list associated with them too, and install it on our machine. If we don’t have it, we must set to false the checkCertificateRevocation
argument of the AuthenticateAsServer(...)
and AuthenticateAsClient(...)
methods in order to avoid exceptions.
On the basis of some tests that I made, I have found that the client authentication occurs even if we use a digital signature’s certificate for the client. In my opinion, this is not a good thing. Whatever the certification authority we use, private or commercial, the issuing of a certificate implies (or should imply) the acceptance of some certificate policies that state under which condition the certificate is valid (for more information see the RFC3647 )). Suppose that a certification authority issues a digital signature’s certificate to an entity that must use it only to sign documents. What if the entity uses the same certificate to access reserved, for it, data stored in a remote server that uses TLS/SSL and client authentication? To avoid this you can use the ServerValidationCallback(...)
delegate defined in Listing 8 to check the EnhancedKeyUsage
extension value of the client’s certificate and refuse the communication if its Object Identifier (OID) is not 1.3.6.1.5.5.7.3.2.
After the handshake, the SSL stream’s properties can be investigated by checking the value of the SslStream
class properties reported in Table 4. Table 7 shows the result obtained for each allowed protocol when certificates are generate using the RSA algorithm.
Protocol |
KEA |
SYM (bit) |
HSH (bit) |
CipherSuite |
TLS1.0 |
RSAKeyX |
AES (128) |
SHA1 (160) |
TLS_RSA_WITH_AES_128_CBC_SHA |
SSL3.0 |
RSAKeyX |
RC4 (128) |
SHA1 (160) |
SSL_RSA_WITH_RC4_128_SHA |
SSL2.0 |
RSAKeyX |
RC4 (128) |
MD5 (128) |
SSL_CK_RC4_128_WITH_MD5 |
CONCLUSION
As you have seen, to implement at low level a secure communication channel by using the TLS/SSL protocol isn’t a trivial task. It requires at least a basic knowledge about symmetric and asymmetric cryptography, hash calculation and public key infrastructure (PKI). Even if in the last few years a large demand for security has taken place, the complexity behind it sometimes is the reason why it is put aside. I hope this article can help you to better understand the TLS/SSL protocol and the principles behind it thus simplifying your job when dealing with it.
Load comments