如何在HTTPS Node.js服务器上使用自签名证书?

61

我已经开始为一个需要所有请求都使用HTTPS的API编写一个包装器。在开发和测试期间,我不想向实际的API发送请求,而是希望在本地运行模拟响应的服务器。

我对如何生成所需证书以创建HTTPS服务器并向其发送请求感到困惑。

我的服务器看起来像这样:

var options = {
  key: fs.readFileSync('./key.pem'),
  cert: fs.readFileSync('./cert.pem')
};

https.createServer(options, function(req, res) {
  res.writeHead(200);
  res.end('OK\n');
}).listen(8000);

pem文件是通过以下方式生成的:

openssl genrsa 1024 > key.pem
openssl req -x509 -new -key key.pem > cert.pem

一个请求看起来像这样:

var options = {
  host: 'localhost',
  port: 8000,
  path: '/api/v1/test'
};

https.request(options, function(res) {
  res.pipe(process.stdout);
}).end();

通过这个设置,我得到了Error: DEPTH_ZERO_SELF_SIGNED_CERT错误,所以我认为我需要为请求添加一个ca选项。

那么我的问题是如何生成以下内容:

  1. 服务器的key
  2. 服务器的cert
  3. 请求的ca

我已经阅读了一些关于使用openssl生成自签名证书的文章,但似乎无法理解并确定在我的node代码中应该使用哪些密钥和证书。

更新

API提供了一个CA证书来替代默认值。以下代码使用他们的证书可以工作,这就是我想在本地复制的内容。

var ca = fs.readFileSync('./certificate.pem');

var options = {
  host: 'example.com',
  path: '/api/v1/test',
  ca: ca
};
options.agent = new https.Agent(options);

https.request(options, function(res) {
  res.pipe(process.stdout);
}).end();
6个回答

69

更新(2018年11月):您是否需要自签名证书?

或者真正的证书是否更好地完成了工作?您考虑过这些吗?

