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

一个带 SSL 的工作 TCP 客户端和服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (55投票s)

2015年6月16日

CPOL

28分钟阅读

viewsIcon

309860

downloadIcon

13135

一个使用基于TCP的TLS的Windows客户端和服务器的工作示例。

引言

这是一个项目(技术上是几个项目),旨在演示如何使用Microsoft实现的TLS(称为SCHANNEL)。这是一个使用TLS的多线程服务器和一个可以连接到它的客户端的工作示例。还有一个非常简单的示例客户端,它连接到一个商品Web服务器,只是为了展示最简单的用法,以及几个示例.NET客户端——我不会在这里进一步讨论它们,但如果您需要,示例(名为SimpleClientSimpleClientCsStreamClientCs)是可用的。

对于精通TLS的人来说,请注意此示例支持服务器端和(可选)客户端的SNI和通配符SAN证书,并提供灵活的证书选择和接受。如果您不熟悉TLS的世界,此示例提供了一些代码,旨在易于重用于简单或复杂的客户端和服务器。

背景

传输层安全(TLS)是一种数据可以在两个端点之间安全可靠地移动的方式。使用它,您可以验证连接的另一端是否是其声称的身份,和/或在数据在两个端点之间传输时对其进行加密。TLS的前身是SSL(安全套接字层),但SSL和TLS通常可以互换使用,并且本文的早期版本大多使用SSL,就像代码库中的许多名称一样。

TLS依赖于公钥基础设施,通常这些密钥存储在证书存储中。在Microsoft世界中,常见的存储是注册表的一些加密部分(Java和OpenTLS以不同的方式处理)。有一组用于整个机器的命名存储和一组用于每个用户的命名存储,所有这些都可以使用MMC管理单元查看。

通常,服务器从名为“个人”的机器存储(奇怪的是,在代码中它被称为“我的”)获取其证书(其中包含公钥并指向私钥),客户端从名为“个人”的每个用户存储(同样在代码中是“我的”)获取其证书。客户端通常根本不使用证书,因为客户端证书是可选的,除非服务器要求一个;此示例服务器默认要求一个,但示例客户端可以在必要时创建一个合适的证书。服务器证书标识系统,客户端证书(如果存在)通常标识特定用户。这是最常见的设置,但最低要求仅仅是服务器提供一个证书。有关更多详细信息,请参阅创建数字证书

客户端概述

客户端尝试在您使用命令行参数指定的主机上打开到端口号41000的TCP套接字(名称默认为本地主机的DNS名称)。一旦该套接字打开,客户端就会启动TLS握手(初始数据交换以协商通信选项)并验证服务器提供的证书。服务器请求客户端证书,因此客户端会选择一个如果可能服务器信任的证书,否则它会从用户个人存储中选择一个(有些随意),如果找不到,它会创建一个。客户端是一个控制台应用程序,因此它会在控制台上显示进度。如果您在调试器中运行调试版本,输出窗口中也会有大量详细信息。

服务器概述

服务器在端口号41000上等待传入连接,当有连接到达时,它将该连接的套接字交给一个新启动的线程进行处理(这是对于为每个新套接字执行大量工作的服务器而言的常见模式)。与客户端一样,它是一个控制台应用程序,因此它会在控制台上显示进度。如果您运行调试版本并附加调试器,也会有大量详细输出。

一旦线程启动,它会等待套接字另一端的客户端发起TLS握手(或者直到最终超时)。作为握手的一部分,客户端将使用SNI告诉服务器它正在尝试连接的服务器名称, armed with that information,每个线程将在机器存储中查找一个命名适当的证书(后续请求相同主机名的连接使用相同的证书)。所选证书还必须满足其他要求,例如具有私钥并标记为可用于服务器身份验证。服务器从客户端请求证书,并选择性地在将连接标记为成功之前对其进行验证。

服务器末尾有一段代码,用于自动启动几个客户端实例,一个连接到服务器名称“localhost”,另一个只允许默认连接到本地主机名。这使得测试更容易,并允许您查看证书的选择和/或创建。

构建环境

