支持者
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 的实现具备现代对端所期望的保护措施,并且不包含那些曾导致已知攻击的协议部分。
扩展主密钥。RFC 7627 由客户端提出,并由服务器接受。不支持它的对端依然会被接受。
不支持重新协商。重新协商请求会被以 no_renegotiation 警报拒绝。会话恢复使用会话票据,并且需要扩展主密钥,参见下文的“会话恢复”。
不支持 SHA-1 签名。依照 RFC 9155,SHA-1 签名既不会被发送,也不会被接受。不带 signature_algorithms 扩展的 TLS 1.2 ClientHello 会被拒绝,因为那意味着只能使用 SHA-1。
降级保护。RFC 8446 第 4.1.3 节的降级保护在双方都生效。同时允许 TLS 1.3 的服务器会在每一个 TLS 1.2 应答上做标记,而提议过 TLS 1.3 的客户端会拒绝带有该标记的 TLS 1.2 应答。
恒定时间 CBC。CBC 记录以恒定时间解密,从而堵住 Lucky Thirteen 攻击,RSA 密钥交换则应用 RFC 5246 第 7.4.7.1 节的对策。
证书密钥用途。对于 RSA 密钥交换,服务器证书需要具备 keyEncipherment 密钥用途。ECDHE 套件和 TLS 1.3 则需要 digitalSignature。
仅限 TLS 1.3 的功能。KeyUpdate 和导出密钥材料仅存在于 TLS 1.3 会话中。
会话恢复
该引擎通过无状态的会话票据来恢复会话,在 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,在服务器一侧,这些根证书只用于验证客户端证书。系统根证书的来源因平台而异:
Windows。ROOT 系统存储,即受信任的根证书颁发机构,通过 crypt32.dll 读取。
macOS。Security.framework 的锚点证书,以 /etc/ssl/cert.pem 作为后备。
iOS。系统根证书无法被列出。当没有加载任何根证书时,仅因根证书不受信任而被引擎拒绝的证书链,会交给 Security.framework 结合 SSL 策略和主机名进行评估。引擎自身的密钥用途和主机名检查随后仍然会执行。
Android。/apex/com.android.conscrypt/cacerts(Android 14 及更高版本)或 /system/etc/security/cacerts 中的证书文件,PEM 或 DER 格式。列在 /data/misc/user/0/cacerts-removed 中、被用户禁用的根证书会被跳过,当应用程序能够读取相应目录时,/data/misc/user/0/cacerts-added 中的用户根证书会被加入。
Linux 及其他平台。以下文件中第一个存在且至少包含一个证书的文件:/etc/ssl/certs/ca-certificates.crt、/etc/pki/tls/certs/ca-bundle.crt、/etc/ssl/ca-bundle.pem、/etc/pki/tls/cacert.pem、/etc/ssl/cert.pem 和 /etc/ssl/certs/ca-bundle.crt。
若要自行加载一个证书文件夹,单元 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。
无论 OpenSSL 导出了什么,始终使用 sgc QUIC 引擎。在服务器上,QUICMode 会报告 qmCustom。
该握手携带 QUIC 传输参数,要求使用 ALPN,并把 TLS 警报报告为 QUIC 错误码。服务器证书从 PEM 或 PFX 读取,对端会依据 RootCertFile 或系统根证书进行校验,包括主机名。
会话票据、会话恢复和 0-RTT 的工作方式与在 OpenSSL 上相同。服务器在握手后发送票据,客户端把它与 ALPN 和服务器传输参数一起存储,且每份票据只接受一次 0-RTT。QUIC 票据绝不会跨用到基于 TCP 的 TLS 上。
报文保护同样是原生实现,因此 QUIC 和 HTTP/3 完全不会加载 OpenSSL,无需任何开关。
引擎对比请参阅 QUIC。
它不做什么
该引擎是有意做得狭窄的。下面每一条都是真实的限制,而不是某个可以去寻找的设置。
仅支持 TLS 1.3 和 TLS 1.2。不支持 SSL,也不支持 TLS 1.0 和 TLS 1.1。当 Version 为 tls1_0 或 tls1_1 时,客户端会在连接时引发异常,服务器会在启动时引发异常。既不支持 TLS 1.3 也不支持 TLS 1.2 的对端无法连接。
没有票据就没有 PSK。会话恢复只能通过引擎自己签发的会话票据完成,参阅上文的“会话恢复”。不支持外部的预共享密钥。
基于 TCP 的连接不支持 0-RTT。在基于 TCP 的 TLS 连接上,既不发送也不接受早期数据。握手后的客户端认证同样未实现。
QUIC 通过专门的模式支持。QUIC 和 HTTP/3 使用上文所述的 QUIC 模式,在各自的组件上配置。
默认不使用系统根证书。除非将 NativeTLS_Options.UseSystemRoots 设为 True,信任根只来自 RootCertFile,别无其他来源,因此需要验证证书的客户端必须随软件提供或指向一个根证书文件。对于一份已能正常工作的 OpenSSL 或 SChannel 配置来说,这是最可能被注意到的一处行为差异。
不会触发 OnSSLVerifyPeer。该事件属于 OpenSSL 处理器。原生引擎有自己的验证钩子,它们会一次性收到整条对端证书链以及引擎的判定结果,而不是每一层深度一个证书,因此想要接管信任判定的应用程序必须使用这些钩子,而不是该组件事件。
对端发来的任何内容都不会引发异常。协议失败会以相应的 TLS 警报结束连接,并照常通过组件的错误事件报告原因。
在使用 Delphi 构建的 Windows 版本中,当 CPU 具备 AES-NI 和 PCLMULQDQ 指令集时,AES-GCM 和 AES-CBC 的记录保护会使用这些指令,参阅对称加密中的 sgcAES_HardwareAvailable。若需要一个整个库都不加载 OpenSSL 的构建,请参阅不使用 OpenSSL 运行。