(注意:Let's Encrypt也可以向私有网络颁发证书)

屏幕录制

https://coolaj86.com/articles/how-to-create-a-csr-for-https-tls-ssl-rsa-pems/

完整、可行的示例

  • 创建证书
  • 运行Node.js服务器
  • Node.js客户端无警告或错误
  • cURL无警告或错误

https://github.com/coolaj86/nodejs-self-signed-certificate-example

localhost.greenlock.domains为例(它指向127.0.0.1):

server.js

'use strict';

var https = require('https')
  , port = process.argv[2] || 8043
  , fs = require('fs')
  , path = require('path')
  , server
  , options
  ;

require('ssl-root-cas')
  .inject()
  .addFile(path.join(__dirname, 'server', 'my-private-root-ca.cert.pem'))
  ;

options = {
  // this is ONLY the PRIVATE KEY
  key: fs.readFileSync(path.join(__dirname, 'server', 'privkey.pem'))
  // You DO NOT specify `ca`, that's only for peer authentication
//, ca: [ fs.readFileSync(path.join(__dirname, 'server', 'my-private-root-ca.cert.pem'))]
  // This should contain both cert.pem AND chain.pem (in that order) 
, cert: fs.readFileSync(path.join(__dirname, 'server', 'fullchain.pem'))
};


function app(req, res) {
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, encrypted world!');
}

server = https.createServer(options, app).listen(port, function () {
  port = server.address().port;
  console.log('Listening on https://127.0.0.1:' + port);
  console.log('Listening on https://' + server.address().address + ':' + port);
  console.log('Listening on https://localhost.greenlock.domains:' + port);
});

客户端.js

'use strict';

var https = require('https')
  , fs = require('fs')
  , path = require('path')
  , ca = fs.readFileSync(path.join(__dirname, 'client', 'my-private-root-ca.cert.pem'))
  , port = process.argv[2] || 8043
  , hostname = process.argv[3] || 'localhost.greenlock.domains'
  ;

var options = {
  host: hostname
, port: port
, path: '/'
, ca: ca
};
options.agent = new https.Agent(options);

https.request(options, function(res) {
  res.pipe(process.stdout);
}).end();

制作证书文件的脚本:

make-certs.sh

#!/bin/bash
FQDN=$1

# make directories to work from
mkdir -p server/ client/ all/

# Create your very own Root Certificate Authority
openssl genrsa \
  -out all/my-private-root-ca.privkey.pem \
  2048

# Self-sign your Root Certificate Authority
# Since this is private, the details can be as bogus as you like
openssl req \
  -x509 \
  -new \
  -nodes \
  -key all/my-private-root-ca.privkey.pem \
  -days 1024 \
  -out all/my-private-root-ca.cert.pem \
  -subj "/C=US/ST=Utah/L=Provo/O=ACME Signing Authority Inc/CN=example.com"

# Create a Device Certificate for each domain,
# such as example.com, *.example.com, awesome.example.com
# NOTE: You MUST match CN to the domain name or ip address you want to use
openssl genrsa \
  -out all/privkey.pem \
  2048

# Create a request from your Device, which your Root CA will sign
openssl req -new \
  -key all/privkey.pem \
  -out all/csr.pem \
  -subj "/C=US/ST=Utah/L=Provo/O=ACME Tech Inc/CN=${FQDN}"

# Sign the request from Device with your Root CA
openssl x509 \
  -req -in all/csr.pem \
  -CA all/my-private-root-ca.cert.pem \
  -CAkey all/my-private-root-ca.privkey.pem \
  -CAcreateserial \
  -out all/cert.pem \
  -days 500

# Put things in their proper place
rsync -a all/{privkey,cert}.pem server/
cat all/cert.pem > server/fullchain.pem         # we have no intermediates in this case
rsync -a all/my-private-root-ca.cert.pem server/
rsync -a all/my-private-root-ca.cert.pem client/

# create DER format crt for iOS Mobile Safari, etc
openssl x509 -outform der -in all/my-private-root-ca.cert.pem -out client/my-private-root-ca.crt
例如:
bash make-certs.sh 'localhost.greenlock.domains'
希望这能彻底解决这个问题。并附上更多说明:https://github.com/coolaj86/node-ssl-root-cas/wiki/Painless-Self-Signed-Certificates-in-node.js 在iOS移动版Safari上安装私有证书
您需要创建一个根CA证书的DER格式副本,并将其扩展名更改为.crt。
# create DER format crt for iOS Mobile Safari, etc
openssl x509 -outform der -in all/my-private-root-ca.cert.pem -out client/my-private-root-ca.crt

然后您可以使用Web服务器简单地提供该文件。当您单击链接时,应该会询问您是否要安装证书。

为了演示这个过程,您可以尝试安装MIT的证书颁发机构:https://ca.mit.edu/mitca.crt

相关示例


遗憾的是这似乎不够。添加Websockets,然后尝试从Safari访问。即使我提供证书,也会收到一个消息询问是否要安装证书,但安装后安全的Websockets仍然失败。您在使用自签名证书+ Safari iOS方面有过什么好运吗? - gman
@gman:如果你想在Safari中使用它,你需要添加你的根证书。我更新了教程,加入了一个iOS部分。 - coolaj86
如果您需要获取所调用服务器的CA pem,请使用以下命令(输出中的最后一个证书):openssl s_client -showcerts -connect www.example.com:443 </dev/null - John Culviner
1
你的回答非常有价值,非常实用! - anni
@coolaj86,你提到了“Let's Encrypt也可以为私有网络颁发证书”,你有相关文档的链接吗? - Gershom Maes
@GershomMaes 你只需要使用DNS验证。只要DNS可以公开验证,IP地址是私有的就没问题。因此,你可以拥有mylaptop.internal.company.com。 - coolaj86

13

尝试将以下内容添加到您的请求选项中

var options = {
  host: 'localhost',
  port: 8000,
  path: '/api/v1/test',
  // These next three lines
  rejectUnauthorized: false,
  requestCert: true,
  agent: false
};

这个很好用,是本地开发的完美解决方案,但在推送到生产环境时可能需要关闭。 - undefined

3
这个过程允许你创建证书颁发机构和证书:
  1. 下载 ca.cnf 文件以用作配置快捷方式:

    wget https://raw.githubusercontent.com/anders94/https-authorized-clients/master/keys/ca.cnf


  1. 使用以下配置创建新的证书颁发机构:

    openssl req -new -x509 -days 9999 -config ca.cnf -keyout ca-key.pem -out ca-cert.pem


现在我们已经有了证书颁发机构的ca-key.pemca-cert.pem,让我们为服务器生成一个私钥:openssl genrsa -out key.pem 4096
  1. 获取 server.cnf 文件并用作配置快捷方式:

    wget https://raw.githubusercontent.com/anders94/https-authorized-clients/master/keys/server.cnf


使用以下配置生成证书签名请求:
openssl req -new -config server.cnf -key key.pem -out csr.pem
  1. 签署请求:

    openssl x509 -req -extfile server.cnf -days 999 -passin "pass:password" -in csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem

我在这里找到了这个过程,以及有关如何使用这些证书的更多信息。


我可以在生产环境中使用这种方法吗? - Geek Guy

3
您的密钥生成看起来没问题。您不需要CA,因为您没有拒绝未签名请求。
在您的readFileSync方法末尾添加.toString(),这样您实际上传递的是字符串而不是文件对象。

在最近的Node版本中,rejectUnauthorized选项默认设置为true,因此请求被拒绝。当未指定编码时,readFileSync返回一个缓冲区,因此toString是不必要的,并且ca、cert和key选项接受缓冲区或字符串。 - Brett
这是否意味着您将拒绝未签名的请求设置为false并且它正在工作? - binderbound
是的,我可以做到,但这不是我想要的。我正在使用的API已经为我提供了一个CA证书,我想在本地复制这个设置。 - Brett

2
尝试添加:

  agent: false,
  rejectUnauthorized: false

1
这与@Loourr所建议的没有任何区别。 - blueprintchris

0

当您拥有自签名证书时,您可以使用环境变量:NODE_EXTRA_CA_CERTS 告诉 Node.js 使用它。

将所有生成的 *.cert.pem 文件复制到单个文件中。 我将其放在包含所有密钥和证书的目录中:

> (cd $keys; cat *.cert.pem > node_extra_ca_certs)

告诉 Node 去哪里找它们:
> export NODE_EXTRA_CA_CERTS=$keys/node_extra_ca_certs

现在,当您运行Node时,它将接受您的私有证书作为有效证书。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接