大部分代码在Visual Studio 2010、2012、2013和2015下编译,但是当我添加SAN和通配符证书匹配时,我使用了更现代的C++构造,需要至少VS2013,并将项目升级到使用VS 2015(版本140)工具链,2018年9月的版本使用VS 2017(版本141工具链),但稍作修改即可使用VS 2015编译。这是一个32位Unicode构建,在该版本中没有ANSI替代方案,尽管传输的样本数据是字节流。我期望它能在Windows Vista以上的所有Windows版本上运行。2019年7月的更新包括可选的64位构建,而8月(2.1.0版)的更新升级到VS2019和版本142工具链(尽管它仍可与2017和141一起使用)。

2020年1月,有人需要将TLS集成到一些现有的ANSI(即多字节)代码中,因此我创建了一个ANSI构建,并取消了客户端对MFC的使用,以使代码在2.1.1版本中具有更广泛的兼容性。SSLClient和SSLServer代码始终是Unicode的,但生成的库和头文件在必要时可以与多字节调用者一起使用。Unicode接口保持不变。

2022年3月,SSLClient和SSLServer项目合并到一个StreamSSL项目中。这在2.1.4版本中发布,SSLClient和SSLServer仍然保留,以供仍在使用它们的人使用。我预计将在2.1.5版本中删除它们。

在2.1.4版本之前,源代码使用了一些ATL实用函数和类,但客户端和服务器都没有整体使用ATL框架。

2.1.4版本使用版本143工具链,因为VS2022是当时最新的版本,但它仍然与141工具链(VS 2017)兼容。

2.1.6版本(跳过了2.1.5)同样使用143工具链和C++14,因此它也应该能够与141工具链一起使用,尽管我尚未测试。

创建数字证书

在生产环境中,您将从受信任的机构(“证书颁发机构”或CA)获取证书。CA将负责确保您有权请求您所要求的证书。例如,如果您要为TLS请求服务器证书,CA将确保您是服务器所在域的所有者。此类证书通常会通过其完全限定域名(例如hostname.companyname.com)来标识服务器。如果一个随机的人请求“microsoft.com”域中的证书,负责任的CA就不应该颁发。尽管偶尔会发生错误,但它们很少见,您通常可以信任合法CA颁发的证书。

每个证书都有许多属性,但对TLS最重要的属性是“主题名称”(证书所针对的实体,例如特定服务器或特定用户)、“密钥用法”和“增强型密钥用法”,描述了证书的预期用途(例如“服务器身份验证”),以及您是拥有证书的私钥还是仅拥有公钥。其他属性,如有效期或颁发者名称,在此示例中并不重要,但在生产环境中会很重要(因为那时您会关心谁颁发了证书以及它是否当前有效)。客户端证书通常标识特定用户,因此它们通常将电子邮件地址作为主题名称。

较新的证书标准允许每个证书具有一个“主题备用名称”(SAN),这允许相同的证书用于名称列表。另一种允许多个名称的方式是“通配符”证书,它允许同一域中的多个服务器受到同一证书的保护。在此示例中,仅支持*.<域名>形式的通配符。

