虽然 John Donne 的诗中没有提及,但任何应用程序都不是孤立的。应用程序常常需要与远程服务器进行通信并与它们交换信息。为了帮助实现此目的,Node.js 拥有许多包,通过使用各种协议与服务器进行通信。
但是,使用远程服务器却会带来伪装的风险。 攻击者可以假装成合法的合作伙伴,盗窃或伪造信息。为了防止这种情况,我们可以使用证书,这已在“ IBM Global Security Kit 7.0:管理证书 ”(一本 PDF 格式的 IBM 白皮书)中进行了介绍。在本文中,将学习如何在运行于 Bluemix™ 内的 Node.js 应用程序中使用证书来防止这种伪装。
运行应用程序
获取代码
“ 在本文中,将学习如何在运行于 Bluemix 上的 Node.js 应用程序中使用证书来防止攻击者伪装成为合法的合作伙伴。 ”
此应用程序允许用户输入一个 URL。然后与服务器进行通信,以获取服务器证书。它还允许您检查自签名的证书。
HTTPS 协议可能是全世界最重要的应用程序级协议的安全版本,它不但被用作用户界面,还被用于程序之间的通信(通过使用 REST)。为了充当 HTTPS 客户端,Node.js 应用程序使用了 HTTPS 包 。
// Issue http requests var https = require("https"); . . . var httpsReq = https.request(req.query.url, function(httpsRes) {…});
创建 HTTPS 请求有两个步骤。首先,实例化 HTTPS 包,然后使用 https.request
发出请求。 https.request
函数接收两个参数。 第一个参数可以是服务器的 URL(正如后面将会讲到的,它也可以是一个选项数组)。第二个参数是一个在应用程序收到响应时调用的函数。
httpsReq.end();
Express 不允许在仍有未处理的请求时进行响应。 上面的调用告诉 HTTPS 包该请求已完成,是时候发送它了。
httpsReq.on("error", function(err) {…});
上面的调用设置了一个错误处理程序(error handler)。该程序在这里很重要,因为您既想要看到无效的证书,又想要看到有效的证书。
该 HTTPS 请求(样例程序中的 httpsReq)包含一个名为 socket
的字段,它是一个通信套接字。此字段的类型为 tls.TLSSocket。要获取证书,可以使用函数 getPeerCertificate()
。此函数接收一个布尔值。 如果该值为 true,那么不仅会返回证书,还会返回证明该证书的整个证书链。
在示例程序中,此操作是在函数 socketCert2HTML
中完成的:
if (socket == null) return "<b>No socket</b>"; var cert = socket.getPeerCertificate(false); if (cert == null) return "<b>No certificate in the socket</b>";
该函数首先检查两个错误条件。因为还可以使用错误处理程序调用此函数,所以可能根本没有套接字,或者套接字可能没有证书。
var auth = ""; if (socket.authorized) auth = "<b>Certificate is legitimate</b>"; else auth = "<b>Certificate looks fake</b><br />" + socket.authorizationError;
要检查证书是否被视为已授权(这意味着它是该环境信任的证书颁发机构颁发的有效证书),可以使用 socket.authorized
。如果证书存在问题,问题的性质会在 socket.authorizationError
中进行解释。
return auth + "<br /><pre>" + JSON.stringify(cert, null, 4) + "</pre>";
证书本身是一种可以用 JSON 格式显示的结构。 以下是 Bluemix 自身的证书的一部分:
{ "subject":{ "C":"US", "ST":"New York", "L":"Armonk", "O":"International Business Machines Corporation", "CN":"*.ng.bluemix.net" }, "issuer":{ "C":"US", "O":"DigiCert Inc", "CN":"DigiCert SHA2 Secure Server CA" }, "subjectaltname":"DNS:*.ng.bluemix.net, DNS:ng.bluemix.net", "infoAccess":{ "OCSP - URI":[ "http://ocsp.digicert.com" ], "CA Issuers - URI":[ "http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt" ] }, "modulus":"E02DABBF2337B98C42D572...", "exponent":"10001", "valid_from":"Sep 29 00:00:00 2014 GMT", "valid_to":"Nov 8 12:00:00 2017 GMT", "fingerprint":"97:F4:45:FA:7B:9A:12:98:FB:18:5F:76:80:19:02:A3:84:FE:4D:74", "ext_key_usage":[ "1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.2" ], "serialNumber":"06380DF0F0AF299B5AD75BBB851F7301", "raw":{ "type":"Buffer", "data":[…] } }
在知道如何检索证书细节后,可以使用这些细节在您自己的代码中“手动”检查证书,而不依赖于服务器软件。
此刻,可能有人问我为什么要如此费心地撰写这篇文章。我可以告诉您是为了检查 socket.authorized
。事实上,您甚至不需要这么做。 如果证书被识别为无效的,那么 https 请求会抛出一个错误。
这是事实。但是,您亲自检查证书的原因有很多:
检查证书的最简单方法是使用 指纹值(fingerprint value) 。指纹值是一种密码校验和,它的值取决于证书中的所有重要字段。
要查看此功能,可以在应用程序的第二个面板中键入一个主机名。如果该主机名是 certs.simple-tech.com(或 IP 地址 129.41.135.12)。那么该指纹是匹配的,而且证书会被接受。否则,证书会遭到拒绝。
要解决的第一个问题是,如果为 https.request()
函数提供一个 URL,此 URL 的证书无法自动验证,该函数抛出一条错误消息并退出,此时会发生什么。自签名证书和来自未知证书颁发机构的证书不会被识别为有效证书。解决方案是将连接参数指定为一个关联数组而不是一个 URL 字符串,以便允许使用额外的参数。
// Check if the certificate matches ceerts.simple-tech.com app.get("/check_self_signed", function(req, res) { var httpsReq = https.request({ hostname: req.query.hostname, port:443, path:"/", method:"GET", rejectUnauthorized: false // To avoid an error, because the cert is // unauthorized }, function(httpsRes) {…}); });
大部分连接参数都是标准 HTTP 值,它们通常包含在 URL 中。 rejectUnauthorized
异常是一个布尔值,可用来禁用不良证书错误。
证书的指纹是以人类可读的字符串形式存在的。要验证它,可以检查它是否与您知道的指纹等效。
function(httpsRes) { if (httpsReq.socket.getPeerCertificate().fingerprint == "D6:0A:10:CB:C5:B3:F9:B6:A8:89:05:F2:50:28:5E:17:75:A6:98:76") res.send("This is certs.simple-tech.com"); else res.send("This is a different server"); }
请注意,这段简单代码没有检查证书是否过期。如果需要检查这一点,可以使用 JavaScript Date.now() 函数。
将指纹直接存储在代码中对程序员而言很简单,但在服务器上的指纹发生改变时,这会让指纹调整操作变得更困难。要将指纹存储为一个容易在未来修改的属性,请参阅“ 使用 Bluemix 和 MEAN 堆栈构建自助发表 Facebook 信息的应用程序,第 3 部分 ”中的第 5 步。
在包含证书的私钥被泄漏时,无法再使用该证书,即使它仍在有效期内。证书本身不可能修改,所以为了向客户告知有关这些证书的信息,证书颁发机构使用了证书吊销列表 (CRL),通过开放证书状态协议 (OCSP) 来提供该信息。
不幸的是,在撰写本文时(2015 年 10 月),Bluemix Node.js 环境中对 OCSP 的支持似乎遭到了破坏。要验证这一点,请检查 https://test-sspev.verisign.com:2443/ 的状态。该地址有一个已吊销的证书,目的是为了允许客户端开发人员检查其代码。大多数浏览器都不允许您访问该 URL。
但是,Node.js 中的证书检查器允许访问此站点。
要检查是否仍是这种情况,可以单击 此处 。这是在解决该问题前看到的信息:
如果将要使用 HTTPS 访问许多无法识别其证书的网站,将指纹功能封装在一个对象中而不是将它添加到每个请求中会更容易一些。在理想情况下,此类对象应该与 HTTPS 尽可能相似。
// Wrapper around https to use a fingerprint instead of the normal // certificate verification.This wrapper replicates the two // client functions in https, https.request() and https.get(). var fingerprintHttps = { request: function(requestUrl, fingerprint, handler) { … }, get: function(requestUrl, fingerprint, handler) { … } };
第一步是创建一个将包含这些函数的结构。 对待 JavaScript 中函数的方式与对待其他任何变量类型的方式一样,所以语法是相同的。因为 HTTPS 有两个客户端函数 https.request()
和 https.get()
,所以我们在这里实现它们。
var opts; // Is requestURL a string URL we need to parse, or already an array of options? if(typeof requestUrl == "string") opts = url.parse(requestUrl); else opts = requestUrl; // Add the parameter to disable automatic checking of certificate // validity opts.rejectUnauthorized = false;
HTTPS 客户端函数可以接受一个字符串或一个结构作为第一个参数。出于我们的目的,我们需要使用一个结构来指定 rejectAuthorized = false
。实现此目的的方法是检查该参数是否为字符串。如果是,则将它解析为一种结构。如果它已经是一种结构,则使用该结构。
要获得 JavaScript 中的变量类型,可以使用 typeof 运算符 ,如上所示。
// Issue the actual request var httpsReq = https.request(opts, function(httpsRes) { if (httpsReq.socket.getPeerCertificate().fingerprint == fingerprint) handler(httpsRes); else httpsReq.emit("error", new Error("Fingerprint mismatch")); });
发出实际请求的部分非常简单。唯一的新内容是 httpsReq.emit()
函数调用。就像我们之前看到的,在 HTTPS 对象上注册错误处理程序的方法是使用 .on("error", <handler>)
语法。此方法起作用是因为,HTTPS 对象是 EventEmitter 的实例,因此可以使用 .emit()
创建随后由 .on()
监听器处理的事件。
备注:这是一个用于教学目的的简化实现。真正的实现会用监听器监听 HTTPS 对象可以创建的所有事件类型,并将所有这些事件传递到向 fingerprintHttps
对象注册的针对相同事件的任何监听器。否则,它可能使用 真正的继承(real inheritance) ,而不是一个简单的包装器。
目前为止,我们的实现简单、明了,但可能不安全。这是操作顺序:
https.request() call
)。 这个操作顺序中的问题在于,我们在识别服务器之前发送了请求。我们可能忽视了伪造的响应,但伪装的服务器仍在接收请求,请求中可能包含机密信息。这对一些应用程序而言是无法接受的。
HTTPS 包没有为我们提供指定建立安全连接时调用回调的地方。但是,它确实拥有请求过程中在各个点中发出的事件。(有关的更多信息,请参阅 Node.js v4.2.1 文档 。)这为我们提供了一个在发送请求的剩余部分之前处理证书的地方(只要证书可用)。
httpsReq.on("socket", function() { // We are connected to a socket now httpsReq.socket.on("secureConnect", function() { // Now we are connected with encryption
此代码有两个事件处理程序。第一个处理程序 httpsReq.on("socket", function() {…});
在为请求创建套接字时调用。但是,这个时候检查证书太早了。该处理函数会在客户端与远程服务器进行通信、获取证书和创建隧道之前调用。因此,在该事件处理程序内,有另一个事件处理程序,后者会在套接字发出 secureConnect 事件时调用。第二个事件在创建了隧道并且证书可用时发生。
res.send("Fingerprint:"+ httpsReq.socket.getPeerCertificate().fingerprint); // Pretend the fingerprint does not match, and abort the request httpsReq.abort();
第二个处理程序内的代码没有多大的作用。它发回指纹向用户显示一些事情正在发生,然后假设该指纹是错误的并中止。此功能与 fingerprintHttps
对象的集成留给读者作为练习。
现在,您将能够在自己的代码中安全地使用 HTTPS 服务器,无论它们的证书是否被 Bluemix 识别。LDAP 等其他客户端也使用了 tls.TLSSocket,所以保护它们的方法是类似的。
相关主题: MEAN Node.js 证书
BLUEMIX SERVICE USED IN THIS TUTORIAL: SDK for Node.js 运行时 帮助您轻松地开发、部署和扩展服务器端 JavaScript 应用程序。