tls相关
大致原理
为了让两个之间实现安全传输,(我们把服务端统一叫做TcpServer,客户端统一叫做TcpClient),TcpServer在listen完了accept之后要用一个证书来声明自己是谁,而TcpClient在connect之后要问TcpServer是否具有自己想要的一个证书(确认服务端身份),如果是自己指定的那个证书,就说明是自己要连接的那个TcpServer,这时候连接就会建立成功,以后发给TcpServer的数据,都会用TcpServer声明的证书里面的公钥来加密后再发送给TcpServer,TcpServer会用自己机器里的私钥对数据解密。
以上保证了TcpClient不会连接到非法的TcpServer,第三方截取TcpClient发送的数据也解不开。
然后TcpServer也可以(可选)要求TcpClient必须提供证书,用以证明是传入的TcpClient自己信任的TcpClient。TcpClient会把证书(从某个CA申请的)进行签名后发送给TcpServer,TcpServer会确认TcpClient提供的证书发行者是否是自己信任的发行者(就是是否装有这个发行者的根证书),如果信任就会建立连接成功。
以上保证了TcpServer和TcpClient之间实现了双向的身份验证,建立了信任连接。这时候TcpServer和TcpClient会各自用一定的算法生成一个密钥做为会话密钥,并通知给对方,之后相互传输数据就用这个会话密钥进行对称加密后传给对方,这样做是因为对称加密比非对称加密性能要好一个数量级。(密钥交换一般是服务端用一段随机数传给客户端,客户端用一段随机数传给服务端,这个传输是安全传输,然后双方用两个随机数合并起来的一个数做为会话密钥)
其中的用sha1做为消息完整性算法,3des做为消息机密性算法,rsa做为密钥交换算法(在安全策略里打开fips选项 )。
证书相关操作
申请服务端证书
先找一台机器装上证书颁发服务(在添加/删除程序-添加删除windows组件里),然后再另一台机器通过浏览器申请证书,该网址类似如下(其中ms-onlytiancai为证书服务器机器名):
http://ms-onlytiancai/certsrv/
选择高级申请证书,如下图,识别信息要填全,否则客户端验证的时候会出现证书链(我测的时候确实如此,但证书链的概念是根CA和多级的中级CA之间信任关系的一个链,具体看相关链接)失败。d
证书类型要选择“服务器身份验证证书(有些CA可能没有单独的这样的模板,那就选择“计算机副本”证书,该证书可以用来验证服务器身份和客户端身份)”,其它选项不要动,最后点申请,然后再在CA服务器的【管理工具】-【证书颁发机构】的“挂起的证书”里把刚申请的证书颁发一下,最后还在证书申请网页上点“查看挂起的证书申请的状态”,通过向导,安装证书,提示你是否信任该CA时,点是,这时候会自动把该证书安装到“个人”区域,并把该CA证书安装到“受信任的根证书颁发机构”。这一步是必须的,这样做服务端就会信任该CA发行的所有证书(如果这一步没有安装根证书,后面可以从控制台里把CA证书单独导入到本地计算机的受信任颁发机构里,CA证书不是申请的,是直接在证书申请页面的下方的链接下载的),后面申请客户端证书的时候也从这个CA来申请,服务端才会信任这个客户端。
最后申请下来的证书,大致如上图所示,必须是服务器身份认证证书(至少证书目的里由“保证远程计算机身份”),而且必须得有一个该证书对应的私钥,这个私钥应该是在DPAPI(CAPI)存储器里存着呢,按照上面的操作应该会得到这样的一个证书。用makecert -r -pe -n "CN=TestCert" -e 01/01/2036 -sr localMachine c:" TestCert.cer生成的证书不会有对应的私钥,这样的证书是不能用的,到时候客户端验证的时候会抛一个异常,提示你没有私钥(需要一个关联私钥的证书)。
查看本机已安装的证书,在IE的选项-内容-证书选项里(在控制台里添加/删除管理单元里添加“证书管理单元”也可以管理)。
申请服务端证书,姓名字段一定要写成服务器的名称,netbios名或者dns名称。
申请客户端证书
过程和申请服务端证书一样,只是证书类型选择“客户端身份验证证书”。
导出证书
在证书管理控制台里把服务端证书和客户端证书全部导出到本地,导出选项全部为默认。
服务器为:C:"certs"huhao.pxe(带私钥)
客户端为:C:"certs"client.cer
如果服务端为windows服务,需要把服务端证书导入到本地计算机里,默认服务器证书只安装到了当前用户的个人区域,需要从当前用户的个人区域导出来,再导入到本地计算机的个人区域,导出的时候记着选上导出私钥,否则导入到本地计算机里的证书就不会关联私钥了。
应用相关
服务端代码
类库
using System;
using System.Net.Sockets;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Security.Authentication;
using System.Threading;
class TCPServer_SSL
{
private TcpListener _listener = null;
private IPAddress _address = IPAddress.Parse("127.0.0.1");
private int _port = 55555;
CTORs#region CTORs
public TCPServer_SSL()
{
}
public TCPServer_SSL(string address, string port)
{
_port = Convert.ToInt32(port);
_address = IPAddress.Parse(address);
}
#endregion // CTORs
Properties#region Properties
public IPAddress Address
{
get { return _address; }
set { _address = value; }
}
public int Port
{
get { return _port; }
set { _port = value; }
}
#endregion
public void Listen()
{
try
{
_listener = new TcpListener(_address, _port);
// Fire up the server.
_listener.Start();
// Enter the listening loop.
while (true)
{
Console.Write("Looking for someone to talk to… ");
// Wait for connection.
TcpClient newClient = _listener.AcceptTcpClient();
Console.WriteLine("Connected to new client");
// Spin a thread to take care of the client.
ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessClient),
newClient);
}
}
catch (SocketException e)
{
Console.WriteLine("SocketException: {0}", e);
}
finally
{
// Shut it down.
_listener.Stop();
}
Console.WriteLine("\nHit any key (where is ANYKEY?) to continue…");
Console.Read();
}
private void ProcessClient(object client)
{
using (TcpClient newClient = (TcpClient)client)
{
// Buffer for reading data.
byte[] bytes = new byte[1024];
string clientData = null;
//第三个参数是验证客户端证书的回调,最后一个参数是用来指定多个证明自己的证书的回调,这里为空
//前两个参数分别是一个流和是否在关闭sslstrem关闭内部流的选项
using (SslStream sslStream = new SslStream(newClient.GetStream(), false, ValidateClientCertificate,null))
{
try
{
//该方法的第一个参数是用于服务端身份验证的证书,第二个参数指定是否需要验证客户端的身份
//第三个参数是指定安全传输的协议,最后一个参数指定是否检查吊销证书
sslStream.AuthenticateAsServer(GetServerCert("117f32ff000000000007"), true, SslProtocols.Tls, false);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
// Loop to receive all the data sent by the client.
int bytesRead = 0;
while ((bytesRead = sslStream.Read(bytes, 0, bytes.Length)) != 0)
{
// Translate data bytes to an ASCII string.
clientData = Encoding.ASCII.GetString(bytes, 0, bytesRead);
Console.WriteLine("Client says: {0}", clientData);
// Thank them for their input.
bytes = Encoding.ASCII.GetBytes("Thanks call again!");
// Send back a response.
sslStream.Write(bytes, 0, bytes.Length);
}
}
}
}
public static bool ValidateClientCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
Console.WriteLine("ValidateClientCertificate-certificate.Subject:\r\n{0}", certificate.Subject);
if (sslPolicyErrors != SslPolicyErrors.None && sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors)
return false;
if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors)
{
//不判断吊销证书
foreach (X509ChainStatus s in chain.ChainStatus)
{
Console.WriteLine("ValidateClientCertificate-chain.ChainStatus:\r\n{0}-{1}", s.Status, s.StatusInformation);
if (s.Status != X509ChainStatusFlags.OfflineRevocation && s.Status != X509ChainStatusFlags.RevocationStatusUnknown)
{
return false;
}
}
}
return true;
}
//该方法从本地计算机的个人证书区域按证书序列号查找指定的证书用于服务器身份验证
//以及打印出证书的详细信息
private static X509Certificate GetServerCert(string serialNumber)
{
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificate =
store.Certificates.Find(X509FindType.FindBySerialNumber,
serialNumber, true);
X509Certificate2 x509 = certificate[0];
byte[] rawdata = x509.RawData;
Console.WriteLine("509 count :{0}", store.Certificates.Count);
Console.WriteLine("Content Type: {0}{1}", X509Certificate2.GetCertContentType(rawdata), Environment.NewLine);
Console.WriteLine("Friendly Name: {0}{1}", x509.FriendlyName, Environment.NewLine);
Console.WriteLine("Certificate Verified?: {0}{1}", x509.Verify(), Environment.NewLine);
Console.WriteLine("Simple Name: {0}{1}", x509.GetNameInfo(X509NameType.SimpleName, true), Environment.NewLine);
Console.WriteLine("Signature Algorithm: {0}{1}", x509.SignatureAlgorithm.FriendlyName, Environment.NewLine);
Console.WriteLine("Private Key: {0}{1}", x509.PrivateKey.ToXmlString(false), Environment.NewLine);
Console.WriteLine("Public Key: {0}{1}", x509.PublicKey.Key.ToXmlString(false), Environment.NewLine);
Console.WriteLine("Certificate Archived?: {0}{1}", x509.Archived, Environment.NewLine);
Console.WriteLine("Length of Raw Data: {0}{1}", x509.RawData.Length, Environment.NewLine);
Console.WriteLine("x509.SerialNumber: {0}{1}", x509.SerialNumber, Environment.NewLine);
if (certificate.Count > 0)
return (certificate[0]);
else
return (null);
}
catch (Exception ex)
{
Console.WriteLine(ex);
return null;
}
finally
{
store.Close();
}
}
}
控制台代码
static void Main(string[] args)
{
try
{
TCPServer_SSL server = new TCPServer_SSL("127.0.0.1", "8000");
server.Listen();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.Read();
}
客户端
类库
using System;
using System.Net.Sockets;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
class TCPClient_SSL
{
private TcpClient _client = null;
private IPAddress _address = IPAddress.Parse("127.0.0.1");
private int _port = 5;
private IPEndPoint _endPoint = null;
public TCPClient_SSL(string address, string port)
{
_address = IPAddress.Parse(address);
_port = Convert.ToInt32(port);
_endPoint = new IPEndPoint(_address, _port);
}
public void ConnectToServer(string msg)
{
try
{
using (_client = new TcpClient())
{
_client.Connect(_endPoint);
//SslStream第一个参数是一个流,可以是由用Socket类new的一个NetworkStream
//第二个参数为false的时候,关闭sslStream就会关闭对应的NetworkStream
//第三个参数是一个回调,用来控制服务端的验证
using (SslStream sslStream = new SslStream(_client.GetStream(),
false, new RemoteCertificateValidationCallback(
ValidateServerCertificate)))
{
X509CertificateCollection cert = new X509CertificateCollection();
//下面是从一个导出证书文件里加载证书
X509Certificate cer = X509Certificate2.CreateFromCertFile("c:\\certs\\huhao.cer");
cert.Add(cer);
//AuthenticateAsClient方法第一个参数要写服务端证书的名字,和服务器的机器名和dns名应一致
//第二个参数是一个证书集合,如果是多个证书,可以在一个回调函数里选择指定证书用以验证客户端
//第三个参数指定安全传输的协议,可以是ssl,tls的不同版本
//最后一个参数表示是否检查吊销证书列表,吊销证书是定时推给服务器的,验证起来比较耗性能
sslStream.AuthenticateAsClient("ms-7fa82788ed1e", cert, System.Security.Authentication.SslProtocols.Tls, false);
sslStream.ReadTimeout = 5000;
sslStream.WriteTimeout = 5000;
// Get the bytes to send for the message.
byte[] bytes = Encoding.ASCII.GetBytes(msg);
// Send message.
Console.WriteLine("Sending message to server: " + msg);
//这里可以使用BeginWrite
sslStream.Write(bytes, 0, bytes.Length);
// Get the response.
// Buffer to store the response bytes.
bytes = new byte[1024];
// Display the response.
//这里可以使用BeginRead
int bytesRead = sslStream.Read(bytes, 0, bytes.Length);
string serverResponse = Encoding.ASCII.GetString(bytes, 0,
bytesRead);
Console.WriteLine("Server said: " + serverResponse);
}
}
}
catch (SocketException e)
{
Console.WriteLine("There was an error talking to the server: {0}",
e.ToString());
}
}
private bool ValidateServerCertificate(object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
else
{
if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
{
Console.WriteLine("The X509Chain.ChainStatus returned an array " +
"of X509ChainStatus objects containing error information.");
}
else if (sslPolicyErrors ==
SslPolicyErrors.RemoteCertificateNameMismatch)
{
Console.WriteLine("There was a mismatch of the name " +
"on a certificate.");
}
else if (sslPolicyErrors ==
SslPolicyErrors.RemoteCertificateNotAvailable)
{
Console.WriteLine("No certificate was available.");
}
else
{
Console.WriteLine("SSL Certificate Validation Error!");
}
}
Console.WriteLine(Environment.NewLine +
"SSL Certificate Validation Error!");
Console.WriteLine(sslPolicyErrors.ToString());
return false;
}
}
控制台
static void Main(string[] args)
{
try
{
TCPClient_SSL client = new TCPClient_SSL("127.0.0.1", "8000");
client.ConnectToServer("11111");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.Read();
}
注意事项
1. 如果TcpServer是一个windows服务,用于服务端的证书不能用X509Certificate2.CreateFromCertFile方法加载导出的证书文件来获取,而应该用相关API来读取本地计算机证书存储器的my区域的证书,查找可以按证书序列号来查,具体代码大约如下。
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection cert = store.Certificates.Find(X509FindType.FindBySerialNumber,
“611cea09000000000002”, false);
_certificate = cert[0];
2. 如果是服务端是windows服务,由于windows服务运行账户是本地系统账户,并不是某个具体用户,所以这时候用于服务端验证的证书应该导入到本地计算机的my(个人)区域,然后本地计算的“受信任的根证书颁发机构“要导入客户端证书的CA证书(在客户端证书申请页有下载链接)。
3. 出现“启用服务端SSL必须使用关联私钥的证书“(The server mode SSL must use a certificate with the associated private key.)是因为服务端用的证书是从文件里取的,而不是通过证书存储API取出来的,一般是这个问题。
4. 出现“没有识别提供给安全软件包的凭证“(System.Net.SSPIWrapper.AcquireCredentialsHandle方法)的错误,可能是因为你的机器是虚拟机,或者你试着重启一下试试能否解决,该问题出现后,代码几乎没变,后来就好了。
5. 通过证书申请页面申请的证书一般会安装在当前用户的个人存储区,如果要把这个证书安装到计算机里,可以在证书管理控制台里把用户个人区域里的证书连私钥导出成pfx文件(不是.cer文件),然后在在证书管理控制台(开始-运行-输入mmc.exe,在添加删除管理单元里把“证书“单元添加进来,可以选择当前用户和计算机)里导入到计算机的个人区域里。
6. 可以使用SslStream的异步读写方法来提高性能
7. 从证书存储区获取证书的时候用证书序列号比较容易精确的获取证书,比用subject等要准确
8. SslStream.AuthenticateAsClientd的targetHost参数为客户端验证的服务端证书名称,就是服务器认证证书的“颁发给“字段。这里一定要写对,既不是CA的服务器地址,也不是类似http://ms-onlytiancai/certsrv/的字符串,也不是TcpServer的地址(实际上是TcpServer的netbios名称或dns名称,和申请服务器证书时的姓名字段是一个,必须相同)。如果填写其它字符串,在客户端验证的时候会出RemoteCertificateNameMismatch错误。
https webServices
服务端配置SSL(https)
1. 在IIS的默认网站的属性-目录安全性-安全通信-服务器证书对话框里,选择新建一个证书,按照向导,一路向下,到“站点公用名称列”,在“公用名称”里输入服务器的dns名称或者netbios名称,这里一定要填写对,最后会在硬盘上生成一个cerreq.txt文件的服务器证书申请单。
2. 把上一步生成的申请单传给CA,在CA服务器的“证书颁发机构”管理器上右键点“新建证书申请”,在弹出的对话框里选择cerreq.txt,确定后“挂起的证书”节点会多一个证书申请,右键点“颁发”。颁发后在到“已颁发的证书”节点里找到刚刚颁发的证书,右键点“导出”,对话框里在“包含二进制的列”下拉框里选择“二进制证书”,然后选择“保存证书到一个文件”,最后会在硬盘上生成一个.cer文件(应该是带私钥的)。
3. 在服务器上拷贝一下上一步生成的.cer文件,再在IIS属性的目录安全性-安全通信-服务器证书,弹出一个“欢迎使用web服务器证书向导”对话框,下一步后选择“处理已挂起的证书申请”,下一步中选择拷贝过来的.cer文件,再下一步选443端口。
4. 然后在你的web服务的虚拟目录-目录安全性-安全通信-点编辑按钮,把“要求安全通道(ssl)”的复选框打上,如果需要验证客户端证书的话,在“客户端证书”单选框列表里选择“要求客户端证书”。如果选择了“要求客户端证书”,可以在下面的“启用客户端证书映射”的“编辑”对话框里把证书映射到服务器的指定账户。
5. 如果启用了“要求客户端证书”,服务器要信任客户端证书的CA,具体操作如下。开始-运行,输入mmc.exe。在控制台里点文件-添加/删除管理单元,选择“证书”单元,然后点添加的时候选择“计算机账户-本地计算机”(一定要选对,如果选择“我的用户账户的话,只是当前账户信任某个CA,当前计算机不会信任指定CA的”)。然后在证书控制台里的“受信任的颁发机构”节点上右键点导入,把在证书申请页面上下载的CA证书导入进来。这样这台服务器就新人所有这个CA颁发的客户端证书了。
客户端访问
1、 先为客户端申请一个客户端证书,CA就用服务器计算机信任的CA。
2、 编码,如下,假设cxz计算机上有一个service的服务,有个helloworld方法
using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using CallSslWebService.cxz;
namespace CallSslWebService
{
class Program
{
static void Main(string[] args)
{
try
{
//挂接验证服务端证书的回调
ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback;
cxz.Service service = new Service(); //实例一个web服务
//如果web服务没有启用匿名访问,要声明credentials
ICredentials credentials = new NetworkCredential("administrator", "1234%^@");
service.Credentials = credentials;
//如果服务器要求提供客户端证书,下面代码提供了客户端证书
X509Certificate cer = X509Certificate.CreateFromCertFile("C:\\certs\\client.cer");
service.ClientCertificates.Add(cer);
//调用web服务
Console.WriteLine(service.HelloWorld());
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.Read();
}
public static bool RemoteCertificateValidationCallback(Object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
//如果没有错就表示验证成功
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
return false;
}
}
}
源码下载地址
https://files.cnblogs.com/onlytiancai/ssltest.rar
相关链接:
Securing Stream Data
http://codeidol.com/csharp/csharpckbk2/Security/Securing-Stream-Data/
构建基于windows证书服务的公钥基础结构
http://bbs.51cto.com/thread-426225-1-1.html
Windows 2000 公钥基础结构详解
http://www.cnblogs.com/coldwine/archive/2005/08/31/227071.html
声明:本文代码修改自相关链接的第一篇文章