出于测试目的,您不需要CA颁发的证书;您可以使用自己生成的证书(所谓的“自签名证书”)。如果找不到合适的服务器和/或客户端证书,此示例将使用它提供的主题名称(基本上是主机名和用户名)为您创建证书,执行此操作的代码位于*CertHelper.cpp*中的CreateCertificate中。如果您更喜欢提供自己的证书,最简单的方法是要求IIS为您创建一个,或使用PowerShell中的New-SelfSignedCertificate命令创建一个(有关高级示例,请参阅https://blog.davidchristiansen.com/2016/09/howto-create-self-signed-certificates-with-powershell/)。Leon Finker的文章(见下文)描述了我尚未尝试过的其他替代方案。对于客户端,此示例代码将使用它可以找到的任何具有私钥的证书,如果找不到,甚至会创建一个。

客户端详情

客户端应用程序名为StreamClient,其基本流程相当简单

  • 声明一个TCP连接(套接字)
  • 连接它
  • 通过TCP连接协商TLS
  • 发送和接收几个测试消息
  • 停止使用TLS,但保持TCP连接打开
  • 发送几个明文测试消息,它们之间有延迟
  • 关闭TLS连接
  • 发送未加密消息
  • 关闭套接字

如果您只需要一个简单的客户端,并且不太关心TLS或TCP的细节,只需阅读下面的“主程序”,然后跳到服务器详情,如果您也需要一个服务器。

主程序

这实际上是您建立连接所需的全部。它位于*StreamClient.cpp*中,只是声明了一些对象,打开一个TCP连接,并将其传递给一个实现ISocketStream的对象,以便完成连接。代码的本质如下所示

CActiveSock * pActiveSock = new CActiveSock(ShutDownEvent);
CSSLClient * pSSLClient = nullptr;
pActiveSock->SetRecvTimeoutSeconds(30);
pActiveSock->SetSendTimeoutSeconds(60);
bool b = pActiveSock->Connect(HostName.c_str(), Port);
if (b)
   {
   char Msg[100];
   pSSLClient = new CSSLClient(pActiveSock);
   b = SUCCEEDED(pSSLClient->Initialize(HostName));
   if (b)
      {
      cout << "Connected, cert name matches=" << pSSLClient->getServerCertNameMatches()
         << ", cert is trusted=" << pSSLClient->getServerCertTrusted() << endl;
      if (pSSLClient->Send("Hello from client", 17) != 17)
         cout << "Wrong number of characters sent" << endl;
      int len = 0;
      while (0 < (len = pSSLClient->Recv(Msg, sizeof(Msg))))
         cout << "Received " << CStringA(Msg, len) << endl;
      }
   else
      cout << "SSL client initialize failed" << endl;
   ::SetEvent(ShutDownEvent);
   pSSLClient->Close();
   }

请注意getServerCertNameMatchesgetServerCertTrusted方法;它们告诉客户端对证书信任的程度。

如果您编译并运行客户端,它将连接到服务器并在控制台窗口中显示类似以下内容

Connecting to localhost:41000
Socket connected to server, initializing SSL
Optional client certificate requested (without issuer list), no certificate found.
Client certificate required, issuer list is empty, selected name: david.maw@unisys.com
A trusted server certificate called "localhost" was returned with a name match
Connected, cert name matches=1, cert is trusted=1
Sending greeting
Sending second greeting
Listening for message from server
Received 'Hello from server'
Received 'Goodbye from server'
Shutting down SSL
Sending first unencrypted data message
Sleeping before sending second unencrypted data message
Sending second unencrypted data message
Sleeping before sending termination to give the last message time to arrive
Press any key to pause, Q to exit immediately

调试版本也可以更改为显示接收到的服务器证书的详细信息。它看起来像这样

Certificate sample

要使其显示证书,请更改示例客户端以设置g_ShowCertInfo,它将显示信息

if (g_ShowCertInfo && debug && pCertContext)
    ShowCertInfo(pCertContext, L"Client Received Server Certificate");

如果您只需要一个简单的TLS客户端,只需修改此代码以执行您想要的操作,然后停止。如果您对更多控制或其工作原理感兴趣,请继续阅读……

控制证书

您可以做一些事情来更好地控制连接,您可以提供一个CertAcceptable函数来评估服务器提供的证书并在您不喜欢时拒绝它(这将关闭连接)。您还可以提供一个SelectClientCertificate函数,允许您控制发送到服务器的客户端证书(如果有)。使用这些的代码如下所示

pSSLClient->ServerCertAcceptable = CertAcceptable;
pSSLClient->SelectClientCertificate = SelectClientCertificate;

这些示例都包含在示例源代码中,但您不需要使用其中任何一个,如果您使用,您可以分配lambda表达式,或者,像示例一样,定义具有适当参数的函数,然后简单地分配它们。

TCP客户端(CActiveSock)

TCP客户端连接被抽象为名为CActiveSock的类,在*ActiveSock.cpp*中定义并在*ActiveSock.h*中声明。这个类实现了一些简单的函数(例如ConnectDisconnectSendReceive),它们抽象了与WinSock接口的细节,因此主调用者无需处理它们。要使用它,您声明一个CActiveSock对象,可选地在其上设置一些超时,然后要求它连接到特定的端点(主机名和端口号)。

TLS客户端 (CSSLClient)

实现TLS连接的代码位于CSSLClient对象中,其中一些辅助函数位于*SSLHelper.cpp*中。CSSLClient构造函数需要一个已连接的CActiveSock来为其提供通信通道。一旦构造完成,您调用其Initialize方法,一旦成功完成,您就拥有了一个开放通道,可以通过SendRecv进行通信。TCP可能不会发送您请求的整个消息(例如,它可能耗尽缓冲区空间),或者不会在单个消息中传递您发送的字节(消息可能会被拆分或合并),但Send<font color="#111111" face="Segoe UI, Arial, sans-serif"><span style="font-size: 16px;"> </span></font>Recv会根据需要使用多个网络调用来处理。实际上,TLS没有这种特性——它会在单个消息中发送您请求的内容,并在单个消息中准确传递发送的内容。

验证服务器证书

作为TLS协商的一部分,客户端使用一种称为服务器名称指示(SNI)的机制告诉服务器它认为正在连接的服务器名称,这是TLS协议的一个特性(技术上,它提供了一个列表,但列表中只有一个名称)。一旦服务器知道客户端正在寻找哪个名称(可能没有,在这种情况下使用本地主机名),它就可以提供一个具有匹配主题名称的证书。此示例代码默认执行此操作,但如果找不到匹配的证书,它将使用一个具有错误名称的自签名证书(如果存在)。每次建立连接时,代码都会在哈希表中查找是否已为该服务器名称使用过证书(请参阅GetCredHandleFor),如果存在,则将为该连接重用相同的证书上下文。如果以前从未见过该服务器名称,则会从证书存储中选择一个证书并将其添加到哈希表中,以便下次可以使用。

客户端必须对服务器提供的证书进行的重要评估是“它是否有效”(例如,未过期)、“它是否由受信任的CA颁发”以及“主题名称是否与预期匹配”。一旦TLS握手完成,每个CSSLClient对象都实现了两个调用者可以用来回答这些问题的方法:getServerCertTrustedgetServerCertNameMatches。通常,您不会希望使用连接,除非两者都返回true,否则通信通道可能会受到威胁(即,在到达所需端点之前被读取、更改甚至重定向)。

作为替代方案,您可以提供自己的CertAcceptable函数来评估服务器提供的证书,如果您不喜欢它,则拒绝它(这将关闭连接)。如果您决定提供该函数,它将获得一个指向服务器句柄的指针,以及两个布尔值,以指示证书是否受信任以及其主题名称是否与客户端请求的名称匹配。

选择客户端证书

作为TLS协商的一部分,除非设置了特定的注册表项(见下文),否则服务器会向客户端发送可接受的证书颁发者列表。这允许客户端筛选可能的证书列表,以找到一个由服务器可接受的颁发者颁发的证书,然后将该证书提供给服务器。如果找不到可接受的证书,示例将使用它可以找到的任何证书,如果找不到,甚至会创建一个。服务器代码可以很容易地修改为显示从客户端收到的证书(如果收到),要使其显示证书,请在示例服务器中更改ClientCertAcceptable以插入

ShowCertInfo(pCertContext, _T("Server Received Client Certificate"));

(可选)您可以在客户端提供自己的SelectClientCertificate函数,以允许您控制发送到服务器的客户端证书(如果有)。

有趣的是,此函数会被调用两次,一次是在协商的早期,如果您想提供客户端证书,如果您想提供,如果服务器批准,您就完成了。如果需要客户端证书而您未提供,则连接将失败,因此可能会有第二次调用要求提供证书——此调用可能会附带可接受的证书颁发者列表。

注意 - 如果注册表项HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL中的DWORDSendTrustedIssuerList设置为0,则服务器将不会发送“可接受的颁发者”列表。

服务器详情

服务器比客户端复杂得多——TLS部分基本相同,但在允许它接受多个并发TCP连接方面存在更多复杂性。

主程序

就像客户端一样,这实际上是您建立连接所需的全部;它位于*StreamServer.cpp*中,只是声明了一个CListener,然后告诉它开始在特定端口上监听,并定义连接建立后会发生什么。您在lambda表达式中定义它,并且可以使用传递给它的ISocketStream接口指针来通过TLS连接发送或接收消息。简化的示例代码如下(完整的示例稍微复杂一些,因为它处理在正在使用的套接字上关闭TLS)

Listener->Initialize(Port);
Listener->BeginListening([](ISocketStream * const StreamSock){
   // This is the code to be executed each time a socket is opened
   StreamSock->Send("Hello from server", 17);
   int len = StreamSock->Recv(MsgText, sizeof(MsgText) - 1);
   if (len > 0)
      StreamSock->Send("Goodbye from server", 19);
   });
Listener->EndListening();

如果您编译并运行服务器,它将等待客户端连接(它会运行几个以简化测试)并显示此类控制台输出(此示例显示两个客户端使用SNI自动选择的不同服务器证书依次连接)

WARNING: The server is not running as an administrator.
Starting to listen on port 41000, will find certificate for first connection.
Listening for client connections.

Initiating a client instance for testing.

Waiting on StreamClient to localhost
Server certificate requested for localhost, found "localhost"
An untrusted client certificate was returned for "david.maw@unisys.com"
A connection has been made, worker started, sending 'Hello from server'
Received Hello from client
Sending 'Goodbye from server' and listening for client messages
Waited 1 seconds
Received 'Hello again from client'
Recv returned notification that SSL shut down
Received plaintext 'First block of unencrypted data from client'
Waited 4 seconds
Initial receive timed out, retrying
Waited 2 seconds
Received plaintext 'Second block of unencrypted data from client'
Waited 3 seconds
socket shutting down
Exiting worker

Listening for client connections, press enter key to terminate.

Client completed.
Initiating a client instance for testing.

Additional test clients initiated, press enter key to terminate server.

Server certificate requested for usmv-dgm-home, found "SSLStream Testing"
An untrusted client certificate was returned for "david.maw@unisys.com"
A connection has been made, worker started, sending 'Hello from server'
Waited 63 seconds
Received Hello from client
Sending 'Goodbye from server' and listening for client messages
Waited 1 seconds
Received 'Hello again from client'
Recv returned notification that SSL shut down
Received plaintext 'First block of unencrypted data from client'
Waited 4 seconds
Initial receive timed out, retrying
Waited 2 seconds
Received plaintext 'Second block of unencrypted data from client'
Waited 3 seconds
socket shutting down
Exiting worker

Listening for client connections, press enter key to terminate.

调试构建可以轻松修改以显示UI来显示接收到的客户端证书的详细信息(如果存在),要使其显示证书,请更改示例服务器ClientCertAcceptable函数(见下文)以包含

if (pCertContext)
   ShowCertInfo(pCertContext, "Client Certificate Returned");

如果您只需要一个相当简单的服务器,只需修改此代码以执行您需要的功能,并忽略下面的详细信息。如果您想进行更多控制或了解更多信息,请继续阅读……

控制证书

您可以做几件事来更好地控制连接,您可以提供一个ClientCertAcceptable函数来评估客户端提供的证书(如果有),如果您不喜欢它,则拒绝它(这将关闭连接)。您还可以提供一个SelectServerCert函数,允许您控制发送到客户端的服务器证书。使用这些的代码如下所示

Listener->SelectServerCert = SelectServerCert;
Listener->ClientCertAcceptable = ClientCertAcceptable;

这些示例都包含在示例源代码中,但您不需要使用其中任何一个,如果您使用,您可以分配lambda表达式,或者,像示例一样,定义具有适当参数的函数,然后简单地分配它们。

示例中的SelectServerCert函数利用CertFindServerByName来选择匹配的证书,该函数又调用MatchCertHostName来检查证书的SAN或Subject字段,然后调用HostNameMatches来验证证书中的名称是否与请求的主机名匹配。

TCP监听器(CListen)

在服务器中,您通常需要做的第一件事是确定要监听连接的端口,并告诉Winsock开始在该端口上监听。这有点复杂,因为您可能希望监听的协议(通常是IPv4和/或IPv6上的TCP)以及您希望如何处理在等待下一个套接字打开时阻塞线程(示例通过在单独的线程上运行“监听”循环来处理此问题)。

所有这些都由一个实现多线程监听器的CListen类处理。当连接打开时,它可以执行一些简单的事情,例如启动一个线程来处理它,但之后需要告知服务器实际做什么。传递给BeginListening的lambda表达式提供了这些信息,但通常路径中有一个CSSLServer,用于在基本TCP传输之上添加TLS功能。在BeginListening调用中,lambda表达式会得到一个ISocketstream接口指针,它可以用来发送和接收消息,而CSSLServer可以提供该接口的TLS功能版本,而CPassiveSock只能提供未加密的版本。

TCP服务器(CPassiveSock)

TCP服务器连接由一个名为CPassiveSock的类定义,在*PassiveSock.h*中定义并在*PassiveSock.cpp*中声明——这个类实现了一些简单的函数(例如DisconnectSendReceive),它们抽象了与WinSock接口的细节,因此调用者无需处理它们。请注意,没有“连接”;那是因为这个类只处理作为客户端连接的结果而提供的已打开套接字,由CListen对象进行协调。

TLS服务器(CSSLServer)

实现TLS连接的代码位于CSSLServer对象中。其构造函数需要一个ISocketstream来为其提供通信通道。一旦构造完成,您调用其Initialize方法,一旦成功完成,您就拥有了一个开放的通信通道。CSSLServer对象位于*SSLServer.cpp*中,并且相当重量级,因此一些工作被卸载到*SSLHelper.cpp*中的代码。

从简单的TCP服务器到具有TLS功能的TCP服务器的转换由CSSLServer对象处理。CListener获取Winsock accept提供的SOCKET对象,并为其请求一个CSSLServerCSSLServerSOCKET创建一个CPassiveSock,然后将其附加到CSSLServer以创建服务器端TLS套接字。CSSLServer提供一个ISocketStream接口,该接口被传递给最初传递给BeginListening的lambda函数。

ISocketStream接口的存在主要是为了能够在不使用TLS的情况下仍然让Clistener::Work使用相同的接口(因为服务器可能不会根据通道是否加密而改变其行为)。ISocketStream也抽象了实现的细节,因此lambda无法意外或故意弄乱它们。在示例中没有支持完全不运行TLS的逻辑,但如果您想这样做,只需对CListener进行相当简单的更改,完全不使用CSSLServer即可。

SCHANNEL怎么样?

微软SCHANNEL实现的TLS实际接口有点神秘,部分原因在于相同的接口(安全支持提供程序接口-SSPI)用于访问可替换的提供程序,该提供程序可能执行许多不同的操作。因此,您必须做的第一件事就是通过调用InitSecurityInterface获取一个指向SSPI实现的PSecurityFunctionTable对象。一旦完成,大约20个方法就可用了,例如EncryptMessageQuerySecurityContext

SSPI各种函数中的许多参数在SCHANNEL实现中没有意义,必须为NULL

SSPI接口被设计为由C而不是C++使用,这使得它使用起来相当麻烦。它还提供了对缓冲区使用的严格控制,这进一步增加了复杂性。

基本的TLS握手

为了开始一个TLS会话,第一步是在客户端和服务器之间建立一个通信通道(大多数情况下是TCP连接),然后通过该通道执行“TLS握手”(谷歌“SSL握手”或“TLS握手”,有很多很好的解释)。握手由客户端向服务器发送消息发起,服务器回复并提供证书,客户端响应(可能带有自己的证书),协商密码套件,协商SSL/TLS级别,等等。双方都发送完消息后,建立安全连接。这个握手的细节,无论是从客户端还是服务器的角度,都是SCHANNEL实现的。在这个示例中,服务器实现在CSSLServer中,客户端实现在CSSLClient中。

调用者提供什么

SSPI(SCHANNEL)调用者必须提供发送和接收消息的方法、一些内存缓冲区以及一个状态机来处理握手过程,直到SSPI报告握手完成或失败。您可以在CSSLServer::SSPINegotiateLoop中看到这个逻辑,它基本上只是不断调用SSPI的AcceptSecurityContext过程,直到它返回SEC_E_OK。其他返回值表示各种情况,例如需要更多数据、应发送的响应或失败。

一旦握手完成,您就拥有了一个可以用于发送或接收操作的加密连接。Send最终在类似于SSPINegotiate中的循环中调用SSPI EncryptMessage,而Receive以相同的方式调用SSPI DecryptMessage。Leon Finker下面引用的文章对此有一个很好的概述,如果您对更多细节感兴趣,还有一些进一步的参考资料。

控制TLS协议版本

多年来,TLS已经发展出了许多协议版本:SSL 1.0、2.0、3.0,然后是TLS 1.0、1.1、1.2和1.3(这是撰写本文时普遍使用的最新版本)。理想情况下,使用您可以使用的最新版本,但如果可能,请避免使用TLS 1.0之前的任何版本。SSL 3.0及更早版本已以各种方式受到损害。SSL握手将协商客户端和服务器都可以处理的机制(例如最新协议级别或加密算法)。

警告:切换到TLS 1.3并非易事,并且可能需要新版本的软件,截至2024年4月,该软件尚不存在。TLS 1.3要求AcquireCredentialsHandleSCH_CREDENTIALS参数,而不是当前使用的SCHANNEL_CRED(这已在代码的2.1.6版本中完成并与TLS 1.2一起工作)。TLS 1.3还需要在初始握手后处理TLS会话的重新协商。不幸的是,早期的代码完全没有处理这个问题(SEC_I_RENEGOTIATE情况),而2.1.6版本中的实现无法正常工作(这在TLS 1.2及更早版本中无关紧要,因为它很少使用)。理论上,TLS 1.3禁止重新协商,但为了欺骗中间系统,TLS 1.3握手看起来像是TLS 1.2握手,然后是TLS 1.2重新协商。如果我能找到一个使用SSPI的TLS 1.3的工作示例,希望能给我一些线索,告诉我哪里出了问题,然后我会更新代码。

正如我上面提到的,在2.1.6版本(2024年4月)中,代码使用SCH_CREDENTIALS,它允许通过TLS_PARAMETERS成员指定您将不使用的TLS版本,该成员具有一个grbitDisabledProtocols字段,其中包含SP_PROT_TLS1_0之类的值。在早期版本中,SCHANNEL_CRED用于明确指定允许的版本,因此请查找SchannelCred.grbitEnabledProtocols的设置(它将类似于SP_PROT_TLS1_2_CLIENT)。在这两种情况下,如果需要,只需将其更改为接受早期版本(或将其更改为包含TLS 1.3并修复当前实现中的任何问题)。

实现说明

最初,客户端和服务器之间有许多文件是重复的,这些文件往往有所不同,但差异很小,因此在2018年9月,我决定将所有共享文件移动到树的服务器部分,并更新客户端以指向它们,2019年7月,它们再次移动到公共的..\Common\文件夹中。唯一需要注意的是,#include首先在与源文件相同的目录中查找,因此当StreamClient.cpp使用CertRAII.cpp时,它会获取..\Common\CertRAII.cpp,而 THAT 引用了pch.h——它将是..\SteamClient\pch.h。在2022年(2.1.4版本),客户端和服务器被更改为允许在单个可执行文件中共存。

以前需要MFC才能构建解决方案,但截至2022年(2.1.4版本),不再需要。

此实现演示了如何在停止使用SSL后仍将底层连接用于未加密消息。为了演示这一点,它从客户端向服务器发送了一条未加密消息。我从未见过实际的实现以这种方式工作(在活动连接上关闭SSL,打开SSL很常见,关闭SSL很少见)。因此,此代码不支持从服务器向客户端发送未加密消息——如果您有有效的用例,实现起来相对容易,我只是没有,所以请提问。

超时处理可能看起来过于复杂。它基本上设置为,无论TCP可能选择以多少个段来传递消息,单个接收都会在同一时间超时。另请注意,如果接收超时,调用者可以选择重试(示例中显示了这一点)。

源文件和目标文件

所有源文件都在GitHub上,通常将示例的发布版本与最新版本一起存储在https://github.com/david-maw/StreamSSL/releases/latest(早期版本也在那里)中。

未解决的问题

  1. 缓冲区大小都是固定的(请参阅当前设置为16000的MaxMsgSize——当前的SCHANNEL限制是16384);如果它们是可变的,则会更好。
  2. 利用现代C++中的异步支持,将线程逻辑移出此代码并利用系统提供的服务可能会有所帮助。
  3. 当前(2.1.6)实现不支持TLS 1.3。

致谢

这段代码的TLS部分灵感来自于Leon Finker于2003年在CodeProject上发表的一篇文章(在此处),我强烈推荐它作为想要了解更多关于TLS和SCHANNEL的人的起点。它还受到了一个旧的Microsoft SDK示例(我已找不到)的启发,该示例展示了如何调用SCHANNEL。

创建证书的代码基于Alejandro Campos Magencio博客中的示例“How to create a self-signed certificate with CryptoAPI”代码,可在此处找到。

2019年7月和8月,Thomas Hasse提供了许多修复、一个64位构建和一些清理代码,以及重构部分代码的动力。

2022年3月,Jac Goudsmit进行了更改,允许客户端和服务器代码共存,删除了最后的MFC引用,并全面改进了构建。

历史

此代码在GitHub上维护,地址为https://github.com/david-maw/StreamSSL。我通常先在GitHub上更新代码,然后在这个文章中更新,一旦有重大更改或累积了一些更改。所以,如果您想要最新的源代码,请在GitHub上查看https://github.com/david-maw/StreamSSL/releases。以下是主要更改的简要时间表:

  • 2015年6月 原始文章发布
  • 2015年6月27日 - 源文件添加到GitHub
  • 2015年7月6日 - 为客户端和服务器代码提供了对所用证书的更多控制以及拒绝它们的能力
  • 2016年1月 - 添加了代码,在没有客户端证书可用时创建客户端证书,并默认使用主机名而不是“localhost”
  • 2016年2月 - 更新了源代码,使其可以使用VS2015工具链以及VS2010编译
  • 2016年4月9日 - 更新了代码以处理SAN和通配符证书,需要VS 2015 Unicode构建
  • 2016年9月15日 - 修复了一些语法和拼写错误
  • 2018年8月26日 - 新关键词并修复了令人困惑的计时代码(它有效,但纯属偶然)
  • 2018年9月 - 全面采用RAII,大量清理和一个新的证书缓存
  • 2019年6月 - 移除所有w4警告并设置警告为错误,实现正确的SSL关闭。
  • 2019年7月 - 更新和重构
    • 增强示例以展示停止SSL但继续未加密连接。
    • 使用更现代的C++(使用nullptr、const和true/false,消除“(void)”…)
    • 重构CActiveSock和CPassiveSock以继承自commom CBaseSock
    • SendPartial和RecvPartial不再是公共的,Send和Recv取代了它们
    • 预编译头文件机制已更改为与当前做法匹配,使用pch.h
    • 共享文件(cpp和h)已移至共享文件夹
    • 添加64位构建
    • 删除了CWorker类 - 它没有做太多工作,并且使代码更难理解
  • 2019年8月 - 2.1.0版本
    • 将解决方案拆分为2个库项目和2个使用它们的示例(在“Samples”文件夹中)。选择的用户可以分发库和头文件,而无需分发源代码。
    • 对现代C++进行了各种更新,包括转换为VS2019并在构建中使用最新的C++一致性(部分C++20)。
    • 代码库现在支持版本控制,并导出一个GetVersionText方法供调用者使用(示例也使用AppVersion.h中的信息来创建版本资源)。
    • 解决方案中添加了许可证文件,并在readme中引用。
    • 预编译头文件名称已更改为使用新创建项目会使用的名称(pch.h)。
  • 2019年2月 - 2.1.1版本
    • 允许ANSI(即多字节,而非Unicode)调用者,取消MFC的使用。
    • 添加缺失的x64配置。
    • 添加一个最小的示例客户端程序,展示如何用少量代码连接到服务器
  • 2020年10月 - 2.1.2版本
    • 这纠正了使用VS2019 v 16.8或更高版本编译时的语法错误。
    • 请注意,此版本有一个bug,导致其自识别为2.1.1
  • 2021年3月 - 2.1.3版本
    • 与2.1.2相同,但版本设置为2.1.3(而不是错误的2.1.1)
  • 2022年6月 - 2.1.4版本
    • SSLServer和SSLClient现在可以共存于单个代码文件中
    • 两个项目(SSLClient和SSLServer)合并到StreamSSL中
    • 不再需要MFC
  • 2024年4月 - 2.1.6版本(跳过了2.1.5)
    • 更新以支持未来的TLS 1.3实现。
© . All rights reserved.