SSL(Security Socket Layer)是TLS(Transport Layer Security)的前身,是一种加解密协议,它提供了再网络上的安全传输,它介于网络通信协议的传输层与应用层之间。
为实现TCP层之上的ssl通信,需要用到数字证书。本文通过具体例子来说明如何使用数字证书来实现网络上的安全传输。需要用到.net提供的SslStream, TcpListener, TcpClient, X509Certificate2,X509Store,X509Certification2Collection等类。终于开始涉及到代码了。
一.服务器端
1.指定证书
常用的有两种方式:从文件获取和从certificate store中获取
a.从文件
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2 dialog.Filter = " Cert files(*.pfx;*.p7b;*.cer)|*.pfx;*.p7b;*.cer|All files(*.*)|*.* " ;
3 DialogResult dr = dialog.ShowDialog();
4 if (dr == DialogResult.OK)
5 {
6 Console.WriteLine( " Input the password for the cert: " );
7 StringBuilder stringBuilder = new StringBuilder();
8 while ( true )
9 {
10 ConsoleKeyInfo passInfo = Console.ReadKey( true );
11 if (passInfo.Key == ConsoleKey.Enter)
12 {
13 break ;
14 }
15 stringBuilder.Append(passInfo.KeyChar);
16 }
17 return new X509Certificate2(dialog.FileName, stringBuilder.ToString(), X509KeyStorageFlags.Exportable);
18 }
19 else
20 {
21 return null ;
22 }
注意X509Certificate2构造函数第三个参数,如果想把调用Export方法将cert对象到处,此处必须使用Exportable标记,否则在导出时会抛出异常。
pfx格式的证书包含有private key,因此需要密码的保护,构造函数的第二个参数就是密码。
选取的证书必须包含有private key,否则在SSL的server端使用时会抛出AuthenticationException。
怎么得到pfx文件:使用MMC->File->Add/Remove Sanp-in->Add->Certificates->Add->My user account/Computer account->Finish 查看存储在本机当前用户和所有用户的证书,选择用导出的证书,
右键->All Tasks...->Export...注意要勾选[Yes, export the private key],如果该Radio button被禁用,说明该证书的private key不能被导出,可能是在导入该证书时没有选择标记private key为可导出,如下图所示:
b.从certificate store
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2 store.Open(OpenFlags.ReadOnly);
3 X509Certificate2Collection collection = X509Certificate2UI.SelectFromCollection(store.Certificates,
4 " Select Certificate " ,
5 " Please select a certificate from the following list " ,
6 X509SelectionFlag.SingleSelection);
注意:
Server端指定的cert必须含有privatekey,且Enhanced key usage必须含有Server Authentication (1.3.6.1.5.5.7.3.1)
没有private key:
NotSupportedException: The server modeSSL must use a certificate with the associated private key.
证书purpose不对:
server端:AuthenticationException: A call to SSPIfailed, see inner exception.
client端:IOException: Unable to read data from the transport connection: Anexisting connection was forcibly closed by the remote host..
2.开始TCP监听
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2 listener.Start();
3 while ( true )
4 {
5 Console.WriteLine( " Waiting for a client to connect... " );
6 TcpClient client = listener.AcceptTcpClient();
7 ......
8 }
3.指定服务器证书
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2 // SslStream using the client's network stream.
3 SslStream sslStream = new SslStream(client.GetStream(), false );
4 // Authenticate the server but don't require the client to authenticate.
5 try
6 {
7 sslStream.AuthenticateAsServer(cert, false , SslProtocols.Default, false );
8
9 // Set timeouts for the read and write to 5 seconds.
10 sslStream.ReadTimeout = 5000 ;
11 sslStream.WriteTimeout = 5000 ;
12 ......
13 }
4.发送数据
2 sslStream.Write(message);
3 sslStream.Flush();
5.接收数据
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2 StringBuilder messageData = new StringBuilder();
3 int bytes = - 1 ;
4 do
5 {
6 // Read the client's message.
7 bytes = sslStream.Read(buffer, 0 , buffer.Length);
8 messageData.Append(Encoding.UTF8.GetString(buffer, 0 , bytes));
9 // Check for EOF or an empty message.
10 if (messageData.ToString().IndexOf( " <EOF> " ) != - 1 )
11 {
12 break ;
13 }
14 } while (bytes != 0 );
15
16 return messageData.ToString();
注意:Write后需要调用Flush将数据立刻发送,Read需要多次调用,确定读不到数据位置,因为TCP连接时Stream方式的,在网络中传输可能会分包到达,一次无法全部读取,还需要消息边界。
6.结束
2 client.Close();
二.客户端
1.与服务器端建立TCP连接
2.与服务端建立SSL握手
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2 client.GetStream(),
3 false ,
4 new RemoteCertificateValidationCallback(ValidateServerCertificate),
5 null
6 );
7 try
8 {
9 // The server name must match the name on the server certificate.
10 X509Certificate2 cert = GetCert();
11 X509Certificate2Collection collection = new X509Certificate2Collection();
12 if (cert != null )
13 {
14 collection.Add(cert);
15 }
16 sslStream.AuthenticateAsClient(serverName, collection, SslProtocols.Default, false );
17 }
18 catch (AuthenticationException e)
19 {
20 Console.WriteLine( " Exception: {0} " , e.Message);
21 if (e.InnerException != null )
22 {
23 Console.WriteLine( " Inner exception: {0} " , e.InnerException.Message);
24 }
25 Console.WriteLine( " Authentication failed - closing the connection. " );
26 client.Close();
27 return ;
28 }
如果服务端在调用AuthenticateAsServer方法时指定不需要客户端的证书,则客户端在调用AuthenticateAsClient时可以不指定证书,
如果server在AuthenticateAsServer是指定client需要cert,而client在调用AuthenticateAsClient时没有指定cert或者cert没有private key时:
server端:AuthenticationException:The remote certificate is invalid according to the validationprocedure.
client端:IOException: Unable to read data from the transportconnection: An established connection was aborted by the software in your hostmachine.
Client在AuthenticateAsClient方法指定的名字需要与server端使用cert的名字一致,否则在RemoteCertificateValidationCallback事件中SslPolicyErrors会是RemoteCertificateNameMismatch
3.发送接收数据,关闭连接,与服务器端方法相同
使用Wireshark Network Analyzer工具进行抓包分析,发现在建立TCP连接后,首先进行SSL握手,之后传输的数据都是被加密的,如下图所示:
对SSL的握手和加密传输的详细过程,将在下节分析。