一、方法定位:它取的是对端的Finished消息
tlsSocket.getPeerFinished()是tls.TLSSocket类的实例方法,自Node.jsv9.9.0起可用。它的职责单一:返回SSL/TLS握手过程中预期从对端接收、或实际已接收的新Finished消息。如果截至调用时刻尚未收到对端的Finished消息,返回undefined。
Finished消息在TLS协议中承担握手完整性校验的功能。它是完整握手过程的摘要值——TLS1.0中总长192位,SSL3.0更长。双方各自计算并交换Finished消息,任何一方校验失败即终止连接。getPeerFinished()暴露的就是对端发来的那一份摘要数据。
方法本身不发起任何网络操作,也不等待握手完成。它只是读取当前TLSSocket实例内部已保存的状态。调用时机决定了返回值:握手早期调用得到undefined,握手完成后调用得到Buffer。
二、返回值语义与调用时机
返回值类型为Buffer|undefined。官方文档的措辞是“预期或实际已从套接字接收到的新Finished消息”。这里的“预期”值得注意:在某些内部状态下,OpenSSL可能已经知道将要接收的消息摘要,但尚未完成接收。对于应用层代码,更实际的判断方式是:如果需要在握手完成后使用该值,应当在'secure'事件或tls.connect()的回调中调用,而非在套接字创建后立即调用。
原始文章示例在tls.connect的回调中调用client.getPeerFinished(),然后立即对返回值执行.toJSON().d。这个写法依赖于回调触发时握手已完成、且对端Finished消息已到达。在常规的本地测试中这成立,但在网络延迟较高或对端实现非标准TLS栈的场景中,直接解引用返回值存在风险。更稳妥的写法是先判断undefined:
const tls = require('node:tls');
const fs = require('node:fs');
const options = {
key: fs.readFileSync('private.pem'),
cert: fs.readFileSync('public.pem'),
rejectUnauthorized: false
};
const client = tls.connect(1880, '127.0.0.1', options, () => {
const peerFinished = client.getPeerFinished();
if (peerFinished === undefined) {
console.error('握手回调触发时尚未收到对端 Finished 消息');
client.destroy();
return;
}
console.log('对端 Finished 摘要长度:', peerFinished.length);
client.end();
});
Buffer的长度取决于协商的TLS版本和密码套件,不应硬编码期望值。用length做日志或校验是合理的,用固定长度做断言则可能在新版本或不同套件下失败。
三、与getFinished()的职责对比
tlsSocket.getFinished()返回的是本端发送给对端的Finished消息,与getPeerFinished()构成对称关系。两者都对应OpenSSL的例程:getFinished()对应SSL_get_finished,getPeerFinished()对应SSL_get_peer_finished。
这一对方法的实际用途在RFC5929中定义:tls-unique通道绑定。通道绑定的目的是将应用层认证(如OAuthtoken、SASL认证)与底层的TLS会话绑定,防止攻击者将认证凭证从一个TLS连接转移到另一个连接上。tls-unique取的是首次Finished消息的摘要值,双方各自持有本端和对端的Finished,交叉比对可以确认双方处于同一个TLS会话中。
如果应用只需要验证“对端确实完成了握手”,'secure'事件已经足够,不需要调用getPeerFinished()。这个方法的价值在于需要拿到摘要数据本身做外部比对或协议层面的绑定校验。Node.js社区中提出该API的原始需求来自rippled(XRPLedger的节点实现),其peer协议需要显式交换和校验Finished摘要来确认对端身份。
四、TLS1.3下的行为变化
这是一个需要留意的边界。tls-unique通道绑定定义于RFC5929,其基础是TLS1.2及以下版本中Finished消息的确定性。TLS1.3改变了握手消息的调度方式和密钥派生流程,tls-unique在TLS1.3中的适用性受到限制。Node.js的getPeerFinished()在TLS1.3连接上仍然返回数据,但该数据的语义与TLS1.2下的tls-unique值并不等价。
Node.js的issue跟踪中记录了将getPeerFinished与TLS1.3通道绑定(RFC9266定义的tls-exporter)对齐的讨论。如果项目使用该方法做通道绑定,且连接可能协商到TLS1.3,应当确认绑定方案是否已适配。在无法控制协商版本的环境中,显式限制maxVersion:'TLSv1.2'是一种保守做法,但代价是放弃TLS1.3的安全改进。更合理的路径是评估是否可以用exportKeyingMaterial替代——后者在TLS1.3下有定义良好的行为。
五、常见误区
误区一:认为握手完成就一定返回Buffer。'secure'事件触发表示握手完成,但对端Finished消息的接收与'secure'事件的触发在内部时序上不保证严格有序。在极少数网络条件下或对端实现异常时,getPeerFinished()可能返回undefined。直接调用.toJSON()或.toString()而不做判断,会产生TypeError:Cannotreadpropertiesofundefined。
误区二:用该方法判断握手是否成功。握手失败时'error'事件会触发,TLSSocket被销毁,此时调用getPeerFinished()可能返回undefined,也可能抛出异常(取决于套接字状态)。正确的握手结果判断依据是'secure'事件(成功)或'error'事件(失败),不是Finished消息的有无。
误区三:在服务端'secureConnection'回调之外调用。服务端TLSSocket的getPeerFinished()在'secureConnection'回调触发后可用。如果在tls.createServer的连接建立早期(如'connection'事件)调用,此时握手尚未开始或进行中,返回值是undefined。
六、练习与思考
练习1:搭建一个本地TLS服务端和客户端,在客户端的tls.connect回调中分别打印getFinished()和getPeerFinished()的返回值长度。然后在服务端的'secureConnection'回调中做同样的打印,对比两端的输出。解释为什么客户端和服务端拿到的摘要值不同。
练习2:将客户端的maxVersion限制为'TLSv1.2',重复练习1,记录Finished消息的长度。然后移除限制,观察协商到TLS1.3时长度是否变化。查阅RFC8446中关于Finished消息计算方式的描述,解释差异来源。
思考:如果一个应用需要在应用层认证中绑定TLS会话,但连接可能协商到TLS1.3,你会选择继续使用getPeerFinished()配合版本限制,还是改用exportKeyingMaterial?两种方案分别需要处理哪些兼容性问题?
七、延伸阅读与参考文献
Node.js官方文档tlsSocket.getPeerFinished()条目。提供了返回值的权威定义、与OpenSSLSSL_get_peer_finished的对应关系,以及tls-unique通道绑定的用途说明。https://nodejs.org/api/tls.html#tlssocketgetpeerfinished
Node.js官方文档tlsSocket.getFinished()条目。对称方法的定义,对照阅读有助于理解本端与对端Finished消息的区分。https://nodejs.org/api/tls.html#tlssocketgetfinished
Node.jsPR#19102“tls:exposeFinishedmessagesinTLSSocket”。该方法的引入记录,说明了暴露OpenSSL例程的技术动机和原始需求来源。https://github.com/nodejs/node/pull/19102
Node.jsIssue#19055“exposingSSL_get_finishedandSSL_get_peer_finished”。原始功能请求,记录了rippledpeer协议对该API的具体需求场景。https://github.com/nodejs/node/issues/19055
RFC5929“ChannelBindingsforTLS”。tls-unique通道绑定的规范定义,说明了Finished消息在绑定校验中的使用方式。https://www.rfc-editor.org/rfc/rfc5929