TLS/SSL concepts

TLS/SSL 是一组依赖于公钥基础设施 (PKI) 的协议,用于 在客户端和服务器之间启用安全通信。对于大多数常见 情况,每个服务器都必须具有私钥。

私钥可以通过多种方式生成。以下示例说明了 使用 OpenSSL 命令行界面生成 2048 位 RSA 私钥: 使用 TLS/SSL,所有服务器(以及某些客户端)都必须具有证书。证书是与私钥对应的公钥,并且由证书颁发机构或私钥的所有者对其进行数字签名(此类证书称为“自签名”)。获取证书的第一步是创建证书签名请求 (CSR) 文件。

openssl genrsa -out ryans-key.pem 2048

使用 TLS/SSL,所有服务器(和一些客户机)必须具有证书。证书是与私钥对应的公钥,由证书颁发机构或私钥所有者(这种证书称为“自签名”)进行数字签名。获取证书的第一步是创建证书签名请求(CSR)文件。

OpenSSL 命令行界面可用于为私钥生成 CSR:

openssl req -new -sha256 -key ryans-key.pem -out ryans-csr.pem 

生成 CSR 文件后,可以将其发送给证书颁发机构进行签名,或用于生成自签名证书。

以下示例演示了如何使用 OpenSSL 命令行界面创建自签名证书:

openssl x509 -req -in ryans-csr.pem -signkey ryans-key.pem -out ryans-cert.pem 

生成证书后,可将其用于生成 .pfx 或 .p12 文件:

openssl pkcs12 -export -in ryans-cert.pem -inkey ryans-key.pem \
      -certfile ca-cert.pem -out ryans.pfx 

其中:

  • in: 签名证书
  • inkey: 关联的私钥
  • certfile: 将所有证书颁发机构 (CA) 证书连接到单个文件,例如 cat ca1-cert.pem ca2-cert.pem > ca-cert.pem

完美前向保密Perfect forward secrecy

术语前向保密或完美前向保密描述了密钥协商(即密钥交换)方法的一个特性。也就是说,服务器和客户端密钥用于协商新的临时密钥,这些密钥专门且仅用于当前通信会话。实际上,这意味着即使服务器的私钥遭到破坏,通信也只有在攻击者设法获得专门为该会话生成的密钥对时才能被窃听者解密。

通过在每次 TLS/SSL 握手时随机生成密钥对来实现完美前向保密(与对所有会话使用相同的密钥相反)。实现此技术的这些方法称为“临时”( “ephemeral”)。

目前,通常使用两种方法来实现完美前向保密(请注意传统缩写后追加的字符“E”):

  • ECDHE: An ephemeral version of the Elliptic Curve Diffie-Hellman key-agreement protocol.(椭圆曲线漫射-赫尔曼密钥协商协议的临时版本。)
  • DHE: An ephemeral version of the Diffie-Hellman key-agreement protocol.(区分-赫尔曼密钥协商协议的临时版本。)

默认情况下启用使用 ECDHE 的完美前向保密。在创建 TLS 服务器时,可以使用 ecdhCurve 选项来自定义要使用的支持的 ECDH 曲线列表。有关更多信息,请参阅 tls.createServer() 。

默认情况下 DHE 处于禁用状态,但可以通过将 dhparam 选项设置为 ‘auto’ 来启用它,同时启用 ECDHE。自定义 DHE 参数也受支持,但建议使用自动选择的知名参数。

在 TLSv1.2 中,完美前向保密是可选的。从 TLSv1.3 开始,始终使用 (EC)DHE(PSK 专用连接除外)。

ALPN 和 SNI

ALPN(应用程序层协议协商扩展)和 SNI(服务器名称指示)是 TLS 握手扩展:

  • ALPN:允许将一个 TLS 服务器用于多个协议(HTTP、HTTP/2)

  • SNI:允许使用一个 TLS 服务器,为具有不同证书的多个主机名提供服务。

Pre-shared keys预共享密钥

可以使用 TLS-PSK 支持作为普通基于证书的身份验证的替代方案。它使用预共享密钥而不是证书来验证 TLS 连接,从而提供相互验证。TLS-PSK 和公钥基础设施并不相互排斥。客户端和服务器可以同时容纳这两者,在正常的密码协商步骤中选择其中一个。

