原生 TLS

支持者

 

TsgcWebSocketClient

TsgcTCPClient

TsgcHTTP1Client

TsgcHTTP2Client

TsgcWebSocketServer

TsgcWebSocketHTTPServer

TsgcHTTPServer

TsgcWebSocketProxyServer

TsgcWebSocketLoadBalancerServer

 

*需要 sgcCrypto 包,该包从 Standard 版开始提供。

 

原生 TLS 引擎是一个完全用 Object Pascal 、基于 sgcCrypto 各单元编写的 TLS 1.3 和 TLS 1.2 实现。它不是一层绑定:没有需要部署的 OpenSSL DLL,不调用 SChannel,也不涉及任何平台 TLS 库。同一份源码可从 Delphi 7 编译到 RAD Studio 13,并在这些组件所支持的每一个平台上运行。

 

选用它的理由在于密钥交换。该引擎提供 RFC 10024 的混合后量子密钥交换组,并且 X25519MLKEM768 是它最先提出的组,因此改用它的客户端只要对方服务器支持后量子密钥交换,就会协商出后量子密钥交换,而应用程序无需做任何其他改动。混合组仅存在于 TLS 1.3 中,因此回退到 TLS 1.2 的连接会使用经典曲线。关于这些组是什么,以及为什么混合方案是当下合理的迁移选择,请参阅后量子密码学。

 

开启它

 

只需设置 IOHandler,其他一切保持不变。您已在使用的证书、密钥、根证书、验证和 ALPN 属性仍然有效,该引擎读取的是同一批属性。

 


// client
oClient := TsgcWebSocketClient.Create(nil);
oClient.URL := 'wss://www.esegece.com:2053';
oClient.TLS := True;
oClient.TLSOptions.IOHandler := iohNativeTLS;
oClient.TLSOptions.VerifyCertificate := True;
oClient.TLSOptions.RootCertFile := 'cacert.pem';
oClient.Active := True;

// server
oServer := TsgcWebSocketServer.Create(nil);
oServer.SSL := True;
oServer.SSLOptions.IOHandler := iohNativeTLS;
oServer.SSLOptions.CertFile := 'certificate.pem';
oServer.SSLOptions.KeyFile := 'key.pem';
oServer.Active := True;

 

在服务器上,该引擎同样能在事件驱动的引擎上运行。在 Windows 上把 IOHandlerOptions.IOHandlerType 设为 iohIOCP,在 Linux 上设为 iohEPOLL,处理器就会基于相应的 TLS 提供程序构建,因此同一套配置在三种引擎上都能工作。

 

在不包含 sgcCrypto 包的构建中选择 iohNativeTLS,会引发一个明确说明原因的异常,而不会静默地回退到另一个库。

 

TLS 版本

 

该引擎在 Indy 客户端、Indy 服务器以及 IOCP 和 EPOLL 服务器引擎上,无论作为客户端还是作为服务器,都能使用 TLS 1.3 和 TLS 1.2。只要双方都支持,就总是选择 TLS 1.3。对于原生引擎,客户端上的 TLSOptions.Version 和服务器上的 SSLOptions.Version 表示允许的最低版本。

 

版本 协商结果
tlsUndefined(默认) TLS 1.3 或 TLS 1.2
tls1_2 TLS 1.3 或 TLS 1.2
tls1_3 仅 TLS 1.3
tls1_0、tls1_1 不会协商出任何版本。由于该引擎只支持 TLS 1.2 和 TLS 1.3,客户端会在连接时引发配置错误,服务器则会在启动时引发。

 

OpenSSL 处理器读取该属性的方式并非如此。在 iohOpenSSL 中,tls1_2 严格表示 TLS 1.2;而在 iohNativeTLS 中,它表示 TLS 1.2 或更高版本。真正决定结果的是密码套件列表。要仅使用 TLS 1.2,请将 Version 保持为 tls1_2,并在 NativeTLS_Options.CipherSuites 中只列出 TLS 1.2 的套件。只列出 TLS 1.3 套件的列表会得到仅 TLS 1.3,同时列出两个版本套件的列表则两者都允许。Version 为 tls1_3 时若配合一份 TLS 1.2 套件列表,就没有版本可供协商,连接会在建立时引发异常。

 


