65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (33投票s)

2002 年 7 月 28 日

CPOL

9分钟阅读

viewsIcon

952951

downloadIcon

32352

适用于 .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 的情况下,还需要做更多的工作。

客户端

  1. 打开证书存储。选择要使用的证书。调用 AcquireCredentialsHandle
  2. 当需要连接时。调用 InitializeSecurityContext。发送返回的数据块。
  3. 通过调用 InitializeSecurityContext,传入接收到的数据块并发送返回的数据块,与远程方进入握手循环,直到返回成功。
  4. 当需要发送数据时,调用 EncryptMessage 并将返回的加密数据块发送给远程方。
  5. 当需要解密接收到的数据时,调用 DecryptMessage 并处理解密的数据块
  6. 完成后,使用 SCHANNEL_SHUTDOWN 调用 ApplyControlToken。调用 InitializeSecurityContext
    并发送返回的数据块。

服务器端

  1. 打开证书存储。选择要使用的证书。调用 AcquireCredentialsHandle
  2. 当您收到来自客户端的新连接时,调用 AcceptSecurityContext。发送返回的数据块。
  3. 通过调用 AcceptSecurityContext,传入接收到的数据块并发送返回的数据块,与远程方进入握手循环,直到返回成功。
  4. 当需要发送数据时,调用 EncryptMessage 并将返回的加密数据块发送给远程方。
  5. 当需要解密接收到的数据时,调用 DecryptMessage 并处理解密的数据块
  6. 完成后,使用 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.libssleay32r.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 件事。

  1. 生成客户端/服务器证书和密钥,并以 .pem 格式保存
  2. 下载 CA 证书

还有其他方法可以获取可用于测试目的的证书。您可以自行探索。

如何获取证书

当您准备部署您的应用程序时,您可以从 http://www.verisign.com/http://www.thawte.com/ 订购真实/可验证的证书

但是,如果您只是想使用 SSL 进行测试/玩耍,您至少还有 3 个其他选择。

  1. 最佳选择是:Microsoft 的免费测试证书颁发机构服务器位于:http://131.107.152.153/

    只需点击“请求证书”。“Web 浏览器证书”。使用“高级证书表单”。填写字段。生成客户端和服务器身份验证证书。除了密钥大小外,其余保持默认,如果需要,可以更改为更大的密钥大小。您也可以在那里为您的 IIS 服务器生成 SSL 测试证书(如果需要)。
  2. 您可以探索 OpenSSL 提供的选项。查看此常见问题解答以获取更多信息
    http://www.openssl.org/docs/HOWTO/certificates.txt

  3. 另一个选项是使用 Windows 2000 Server 版的内置 CA。您可以在 VS.NET MSDN 附带的 ATL7 SecureSOAP 示例的自述文件中找到分步示例。

  4. 还有一个选项是使用 .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。

参考文献

MS SChannel 相关

免责声明:此代码和信息“按原样”提供,不提供任何形式的明示或暗示保证,包括但不限于适销性和/或适用于特定用途的暗示保证。
© . All rights reserved.