适用于 .NET 的 SSL/TLS 客户端/服务器和 SSL 隧道






4.83/5 (33投票s)
适用于 .NET 的 SSL/TLS 客户端/服务器类,使用 Windows SSPI 实现 SSL/TLS 协议
使用 MS SSPI SSL 的示例
使用 OpenSSL 的示例
在 Windows 2000 上测试。目录
SSPI 概述和步骤
SSPI 代表安全支持提供程序接口。它是 Windows 提供的安全服务之上的一个抽象层。SSL/TLS 本身是在安全通道安全提供程序中实现的,SSPI 为我们抽象了它。SSPI 通过获取和返回要发送给远程方的数据块来工作。这样,它使我们在选择使用什么协议(例如,tcp/ip)以及如何处理加密/解密数据方面具有最大的灵活性。需要注意的一件事是确保数据是流式传输的,并且没有任何内容乱序。SSPI 提供了许多内置协议:Kerberos、NTLM、SSL/TLS。它们之间几乎有完全相同的函数调用序列。在 SSL/TLS 的情况下,还需要做更多的工作。
客户端
- 打开证书存储。选择要使用的证书。调用
AcquireCredentialsHandle
。
- 当需要连接时。调用
InitializeSecurityContext
。发送返回的数据块。
- 通过调用
InitializeSecurityContext
,传入接收到的数据块并发送返回的数据块,与远程方进入握手循环,直到返回成功。
- 当需要发送数据时,调用
EncryptMessage
并将返回的加密数据块发送给远程方。
- 当需要解密接收到的数据时,调用
DecryptMessage
并处理解密的数据块
- 完成后,使用
SCHANNEL_SHUTDOWN
调用ApplyControlToken
。调用InitializeSecurityContext
并发送返回的数据块。
服务器端
- 打开证书存储。选择要使用的证书。调用
AcquireCredentialsHandle
。
- 当您收到来自客户端的新连接时,调用
AcceptSecurityContext
。发送返回的数据块。
- 通过调用
AcceptSecurityContext
,传入接收到的数据块并发送返回的数据块,与远程方进入握手循环,直到返回成功。
- 当需要发送数据时,调用
EncryptMessage
并将返回的加密数据块发送给远程方。
- 当需要解密接收到的数据时,调用
DecryptMessage
并处理解密的数据块
- 完成后,使用
SCHANNEL_SHUTDOWN
调用ApplyControlToken
。调用AcceptSecurityContext
并发送返回的数据块。
这些基本上是使用 SSPI 进行 SSL/TLS 所必须采取的步骤。在代码中还有一些上面未列出的细节需要处理。例如,适当响应 SSPI 函数返回的任何错误代码。例如,重新协商、断开连接、读取不完整消息/数据过多(即一个完整消息和来自下一个消息的额外数据)。
OpenSSL
OpenSSL 是一个开源项目,用于实现商业级、功能齐全的 SSL/TLS。您可以在以下位置了解更多信息: ttp://www.openssl.org/ (文档: http://www.openssl.org/docs, 源代码: http://www.openssl.org/source)
首先,让我们编译 OpenSSL 库。下载最新的发行版,例如 openssl-0.9.7b.tar.gz
- 解压文件。阅读 OpenSSL 根目录中的
INSTALL.W32
文件。 - 打开“
Visual Studio .NET 命令提示符
”窗口。 - 进入 openssl-0.9.7b 目录。
- 在 OpenSSL 根目录中键入“
perl Configure VC-WIN32
”。(如果您没有,请为 Windows 安装ActivePerl
) - 在 OpenSSL 根目录中键入“
ms\do_ms
”。 - 现在键入“
nmake -f ms\nt.mak
”(这适用于 OpenSSL 库的静态版本)
所有内容都应该顺利构建,并且 .libs 应该输出到 openssl-0.9.7b\out32
目录。
- 在“
VC++ 目录
”选项中为“库文件
”设置此目录。 - 在“
VC++ 目录
”选项中为“包含文件
”设置openssl-0.9.7b\inc32\
目录。 - 将生成的库重命名为
libeay32r.lib
和ssleay32r.lib
以用于发布模式。为调试模式单独构建一个 - 使用 /MDd。它们不可互换。
此时,托管 C++ 项目应该可以顺利编译。
- 阅读 OpenSSL 常见问题以了解如何使用 openssl.exe 创建证书,或者您可以搜索 Google 以获取更多信息和示例。例如,如果您使用 MS CA,您可以从 http://131.107.152.153/ 下载 CA 证书,将其保存到 CA.cer 文件中,并在示例代码中用作 CA 文件参数。
现在,让我们向此 CA 发出证书请求。
- 首先,按照上述常见问题生成密钥:“
openssl genrsa -des3 -out privkey.pem 2048
” - 现在生成请求“
openssl req -new -key privkey.pem -out cert.csr
” - 使用 MS CA 网站上的“
高级证书请求
”表单,选择“通过使用 base-64 编码的 CMC 或 PKCS #10 文件提交证书请求,或通过使用 base-64 编码的 PKCS #7 文件提交续订请求。”链接。告诉它在哪里找到 .csr 文件并选择提交。现在下载生成的证书。 - 将其转换为 .pem:“
openssl x509 -inform DER -in certnew.cer -out clientcert.pem
” - 将证书和私钥合并到一个文件中。“
type clientcert.pem privkey.pem >client.pem
”
现在您可以使用此 client.pem 用于客户端。对服务器端执行相同的步骤。这只是一个示例。
探索更适合您的选项。
因此,要使示例项目与 OpenSSL 一起工作,您至少需要 2 件事。
- 生成客户端/服务器证书和密钥,并以 .pem 格式保存
- 下载 CA 证书
还有其他方法可以获取可用于测试目的的证书。您可以自行探索。
- SSL 证书 HOWTO (tldp.org)
- OpenSSL 证书手册 (pseudonym.org)
- 创建和使用 SSL 证书 (binarytool.com)
如何获取证书
当您准备部署您的应用程序时,您可以从 http://www.verisign.com/ 或 http://www.thawte.com/ 订购真实/可验证的证书
但是,如果您只是想使用 SSL 进行测试/玩耍,您至少还有 3 个其他选择。
- 最佳选择是:Microsoft 的免费测试证书颁发机构服务器位于:http://131.107.152.153/
只需点击“请求证书”。“Web 浏览器证书”。使用“高级证书表单”。填写字段。生成客户端和服务器身份验证证书。除了密钥大小外,其余保持默认,如果需要,可以更改为更大的密钥大小。您也可以在那里为您的 IIS 服务器生成 SSL 测试证书(如果需要)。
- 您可以探索 OpenSSL 提供的选项。查看此常见问题解答以获取更多信息
http://www.openssl.org/docs/HOWTO/certificates.txt
- 另一个选项是使用 Windows 2000 Server 版的内置 CA。您可以在 VS.NET MSDN 附带的 ATL7 SecureSOAP 示例的自述文件中找到分步示例。
- 还有一个选项是使用 .NET SDK/PSDK 附带的 makecert.exe。当前 PSDK 附带的新版本支持可导出私钥(-pe 选项)。
要找出证书的哈希/指纹,请在资源管理器中打开 .cer 文件。转到“详细信息”|“字段”|“指纹”。选择它。然后只需将字节值复制粘贴到您的代码中。
查看已安装的证书
要在 NT 上查看您当前的证书:运行 mmc.exe。转到“控制台”|“添加/删除管理单元”|“添加”|“证书”。您的个人或“MY”存储将在“个人”|“证书”树项下。
客户端类
class SSLConnection
{
public:
//initiate connection, given server's ip and client's certificate
//hash, data to be sent will be returned in WriteSSL callback
void InitiateHandShake(String* ipAddress, Byte thumbPrint[],
Common::Misc::SecurityProviderProtocol prot,
Object* state);
//encrypt data, encrypted data is retuned in WriteSSL callback
void EncryptSend(Byte data[], int ActualLen, Object* state);
//decrypt data, decrypted data is returned in PlainData callback
void DecryptData(Byte data[], Int32 ActualSize, Object* state);
//disconnect from server
bool Disconnect(Object* state);
//clean up
void Dispose();
//load new client's credentials from NewCertificate callback
void LoadNewClientCredentials(Byte sha1hash[]);
public:
//maximum data chunk to use for send/recv at a time
__property int get_MaxDataChunkSize();
//recommended initial chunk size when negotiation just starts,
//this is max auth token size
__property int get_MaxInitialChunkSize();
public:
//callback for encrypted data
WriteSSL* DoWrite;
//callback for decrypted data
PlainData* DoPlainData;
//optional
NewCertificate* DoRenegotiate;
//optional
VerifyServCert* DoServerCertVerify;
//optional
HandShakeSuccess* DoHandShakeSuccess;
...
};
快速示例
Socket sock = ...//init Socket;
SSLConnection conn = new SSLConnection();
conn.DoWrite = new SSL.Client.WriteSSL(Send);
conn.DoPlainData = new SSL.Client.PlainData(OnPlainData);
conn.DoRenegotiate = new SSL.Client.NewCertificate(Renegotiate);
conn.DoServerCertVerify = new SSL.Client.VerifyServCert(ServerCertVerify);
conn.DoHandShakeSuccess = new SSL.Client.HandShakeSuccess(HandShakeSuccess);
sock.Connect(....);
//try not supplying client certificate, null for thumbprint
conn.InitiateHandShake(url, null,
SSL.Common.Misc.SecurityProviderProtocol.PROT_TLS1,
Guid.Empty);
while(!connected)
{
// recv data
// call conn.DecryptData
}
string Request = "GET / HTTP/1.1\r\n" + "Host: localhost\r\n\r\n";
byte[] data = ASCIIEncoding.ASCII.GetBytes(Request);
conn.EncryptSend(data, data.Length, null);
//recv
//conn.DecryptData
conn.Disconnect(null);
conn.Dispose();
////////////////////////////////////////////////////////////////
bool Send(byte[] data, object state)
{
sock.Send(data, 0, data.Length, SocketFlags.None);
}
void OnPlainData(Byte[] data, object state)
{
//do something with decrypted data
}
void Renegotiate(SSL.Client.SSLConnection conn)
{
//server asked for client certificate
//TODO: change values to match your client's certificates hash
byte[] ThumbPrint = new Byte[] {0xBE,0xD7,0x44,0xF4,0x57,
0x54,0xF2,0x08,0x7F,0x06,
0x03,0x0B,0x01,0x33,0xE5,
0x60,0x78,0x3D,0xAF,0x35};
conn.LoadNewClientCredentials(ThumbPrint);
}
void HandShakeSuccess(){connected = true;}
void ServerCertVerify(SSL.Common.Misc.CeriticateInfo ServCertInfo)
{
X509Certificate cert = new X509Certificate(ServCertInfo.CertData);
//display server's certificate information
}
服务器类
class SSLServer
{
public:
//disconnect from given client, based on id
void DisconnectFromClient(Guid ClientID, Object* state);
//clean up
void Dispose();
//encrypt data, encrypted data is retuned in WriteSSL callback
void EncryptSend(Byte data[], int ActualLen, Guid ClientID,
Object* state);
//decrypt data, decrypted data is returned in PlainData callback
void DecryptData(Byte data[], Int32 ActualLen, Guid ClientID,
Object* state);
//remove client based on id from internal list of clients
void RemoveClient(Guid ClientID);
//maximum data chunk to use for send/recv at a time
int MaxDataChunkSize(Guid ClientID);
//setup credentials, certThumbPrint is certificate's hash
void SetupCredentials(Byte certThumbPrint[],
Common::Misc::SecurityProviderProtocol prot);
//ask client to renegotiate and possibly get different credentials
void AskForRenegotiate(Guid ClientID, Object* state);
public:
//recommended initial chunk size when negotiation just starts,
// this is max auth token size
__property int get_MaxInitialChunkSize();
//ask client to provide certificate or not
__property void set_AskClientForAuth(bool value);
public:
//callback for encrypted data
WriteSSL* DoWrite;
//callback for decrypted data
PlainData* DoPlainData;
//optional
VerifyClientCert* DoClientCertVerify;
//optional
HandShakeSuccess* DoHandShakeSuccess;
private:
...
};
快速示例
SSLServer SSL = new SSLServer();
SSL.DoPlainData = new SSL.Server.PlainData(OnPlainData);
SSL.DoWrite = new SSL.Server.WriteSSL(OnSend);
SSL.DoClientCertVerify
= new SSL.Server.VerifyClientCert(OnClientCertInfo);
SSL.DoHandShakeSuccess
= new SSL.Server.HandShakeSuccess(OnClientHandShakeSuccess);
SSL.AskClientForAuth = false;
//TODO: change values to match your server's certificates hash
byte[] ServerCertThumbPrint = new byte[]{0xA4, 0x01, 0xC2, 0xB1, 0x73,
0xD9, 0xD9, 0xF4, 0x77, 0x68,
0x60, 0xE5, 0xAD, 0x25, 0x34,
0xEE, 0x59, 0xEB, 0x0E, 0x8D};
SSL.SetupCredentials(ServerCertThumbPrint,
SSL.Common.Misc.SecurityProviderProtocol.PROT_TLS1);
//listen for incoming connections on Socket
//Accept new connection if any
//begin receiving data from client
Guid clientID = Guid.NewGuid();
SSL.DecryptData(buff, bytesRead, clientID, null);
SSL.DisconnectFromClient(clientID);
//When client handshake complete,
//do SSL.EncryptSend(data, data.Length, ClientID, null); when needed
SSL.Dispose();
////////////////////////////////////////////////////////////////
void OnPlainData(byte[] data, Guid ClientID, object o)
{
//process data
}
bool OnSend(byte[] data, Guid ClientID, object o)
{
//send data out on socket
}
void OnClientCertInfo(SSL.Common.Misc.CeriticateInfo clientCert)
{
X509Certificate cert = new X509Certificate(clientCert.CertData);
//display client's cert info
}
void OnClientHandShakeSuccess(Guid ClientID)
{
// handshake with client is successful
}
SSL 隧道示例
注意:切勿在您的真实/生产 SQL Server 上尝试此操作。在您自己的风险下,使用您的测试数据库安装或其他通过已知 TCP/IP 连接进行的操作进行尝试。另外,仔细阅读代码,了解其功能,确保它符合您的特定目的。
要使用它,只需构建客户端和服务器 .exe 文件。确保 .config 文件与 .exe 文件位于同一位置。您可以通过使用 SSL/TLS 保护 MS SQL 连接来测试它的工作方式。以下是操作步骤:确保您已安装自己的证书并更新了代码中的哈希/指纹。
打开 MS SQL 客户端网络实用程序。转到“别名”|“添加”,然后按如下设置选项
- 服务器别名:SSL
- 网络库:TCP/IP
- 服务器名称:您的 Client.exe 的 IP(例如,“localhost”)
- 端口号:您的客户端监听端口(即 Client.exe.config 中的“PortToListenForRedirect”值)
在 Client.exe.config 中,“SSLDestIP”和“SSLDestPort”将是您的 Server.exe 的 IP 和它监听重定向的端口。
在 Server.exe.config 中,“PortToListenForSSL”是 Server.exe 监听 Client.exe 的端口,“DestIP”和“DestPort”是 MS SQL Server 的真实 IP 和端口,客户端的所有数据都将发送到此处。因此,客户端和 exe 之间的连接将是安全的。例如,Client.exe 将在本地机器上运行,而 Server.exe 将在靠近 SQL Server 的机器上运行。
运行 SSL 客户端/服务器应用程序。
打开 MS SQL 企业管理器控制台。选择“新建 SQL Server 注册”。对于服务器名称,输入 SSL,填写登录信息。单击“确定”。现在,您应该通过 SSL 连接到您的 SQL Server。
参考文献
- SSL 简介(iplanet.com)
- SSL 和 TLS - 设计和构建安全系统 (amazon.com)
- OpenSSL (openssl.org)
- OpenSSL 示例 (rtfm.com)
- 安全支持提供程序接口 (microsoft.com)
- Microsoft Windows 2000 服务器端应用程序编程(第 12 章,安全连接)(amazon.com)
- 使用 Microsoft 的 SSPI 进行 SSL 加密编程 (byte.com)
- PSDK 附带的 SSL Webclient/WebServer 示例。默认情况下,当前 SDK 将其安装到
X:\Program Files\Microsoft SDK\Samples\Security\SSPI\SSL - VS.NET (ATL7) 附带的另一个示例,SecureSOAP 示例:实现安全 SOAP 通信 (HTTPS)。它基本上与 PSDK 示例的代码库相同。
- Microsoft Certificate Services -- SECTESTCA1 (Microsoft) (您可以获取免费的测试证书进行试用)
- 重新审视安全支持提供程序接口 (msdnmag)
- SSL 证书 HOWTO (tldp.org)
- OpenSSL 证书手册 (pseudonym.org)
- 创建和使用 SSL 证书 (binarytool.com)
MS SChannel 相关
- 关于安全通道 (Schannel) (MSDN)
- 使用 Schannel 创建安全连接 (MSDN)
- 安全通道 (Schannel) (MSDN)
- Schannel 示例 - 获取凭据(MSDN)
- Schannel 示例 - 获取证书 (MSDN)
- 信息:Windows 95 和 Windows 98 上 SSL/SSPI 的加密/解密支持 (Q276245)
- 信息:Windows NT 4.0 上 SSL/SSPI 的加密/解密支持 (Q275592)
- SSL 握手期间服务器身份验证过程的描述 (MSDN)
- SSL 握手期间客户端身份验证过程的描述 (MSDN)
- 安全套接字层 (SSL) 握手的描述 (MSDN)
- 操作方法:配置安全套接字层服务器和客户端缓存元素 (MSDN)
- 操作方法:使用 Microsoft 管理控制台为 SQL Server 2000 启用 SSL 加密 (MSDN)