// TLS 1.3 and TLS 1.2, tls1_2 is the lowest version allowed
oClient.TLSOptions.IOHandler := iohNativeTLS;
oClient.TLSOptions.Version := tls1_2;

// TLS 1.3 only
oClient.TLSOptions.Version := tls1_3;

// TLS 1.2 only: name TLS 1.2 suites and no TLS 1.3 suite
oClient.TLSOptions.Version := tls1_2;
oClient.TLSOptions.NativeTLS_Options.CipherSuites :=
  'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256';

 

组与密码套件

 

客户端上的 TLSOptions.NativeTLS_Options 和服务器上的 SSLOptions.NativeTLS_Options 各自携带两个偏好列表。两者都是 OpenSSL 风格的字符串,项目之间以冒号分隔,最偏好的排在最前,并且一份列表会同时用于 TLS 1.3 和 TLS 1.2。将某个列表留空即可保持引擎的默认值。无效的列表会在连接建立时引发异常,因此拼写错误不会被静默忽略。

 

属性 可接受的名称 默认值
Groups X25519MLKEM768、SecP256r1MLKEM768、SecP384r1MLKEM1024、X25519、secp256r1、secp384r1。别名 P-256、prime256v1 和 P-384 同样可以接受,名称比较时不区分大小写。这三个混合组仅适用于 TLS 1.3。 X25519MLKEM768:X25519:secp256r1:secp384r1
CipherSuites TLS 1.3:TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256、TLS_AES_128_GCM_SHA256。TLS 1.2:见下表中的套件。IANA 名称与 ECDHE-RSA-AES128-GCM-SHA256 这样的 OpenSSL 风格名称均可接受,引擎报告时使用 IANA 名称。 TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256,加上以下的 TLS 1.2 默认值

 

列表中的第一个组就是客户端会为之发送密钥共享的那一个,因此把 X25519MLKEM768 留在最前面,正是第一轮报文就已经是后量子的原因。在某一项前面加上问号,可以在当前构建不具备该项时跳过它,而任何其他未知或不可用的项都会让整个列表失败。

 


// only the hybrid post-quantum groups, and only AES-256-GCM
oClient.TLSOptions.NativeTLS_Options.Groups :=
  'X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024';
oClient.TLSOptions.NativeTLS_Options.CipherSuites := 'TLS_AES_256_GCM_SHA384';

// tolerate a build where X25519 is not compiled in
oClient.TLSOptions.NativeTLS_Options.Groups := '?X25519MLKEM768:secp256r1';

 

TLS 1.2 密码套件

 

默认只启用具备前向保密的 AEAD 套件。AES-CBC 套件和 RSA 密钥交换是为那些做不到更好的对端准备的,只有当 CipherSuites 中列出它们时,才会被提供或接受。

 

套件 启用情况
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384、TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384、TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256、TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256、TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256、TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 默认启用,顺序如上。
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA、TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA、TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA、TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA、TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256、TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384、TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256、TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 仅当在 CipherSuites 中列出时才启用。
TLS_RSA_WITH_AES_128_GCM_SHA256、TLS_RSA_WITH_AES_256_GCM_SHA384、TLS_RSA_WITH_AES_128_CBC_SHA、TLS_RSA_WITH_AES_256_CBC_SHA、TLS_RSA_WITH_AES_128_CBC_SHA256、TLS_RSA_WITH_AES_256_CBC_SHA256 仅当在 CipherSuites 中列出时才启用。RSA 密钥交换没有前向保密。

 

TLS 1.2 连接中的 ECDHE 套件使用 Groups 列表中的一个经典组:X25519、secp256r1 或 secp384r1,因此当必须连接 TLS 1.2 对端时,请在列表中保留至少一个这样的组。RSA 密钥交换则完全不使用组。

 


