使用威胁行为者执行中间人(MITM)攻击,拥有SSL/TLS证书不再是信任传入连接的有效理由。因此,开发人员越来越多地采用SSL/TLS固定(也称为证书或公共密钥固定),作为证明连接的真实性和完整性的另一种措施。
在Node.js应用程序中,SSL/TLS固定通过防止攻击者拦截和篡改客户端和服务器之间的通信,从而增加了一层安全性。
本文解释了证书固定,突出了其在node.js应用程序中的好处和用例。
您何时以及为什么使用SSL/TLS固定
SSL/TLS固定是一种安全机制,可帮助防止MITM和其他与证书相关的攻击。它通过确保客户端(例如Node.js应用程序)仅连接到具有预验证的数字证书的服务器来做到这一点。该技术在主机应用程序中存储并使用特定的证书或公共密钥与服务器的公钥或证书进行比较。我们可以通过环境变量将证书或公共密钥将证书或公共密钥进行编码,或将其外部存储在密钥保险库服务中以提高灵活性。
证书固定,通过考虑所有请求将匹配的证书作为流氓并终止它们的所有请求,从而降低了MITM攻击的风险,即使该请求使用HTTPS。它还有助于防止其他与证书相关的漏洞,例如证书欺骗或篡改和妥协证书局(CAS)。
根据Stack Overflow Developer Survey,有47.12%的开发人员使用node.js,使其成为服务器端应用程序的标准Web技术。 Node.js中内置的模块,例如HTTPS,通过验证固定证书来为应用程序添加额外的保护层。使用证书固定的Node.js应用程序将TLS握手期间呈现的证书与预定义的证书或公共钥匙进行比较。如果有不匹配,它将终止运输层的请求。
证书固定的一个用例与移动设备上的财务应用程序有关。 MITM对此类设备的攻击可能会对所有者造成灾难性的灾难,因为该设备可能会传输敏感的个人信息,例如信用卡或联系信息。尽管诸如Android Pie have security features之类的新移动OS版本仅允许应用程序建立安全的连接,但威胁参与者可以通过生成自签名证书来访问设备来发射网络钓鱼攻击。证书固定确保该应用程序即使设备信任自签名证书也不会授予胭脂连接。
为SSL/TLS固定实现做准备
客户端将网络请求发送到服务器时,传输层使用SSL/TLS协议启动安全握手。该服务器提供其数字证书,其中包含用于加密和身份验证的公钥。在握手期间,应用程序从提出的证书中检索服务器的公钥。
应用程序将检索到的公钥与公共密钥的预配置或硬编码副本进行比较。如果公共密钥与本地存储的副本匹配,则将连接视为安全,并且可以进行通信。但是,如果密钥不匹配,该应用程序可以根据其安全策略终止链接或采取适当的措施,例如提高警报或拒绝发送敏感数据。
CAS通过提供SSL/TLS证书来建立安全连接,在证书固定过程中起着至关重要的作用。 CA签署了浏览器为基于Web的应用程序或信任OS链验证的证书。证书固定是确认证书的另一步骤。
TLS是node.js中的内置模块,可以从主机中提取包含公共密钥字段的证书对象。下面的代码演示了如何使用TLS模块向主机提出请求并在对象中检索证书:
const tls = require("tls");
const host = "example.com";
const port = 443;
const socket = tls.connect(port, host, () =>
const certificate = socket.getPeerCertificate();
const publicKey = certificate.publicKey;
console.log("Public Key:", publicKey);
console.log("Certificate:", certificate);
});
socket.on("error", (error) => {
console.error("Connection error:", error);
});
在此代码中,getPeerCertificate
方法负责提取证书。我们还可以使用getPeerX509Certificate方法以x509Certificate对象格式检索同行证书。
在Node.js中实现SSL/TLS固定
要在node.js应用程序中使用ssl/tls固定,我们必须在请求配置中指定键或证书。在https中,ca
,cert
和key
属性在https.request
选项中辅助SSL/tls固定。固定证书时,ca
属性应指定包含可信赖的隐私增强邮件(PEM)编码的证书或CA证书的数组。密钥属性应指定包含与受信任服务器证书关联的私钥的数组。
Snyk Code是开发人员优先的static application security testing(SAST)工具,在开发过程中实时识别代码库中的安全漏洞。它可通过命令行接口(CLI)命令,软件开发套件(SDK)进行连续集成和连续部署(CI/CD)管道,Web接口,集成开发环境(IDES)的扩展程序和代码编辑器。在Node.js项目中,SNYK代码在缺少SSL/TLS证书的支持时可以检测并提醒我们。
要识别缺失的SSL/TLS支持,请使用下面的code test命令从您的终端扫描项目代码库。 snyk ignore --file-path=
命令允许我们忽略我们不想包含在SNYK扫描中的文件或目录,例如包含我们的node.js套件的node_modules
目录:
snyk code test
尽管SSL/TLS固定提供了安全益处,但如果实施不正确,则可能是灾难性的。我们的固定实现应该具有处理边缘案件的故障安全计划,例如撤销或过期的固定证书时。错误处理会阻止整个应用程序在无法确认证书或公钥时崩溃。运行审核可以帮助我们记录此类情况。
开始,创建一个名为snyk-tls
的目录并使用npm init -y
。使用以下命令安装请求软件包:
npm i request
下面的代码在Node.js应用程序中演示了一个固定的公钥,该应用程序匹配使用HTTPS
和request
模块创建的GET
请求的证书公钥。如果公共密钥不匹配,则代码将中止请求。创建一个index.js文件,然后向其添加以下代码:
const Agent = require("https").Agent;
const request = require("request");
const PINNED_KEY = [
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
];
var options = {
url: "HOST_URL",
headers: {
"User-Agent": "Node.js/https",
},
agent: new Agent({
maxCachedSessions: 0,
}),
strictSSL: true,
};
var req = request(options, (err, response, body) => {
if (err) console.log(err);
else console.log(body);
});
req.on("socket", (socket) => {
socket.on("secureConnect", () => {
var key = socket.getPeerCertificate().publicKey;
if (!PINNED_KEY.includes(key)) {
req.emit("error", new Error("Key does not match"));
return req.abort();
}
});
});
在上面的代码中,strictSSL
配置了应用程序仅在客户端提出请求时使用https来检索证书的publicKey
。它还检查PINNED_KEY
阵列以确定公共密钥是否是允许的键之一。如果不包括密钥,则中止方法终止了请求。 Agent
构造函数中的maxCacheSessions
属性还配置了request
模块以防止会话缓存,因此代码在每个请求下检查固定的证书。
对于生产,我们应该使用环境变量或单独的密钥保险库服务存储公共密钥或证书,以防止当我们将代码推向代码存储库时公共密钥。
测试和维护SSL/TLS固定
测试固定实现有效性的一种好方法是模拟MITM攻击。诸如Mitmproxy或Wireshack之类的工具允许我们创建一个测试环境来监视,拦截和代理网络请求测试主机。
要测试SSL/TLS固定的有效性,我们必须禁用固定证书并通过代理启动MITM攻击,以记录应用程序性能的基线行为而不会固定。然后,我们重复使用固定的测试启用启用,以查看应用程序是否可以检测代理并终止连接。
比较证书键哈希是测试SSL/TLS固定的另一种方法。证书哈希是证书独有的,因为加密哈希功能映射任意大小的数据到代表证书的固定尺寸值。这使其成为验证证书的合适选择。使用node.js crypto模块,我们可以从主机的证书中生成哈希,并将其与我们预期的公钥哈希进行比较。
测试我们的SSL/TLS固定的第三种方法是使用网络扫描工具(例如Nmap)执行SSL端口扫描。 NMAP提供了ssl-enum-chipers
命令来扫描指定目标上的SSL端口。
维护和更新固定证书或公共密钥对于维持通信渠道的信任,安全性和完整性至关重要。通过保持最新状态,我们可以减轻与证书折衷,到期和撤销相关的风险,同时确保安全和身份验证的连接。
打开终端中的snyk-tls
文件夹并在下面运行命令。
echo -n | openssl s_client -connect google.com:443 -servername google.com | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > cert.pem
此命令生成一个包含Google.com公共证书的cert.pem
文件。
接下来,在项目文件夹中创建一个main.js
文件。粘贴下面的代码。
// import the required modules
const tls = require('node:tls');
const https = require('node:https');
const { exec } = require('child_process');
// setting up the variables
const certPath = 'cert.pem';
const command = `openssl x509 -noout -in ${certPath} -fingerprint -sha256`;
// generate SHA256 fingerprint of the certificate
const generate_SHA256_fingerprint = () => {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(new Error('Error executing OpenSSL command: ' + error));
return;
}
if (stderr) {
reject(new Error('OpenSSL command returned an error: ' + stderr));
return;
}
resolve(stdout.split('=')[1].trim());
});
});
};
// verify the certificate
const verifyCertificate = async () => {
try {
const cert256 = await generate_SHA256_fingerprint();
console.log('Pinned Certificate Fingerprint:', cert256);
const options = {
hostname: 'google.com',
port: 443,
path: '/',
method: 'GET',
checkServerIdentity: function (host, cert) {
// Make sure the certificate is issued to the host we are connected to
const err = tls.checkServerIdentity(host, cert);
if (err) {
return err;
}
// Pin the exact certificate
if (cert.fingerprint256 !== cert256) {
const msg = 'Certificate verification error: ' +
`The certificate of '${cert.subject.CN}' ` +
'does not match our pinned fingerprint';
return new Error(msg);
}
},
};
options.agent = new https.Agent(options);
const req = https.request(options, (res) => {
console.log('All OK. Server matched our pinned cert');
});
req.on('error', (e) => {
console.error(e.message);
});
req.end();
} catch (error) {
console.error('Error generating SHA256 fingerprint:', error);
}
};
verifyCertificate();
上面的代码导入所需的模块并定义了两个变量certPath
和command
。 certPath
定义了证书文件的路径。 command
拥有Shell命令的值,以生成证书S SHA-256指纹。
generate_SHA256_fingerprint()
函数使用exec
函数来执行生成证书S SHA-256指纹的OpenSSL命令。此功能返回通过指纹解决的承诺。
verifyCertificate()
函数定义了一个选项对象,以配置HTTPS请求。该对象指定hostname
,port
,path
和method
。它还定义了checkServerIdentity()
函数以验证服务器证书。该功能可确保证书属于我们连接到的主机,并检查指纹是否与固定指纹匹配。
要测试此脚本,请在终端中执行命令node main.js
。您应该收到以下输出。
All OK. Server matched our pinned cert
要更新固定证书或键,我们打开API文件并运行生成cert.pem
文件的openssl
命令。当使用外部保险库服务存储固定的证书或公共密钥时,我们可以在不修改我们的应用程序代码的情况下编辑保险柜服务中的值。需要更新我们的固定证书的方案包括:
- 证书或公共钥匙到期
- 数据泄漏
- 发行人的撤销
- 用户的合规性政策在指定的持续时间之后旋转证书
结论
本文教会了我们如何在Node.js应用程序中实现SSL/TLS固定。 SSL/TLS固定通过使用本地存储的副本验证服务器的公钥或证书来增加额外的安全性。它有助于保护我们的应用程序免受MITM攻击和其他与证书相关的漏洞。
node.js具有支持证书固定的https之类的模块。我们可以通过比较TLS握手期间的公共密钥并终止连接,如果存在不匹配,我们可以应用SSL/TLS固定。适当的错误处理对于避免在证书或公共密钥或发行人撤销的边缘案例中锁定的情况也至关重要。通过模拟攻击,比较钥匙哈希并更新证书或密钥来测试SSL/TLS在我们的应用程序中固定,这是确保其有效性的最佳实践。另一个最佳做法是安全地存储公共钥匙。
通过利用SSL/TLS固定,我们可以增强Node.js应用程序的整体安全性,确保可靠且安全的用户体验。