PSK 只是一个很好的选择,因为存在与每台连接机器安全共享密钥的方法,所以它不能替代大多数 TLS 使用的公钥基础设施(PKI)。近年来,OpenSSL 中的 TLS-PSK 实现出现了许多安全缺陷,主要是因为只有少数应用程序使用它。在切换到 PSK 密码之前,请考虑所有可供选择的解决方案。在生成 PSK 时,使用 RFC 4086中讨论的足够的熵是至关重要的。从密码或其他低熵源获取共享秘密是不安全的。

默认情况下,PSK 密码已禁用,因此使用 TLS-PSK 需要使用 ciphers 选项明确指定密码套件。可通过 openssl ciphers -v ‘PSK’ 获取可用密码列表。所有 TLS 1.3 密码均适用于 PSK,可通过 openssl ciphers -v -s -tls1_3 -psk 获取。

根据 RFC 4279,必须支持长度最长为 128 字节的 PSK 标识和长度最长为 64 字节的 PSK。自 OpenSSL 1.1.0 起,最大标识大小为 128 字节,最大 PSK 长度为 256 字节。

由于底层 OpenSSL API 的限制,当前实现不支持异步 PSK 回调。

客户端发起的重新协商攻击缓解

TLS 协议允许客户端重新协商 TLS会话的某些方面。遗憾的是,会话重新协商需要不成比例的服务器端资源,使其成为拒绝服务攻击的潜在媒介。

为了降低风险,重新协商的次数限制在每十分钟三次。当超过这个阈值时,tls.TLSSocket 实例会发出一个“ error”事件。这些限制是可配置的:

  • tls.CLIENT_RENEG_LIMIT 指定重新协商请求的数量。Default: 3.
  • tls.CLIENT_RENEG_WINDOW 以秒为单位指定时间重新协商窗口. Default: 600 (10 minutes).

在没有充分理解含义和风险的情况下,不应修改默认重新协商限制。

TLSv1.3不支持重新协商。

Session resumption会话恢复

建立 TLS 会话可能相对较慢。可以通过保存并稍后重用会话状态来加快此过程。有几种机制可以做到这一点,此处按从最旧到最新(和首选)的顺序进行讨论。

Session identifiers会话标识符

服务器为新连接生成一个唯一 ID 并将其发送给客户端。客户端和服务器保存会话状态。重新连接时,客户端发送其已保存会话状态的 ID,如果服务器也具有该 ID 的状态,则它可以同意使用它。否则,服务器将创建一个新会话。有关更多信息,请参阅 RFC 2246,第 23 页和 30 页。

大多数网络浏览器在进行 HTTPS 请求时都支持使用会话标识符恢复会话。

对于 Node.js,客户端等待 ‘session’ 事件以获取会话数据,并将数据提供给后续 tls.connect() 的 session 选项以重用会话。服务器必须为 ’newSession’ 和 ‘resumeSession’ 事件实现处理程序,以使用会话 ID 作为查找键保存和还原会话数据以重用会话。为了在负载均衡器或集群工作进程之间重用会话,服务器必须在其会话处理程序中使用共享会话缓存(例如 Redis)。

Session tickets会话票证

服务器加密整个会话状态,并将其作为“票证”发送给客户端。重新连接时,该状态在初始连接中发送到服务器。此机制避免了对服务器端会话缓存的需求。如果服务器出于任何原因(无法解密、太旧等)不使用票证,它将创建一个新会话并发送一个新票证。有关更多信息,请参阅 RFC 5077。

许多网络浏览器在进行 HTTPS 请求时,开始普遍支持使用会话票证恢复连接。

对于 Node.js,客户端使用与会话标识符相同的 API 来恢复与会话票证的连接。用于调试,如果 tls.TLSSocket.getTLSTicket() 返回一个值,则会话数据包含一个票证,否则它包含客户端会话状态。

使用 TLSv1.3 时,请注意服务器可能会发送多个票证,从而导致多个 ‘session’ 事件,有关更多信息,请参阅 ‘session’ 。

单进程服务器无需特定实现即可使用会话票证。要在服务器重新启动或负载均衡器之间使用会话票证,服务器必须具有相同的票证密钥。内部有三个 16 字节的密钥,但 tls API 将它们公开为一个 48 字节的缓冲区以方便使用。

可以通过在一个服务器实例上调用 server.getTicketKeys() 来获取票证密钥,然后分发它们,但更合理的做法是安全生成 48 字节的安全随机数据,并使用 tls.createServer() 的 ticketKeys 选项设置它们。密钥应定期重新生成,服务器密钥可以使用 server.setTicketKeys() 重置。