// TLS 1.3, and TLS 1.2 with AES-GCM plus AES-CBC for older clients
oServer.SSLOptions.IOHandler := iohNativeTLS;
oServer.SSLOptions.Version := tls1_2;
oServer.SSLOptions.NativeTLS_Options.CipherSuites :=
  'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:' +
  'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:' +
  'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA';

 

在使用 SGC_CRYPTO_FIPS 构建的版本中,两个基于 X25519 的组、ChaCha20-Poly1305 套件以及 RSA 密钥交换都会被编译排除,默认值也随之变为 SecP256r1MLKEM768:SecP384r1MLKEM1024:secp256r1:secp384r1 和 TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256,TLS 1.2 则使用那四个 ECDHE AES-GCM 套件。请参阅 FIPS 模式。

 

TLS 1.2 行为

 

TLS 1.2 的实现具备现代对端所期望的保护措施,并且不包含那些曾导致已知攻击的协议部分。

 

 

会话恢复

 

该引擎通过无状态的会话票据来恢复会话,在 TLS 1.3(RFC 8446)和 TLS 1.2(RFC 5077)中均可使用。完整握手结束后,服务器会发送一份票据,该票据用只有服务器掌握的密钥以 AES-256-GCM 密封。客户端把它保存在会话缓存中,以主机、端口和 ALPN 列表为键,并在下一次连接时提供该票据,从而跳过证书交换。服务器无法打开的票据,或者已经过期的票据,会导致一次带有新票据的完整握手。

 

属性 默认值 含义
NativeTLS_Options.SessionTickets True 服务器签发票据,客户端存储并提供票据。设为 False 会关闭会话恢复,此时每一次握手都是完整握手。
NativeTLS_Options.TicketLifetime 7200 仅用于服务器。票据可用于恢复的秒数,最大为 604800。服务器票据密钥每隔 TicketLifetime 秒轮换一次,旧密钥在下一个周期内仍然有效。

 

这些选项在客户端上是 TLSOptions.NativeTLS_Options,在服务器上是 SSLOptions.NativeTLS_Options。会话缓存属于每一个客户端组件,因此同一个 TsgcWebSocketClient、TsgcHTTP1Client、TsgcTCPClient 或 TsgcLDAPClient 的重新连接会被恢复,TsgcWebSocketProxyServer 的上游连接也是如此。

 

TLS 1.2 的会话恢复需要 RFC 7627 的扩展主密钥。没有它的 TLS 1.2 会话永远不会被恢复,也不会获得票据;当服务器要求客户端证书时,同样不会签发票据。TLS 1.3 的票据永远不会以 TLS 1.2 会话的形式打开,反之亦然。

 

连接是否被恢复,由下文所述处理器 TsgcIdSSLIOHandlerSocketNativeTLS 的 Resumed 属性报告。

 

协商结果

 

握手完成后,单元 sgcSSL_NativeTLS_Indy 中的客户端处理器 TsgcIdSSLIOHandlerSocketNativeTLS 会报告结果。VersionName 是 TLSv1.2 或 TLSv1.3,CipherSuiteName 是该套件的 IANA 名称,GroupName 是密钥交换所用的组,RSA 密钥交换时为空,PeerSignatureSchemeName 是服务器所使用的签名方案。该处理器就是客户端传递给 OnSSLAfterCreateHandler 的那一个。

 


procedure TForm1.oClientSSLAfterCreateHandler(Sender: TObject;
  aType: TwsSSLHandler; aSSLHandler: TIdSSLIOHandlerSocketBase);
begin
  FHandler := aSSLHandler;
end;

procedure TForm1.oClientConnect(Connection: TsgcWSConnection);
var
  oHandler: TsgcIdSSLIOHandlerSocketNativeTLS;
begin
  if FHandler is TsgcIdSSLIOHandlerSocketNativeTLS then
  begin
    oHandler := TsgcIdSSLIOHandlerSocketNativeTLS(FHandler);
    Memo1.Lines.Add(oHandler.VersionName + ' ' + oHandler.CipherSuiteName +
      ' ' + oHandler.GroupName + ' ' + oHandler.PeerSignatureSchemeName);
  end;
end;

 

证书与验证

 