会话票证密钥是加密密钥,必须安全存储。对于 TLS 1.2 及更低版本,如果这些密钥遭到破坏,则使用这些密钥加密的所有会话票证都可能被解密。不应将这些密钥存储在磁盘上,并且应定期重新生成这些密钥。

如果客户端宣传对票证的支持,服务器将发送这些票证。服务器可以通过在 secureOptions 中提供 require(’node:constants’).SSL_OP_NO_TICKET 来禁用票证。

会话标识符和会话票证都会超时,导致服务器创建新的会话。可以使用 tls.createServer() 的 sessionTimeout 选项配置超时。

对于所有机制,当恢复失败时,服务器将创建新的会话。由于无法恢复会话不会导致 TLS/HTTPS 连接失败,因此很容易忽略不必要的较差 TLS 性能。OpenSSL CLI 可用于验证服务器是否正在恢复会话。使用 -reconnect 选项来 openssl s_client ,例如:

openssl s_client -connect localhost:443 -reconnect 

仔细阅读调试输出。第一个连接应显示“NEW”,例如:

New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256 

后续连接应显示“已重复使用”,例如:

Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256 

Modifying the default TLS cipher suite修改默认的 TLS 密码套件

Node.js 使用一组默认启用的和禁用的 TLS 密码构建。在构建 Node.js 时可以配置此 默认密码列表,以允许发行版提供自己的默认列表。 可以使用以下命令显示默认密码套件:

node -p crypto.constants.defaultCoreCipherList | tr ':' '\n'
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256
ECDHE-RSA-AES128-GCM-SHA256
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES256-GCM-SHA384
ECDHE-ECDSA-AES256-GCM-SHA384
DHE-RSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-SHA256
DHE-RSA-AES128-SHA256
ECDHE-RSA-AES256-SHA384
DHE-RSA-AES256-SHA384
ECDHE-RSA-AES256-SHA256
DHE-RSA-AES256-SHA256
HIGH
!aNULL
!eNULL
!EXPORT
!DES
!RC4
!MD5
!PSK
!SRP
!CAMELLIA 

可以使用 –tls-cipher-list 命令行开关(直接或通过 NODE_OPTIONS 环境变量)完全替换此默认值。例如,以下操作使 ECDHE-RSA-AES128-GCM-SHA256:!RC4 成为默认 TLS 密码套件:

node --tls-cipher-list='ECDHE-RSA-AES128-GCM-SHA256:!RC4' server.js

export NODE_OPTIONS=--tls-cipher-list='ECDHE-RSA-AES128-GCM-SHA256:!RC4'
node server.js 

要验证,请使用以下命令显示设置的密码列表,注意 defaultCoreCipherList 和 defaultCipherList 之间的差异:

node --tls-cipher-list='ECDHE-RSA-AES128-GCM-SHA256:!RC4' -p crypto.constants.defaultCipherList | tr ':' '\n'
ECDHE-RSA-AES128-GCM-SHA256
!RC4 

即 defaultCoreCipherList 列表在编译时设置,而 defaultCipherList 在运行时设置。

要修改运行时中的默认密码套件,请修改 tls.DEFAULT_CIPHERS 变量,这必须在侦听任何套接字之前执行,它不会影响已打开的套接字。例如:

// Remove Obsolete CBC Ciphers and RSA Key Exchange based Ciphers as they don't provide Forward Secrecy
tls.DEFAULT_CIPHERS +=
  ':!ECDHE-RSA-AES128-SHA:!ECDHE-RSA-AES128-SHA256:!ECDHE-RSA-AES256-SHA:!ECDHE-RSA-AES256-SHA384' +
  ':!ECDHE-ECDSA-AES128-SHA:!ECDHE-ECDSA-AES128-SHA256:!ECDHE-ECDSA-AES256-SHA:!ECDHE-ECDSA-AES256-SHA384' +
  ':!kRSA'; 

还可以使用 tls.createSecureContext() 中的 ciphers 选项逐个客户端或服务器替换默认值,该选项在 tls.createServer() 、 tls.connect() 中以及创建新的 tls.TLSSocket 时也可用。

密码列表可以包含 TLSv1.3 密码套件名称(以 ‘TLS_’ 开头的名称)以及 TLSv1.2 及以下密码套件的规范的混合。TLSv1.2 密码支持旧版规范格式,有关详细信息,请参阅 OpenSSL 密码列表格式文档,但这些规范不适用于 TLSv1.3 密码。TLSv1.3 套件只能通过在密码列表中包含其全名来启用。例如,它们不能通过使用旧版 TLSv1.2 ‘EECDH’ 或 ‘!EECDH’ 规范来启用或禁用。