服务器读取 CertFile、KeyFile 和 Password。CertFile 和 KeyFile 可以是 PEM 格式,CertFile 也可以是一个 .pfx 或 .p12 文件,根据扩展名识别。PKCS#12 文件中包含证书、证书链和私钥,因此不需要 KeyFile,该文件以 Password 打开,由 sgcCrypto_PKCS12 中纯 Pascal 实现的 PKCS#12 读取器读取,不涉及 OpenSSL。将 VerifyCertificate 设为 True 的客户端,会拿服务器证书链与下文所述的受信任根进行校验,并检查名称是否与它所连接的主机相符。VerifyDepth 会被传递下去,超过 16 的值会被限定为 16。

 

受信任的根是 RootCertFile 中的证书。将 NativeTLS_Options.UseSystemRoots 设为 True 会额外加入操作系统信任的证书颁发机构,默认值为 False。两者可以合并使用:先加载 RootCertFile 中的根证书,再把系统根证书加入同一个证书存储,只要证书链能追溯到其中任意一个,就会被接受。当找不到任何系统根证书时,不会添加任何内容,也不会引发错误,此时验证仅依赖 RootCertFile。该属性在客户端上是 TLSOptions.NativeTLS_Options.UseSystemRoots,在服务器上是 SSLOptions.NativeTLS_Options.UseSystemRoots,在服务器一侧,这些根证书只用于验证客户端证书。系统根证书的来源因平台而异:

 

若要自行加载一个证书文件夹,单元 sgcSSL_NativeTLS_Certificates 中的 TsgcTLS13TrustStore.AddDirectory(aDirectory, aRemovedDirectory) 会读取 aDirectory 中的每一个 PEM 或 DER 文件,跳过文件名同时也出现在 aRemovedDirectory 中的文件,并返回它添加的证书数量。它在每个平台上都可用。

 

若要固定证书必须携带的名称,而不是使用连接主机,请设置 TLSOptions.OpenSSL_Options.X509Checks.HostName 或 IPAddress,并在 X509Checks.Mode 中设置相应的标志。尽管该引擎并非 OpenSSL,它依然会读取这两个值,因此无论选择哪一种 IOHandler,该属性的含义都是一样的。

 

在服务器上,SSLOptions.VerifyCertificate 会向客户端索取证书,并拿它与上文所述的受信任根进行校验,SSLOptions.VerifyCertificate_Options.FailIfNoCertificate 则拒绝不发送证书的客户端。VerifyClientOnce 在这里无事可做。TLS 1.3 没有重新协商,该引擎在 TLS 1.2 中也拒绝重新协商,因此绝不会再次索取。

 

ALPN 照常通过客户端的 ALPNProtocols 和服务器端的 ALPN 选择工作,因此在原生引擎上跑 HTTP/2 时,协商 h2 的方式与在 OpenSSL 上完全相同。请参阅 ALPN。

 

QUIC 模式

 

该引擎同样能运行 QUIC 和 HTTP/3(RFC 9001)的 TLS 1.3 握手。将 TsgcQUICClient 上的 SSLQUICOptions.IOHandler、TsgcHTTP3Client 上的 TLSOptions.IOHandler、TsgcQUICServer 和 TsgcHTTP3Server 上的 QUICOptions.SSLOptions.IOHandler,或者在启用了 HTTP3Options.Enabled 的服务器上的 SSLOptions.IOHandler,设为 iohNativeTLS。

 

 

引擎对比请参阅 QUIC。

 

它不做什么

 

该引擎是有意做得狭窄的。下面每一条都是真实的限制,而不是某个可以去寻找的设置。

 

 

对端发来的任何内容都不会引发异常。协议失败会以相应的 TLS 警报结束连接,并照常通过组件的错误事件报告原因。

 

在使用 Delphi 构建的 Windows 版本中,当 CPU 具备 AES-NI 和 PCLMULQDQ 指令集时,AES-GCM 和 AES-CBC 的记录保护会使用这些指令,参阅对称加密中的 sgcAES_HardwareAvailable。若需要一个整个库都不加载 OpenSSL 的构建,请参阅不使用 OpenSSL 运行。