尽管 TLSv1.3 和 TLSv1.2 密码套件的相对顺序,TLSv1.3 协议比 TLSv1.2 安全得多,并且如果握手表明支持它,并且启用了任何 TLSv1.3 密码套件,它将始终被选择 超过 TLSv1.2。

Node.js 中包含的默认密码套件经过精心挑选,以反映当前的安全最佳实践和风险缓解措施。更改默认密码套件可能会对应用程序的安全性产生重大影响。只有在绝对必要时才应使用 –tls-cipher-list 开关和 ciphers 选项。

默认密码套件首选 Chrome 的“现代密码术”设置的 GCM 密码,还首选 ECDHE 和 DHE 密码以实现完美的前向保密,同时提供一些向后兼容性。 依赖不安全且已弃用的基于 RC4 或 DES 的密码(如 Internet Explorer 6)的旧客户端无法使用默认配置完成握手过程。如果必须支持这些客户端,则 TLS 建议可能提供兼容的密码套件。有关格式的更多详细信息,请参阅 OpenSSL 密码列表格式文档。

只有五个 TLSv1.3 密码套件:

  • ‘TLS_AES_256_GCM_SHA384’
  • ‘TLS_CHACHA20_POLY1305_SHA256’
  • ‘TLS_AES_128_GCM_SHA256’
  • ‘TLS_AES_128_CCM_SHA256’
  • ‘TLS_AES_128_CCM_8_SHA256’

默认情况下启用前三个。TLSv1.3 支持基于 的两个套件,因为它们在受限系统上可能性能更高,但默认情况下未启用它们,因为它们提供的安全性较低。

X509 证书错误代码

由于 OpenSSL 报告的证书错误,多个功能可能会失败。在这种情况下,该功能通过其回调提供一个 ,该回调具有属性 ,该属性可以采用以下值之一:

  • ‘UNABLE_TO_GET_ISSUER_CERT’ :无法获取颁发者证书。
  • ‘UNABLE_TO_GET_CRL’ :无法获取证书 CRL。
  • ‘UNABLE_TO_DECRYPT_CERT_SIGNATURE’ :无法解密证书的签名。
  • ‘UNABLE_TO_DECRYPT_CRL_SIGNATURE’ :无法解密 CRL 的签名。
  • ‘UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY’ :无法解码颁发者公钥。
  • ‘CERT_SIGNATURE_FAILURE’ :证书签名失败。
  • ‘CRL_SIGNATURE_FAILURE’ :CRL 签名失败。
  • ‘CERT_NOT_YET_VALID’ :证书尚未生效。
  • ‘CERT_HAS_EXPIRED’ :证书已过期。
  • ‘CRL_NOT_YET_VALID’ :CRL 尚未生效。
  • ‘CRL_HAS_EXPIRED’ :CRL 已过期。
  • ‘ERROR_IN_CERT_NOT_BEFORE_FIELD’ :证书的 notBefore 字段中存在格式错误。
  • ‘ERROR_IN_CERT_NOT_AFTER_FIELD’ :证书的 notAfter 字段格式错误。
  • ‘ERROR_IN_CRL_LAST_UPDATE_FIELD’ :CRL 的 lastUpdate 字段格式错误。
  • ‘ERROR_IN_CRL_NEXT_UPDATE_FIELD’ :CRL 的 nextUpdate 字段格式错误。
  • ‘OUT_OF_MEM’ :内存不足。
  • ‘DEPTH_ZERO_SELF_SIGNED_CERT’ :自签名证书。
  • ‘SELF_SIGNED_CERT_IN_CHAIN’ :证书链中的自签名证书。
  • ‘UNABLE_TO_GET_ISSUER_CERT_LOCALLY’ :无法获取本地颁发者证书。
  • ‘UNABLE_TO_VERIFY_LEAF_SIGNATURE’ :无法验证第一个证书。
  • ‘CERT_CHAIN_TOO_LONG’ :证书链太长。
  • ‘CERT_REVOKED’ :证书已撤销。
  • ‘INVALID_CA’ :CA 证书无效。
  • ‘PATH_LENGTH_EXCEEDED’ :路径长度限制超限。
  • ‘INVALID_PURPOSE’ :不受支持的证书用途。
  • ‘CERT_UNTRUSTED’ :证书不受信任。
  • ‘CERT_REJECTED’ :证书被拒绝。
  • ‘HOSTNAME_MISMATCH’ :主机名不匹配。
下一页
上一页