由 npm install 失败引出来的证书问题

起因

前不久刚配了新电脑,想在新电脑上继续开发之前电脑上没有写完的项目,结果拉下来一 npm install 之后就:

npm install 失败
对应的日志

我就觉得很奇怪,明明是一样的工程,一样的 package.json,一样的 package-lock.json,怎么换了一台电脑和一个网络环境就错误了?

于是我开始想办法解决这个问题。

猜测 1:网络环境出了问题

已知:

  1. 我之前开发的时候在另一个城市,现在在家,网络运营商都不是同一个地方,有可能是运营商的解析有问题
  2. 可能是我的代理设置有问题,导致出现了一些奇怪的错误

所以接下来我做了两件事:

  1. 搜索我的代理是否会中途修改 https 协议的头或者内容,但答案是不会,搜了很多也没有相关案例,所以这个思路应该是不对的。
  2. 拜托朋友在他的机子上试着 npm install 一下,结果存在同样的问题,看来并不是网络环境导致的问题

猜测 2:根据错误信息,可能是证书的问题

可以看到报错的内容有 UNABLE_TO_VERIFY_LEAF_SIGNATURE 字样,在搜索引擎上搜搜关键词后,找到这么几个在 stackOverflow 上的回答:

node.js – Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE Phonegap Installation – Stack Overflow

在我按照回答的指示做之后,就可以正常 install 成功了。

但我很疑惑这是为什么,我想知道更深层的原因,而不止于此,所以我就看了一下这个是用来干嘛的,在 npm 官方文档中可以看到:

npm 官方文档中对这项配置的描述

按照我的理解来看,默认是 true,会验证目标站点证书的合法性,如果设置为 false,就不会验证了,只要有证书就行。

既然我设置为 false 就可以正常 install,那是不是意味着我一开始拉取的站点,其证书并不合法?

尝试

浏览器

首先,我尝试用浏览器直接打开这个证书有问题的页面,想看看浏览器会不会进行提示。一般来说,如果证书过期,或者证书的站点和目标站点对不上的话,浏览器地址栏旁边的那个锁则会变红,并提示该站点不安全。

当浏览器访问一个证书站点与目标站点不匹配的网站

但是在我尝试之后,发现浏览器并没有提示任何问题,而是正常地进入了该网站之中。

说明浏览器成功验证了该站点的证书

那既然如此,为什么在 npm install 的时候会提示无法验证这个站点的证书呢?

使用 node 和 python 脚本拉取

我尝试直接使用 node 的 fetch API 对 npm 指向的 url 进行拉取:

fetch('https://r2.cnpmjs.org/cors/-/cors-2.8.5.tgz')
    .then(res => {
        console.log(res)
    })
    .catch(err => {
        console.error(err)
    })

结果和 npm 一样,提示无法验证这个站点的证书。

我不信邪,又用 python 拉取了一遍:

import requests as req

res = req.get('https://r2.cnpmjs.org/cors/-/cors-2.8.5.tgz')

结果和 node 一样,无法验证这个站点的证书。

这时候我就觉得奇怪了,我决定用自己的本站点来尝试一下。

然后,我傻眼了:

python 结果如图,node 结果也是一样的,就不发了

为什么我自己的证书也没法验证???我不是已经配置了 SSL 证书了吗??还是说命令行在整我??

可是,我在通过浏览器访问的时候一直都没有提示我的证书有问题啊??(不然也不会这么久都没发现了)

看来我的站点和 npm install 的这个目标站点都有同样的表现:

  1. 通过浏览器可以正常访问,成功验证证书
  2. 用命令行工具或者脚本则无法正常访问,提示无法验证站点证书。

很有可能它们遇到的是同一个问题。

解谜篇

其实在刚才验证思路的过程中,我找到了这么一个答案:

node.js – Error: unable to verify the first certificate in nodejs – Stack Overflow

不过一开始我觉得看不懂,而且我也不太懂这个所谓的 Certificate chain 是什么东西,就没怎么看,但在后面的搜索和尝试过程中,我发现似乎问题就在这里。

在仔细查看资料之后,我终于搞懂了证书验证失败的原因:

证书链不完整。

证书链

什么是证书链呢?首先可能得打个比方。

一般来说,朋友的朋友会比陌生人可靠,因为朋友提供了可信度。证书也是一样的。

假设有个究极阳角现充叫大斌,是所有人的好朋友,大家都信任他,那么他介绍的人,大家也一样信任。

然后某一天,你遇到了一个陌生人,但你不知道这个陌生人是否可信,于是问他是谁介绍来的。这个陌生人说他是小杨介绍来的,然后你又去问小杨是谁介绍来的,小杨说是大斌介绍来的,因为你信任大斌,所以你就认为这个陌生人也是可信的。

然后第二天你又遇到了一个新的陌生人,问他是谁介绍来的,他说是小刘介绍来的,然后你又问小刘是谁介绍来的,小刘说是小冰介绍来的,但是你找不到小冰是谁介绍来的,所以你选择不信任这个陌生人。

证书链也是这样的,根证书就是这个大家都无条件信任的人,根证书自己给自己保证可信,也给其他人提供凭证。

当你访问一个站点的时候,会先去看是哪个机构给予这个证书凭证,然后再去找这个机构的证书的发行机构,再往上找它的发行机构……一直到找到根证书为止。因为根证书大家都信任,所以客户端也就信任了这个站点。

但是,如果一直往上找,最终没有找到根证书发行机构,而是在中间发行机构断掉了,则证书链不完整,这个站点的证书无法被正确验证,请求也会变成不安全的,在对安全比较严格的情况下请求就会失败。

画了个示意图,大家凑合着看

SSL Checker (sslshopper.com)

👆 这个网站可以查询目标站点的证书链是否完整。一般来说,生成网页证书的时候,会有两种,一种是只包含该站点证书的证书,另一种则是完整包含了从该站点到根证书的证书链证书(一般会被叫做 fullchain)。如果只使用前者,则会有证书链断掉的问题。

可以看到,这个站点的证书链是断掉的

然后我又试了一下我的站点:

可以看到,我的站点证书链也是断掉的

至此,问题的根本原因就找到了。

等等,不是还有个问题么?为什么浏览器可以正确验证证书,而命令行工具不行?

有关这个问题我也搜了一下,提到的是有两种方式

  1. 浏览器在访问站点时会自动缓存中间证书,所以可以直接使用缓存。
  2. 浏览器会自动尝试在可信渠道内搜索中间证书,并自动补全证书链。

而这两点似乎并不是命令行工具的内置功能,所以命令行工具一般是不会自动自动补全证书链的。

但是有关浏览器的这两个说法,相关的资料太少,也可能是我搜索能力不足,没有找到比较权威的资料证明这两个观点,姑且当作直觉上容易接受的解释吧,等以后找到了比较权威的资料之后我再补上。

修复

既然这是服务端的问题,那 npm 指向的那个站点我是没有办法了,但是我自己的站点还是可以修修的。

我的站点证书用的是 acme.sh,GitHub:acmesh-official/acme.sh: A pure Unix shell script implementing ACME client protocol (github.com)

在生成证书的时候,会同时生成一个 fullchain.cer,在 nginx 配置中只要指定这个为证书就行了。

目前已修复完毕。

完整的证书链应该长这样

如果你的证书提供商没有给你提供 fullchain 证书,也没有关系,因为一般来说机构的证书都是公开的,网上也有不少的证书链补全工具。只要把你的证书扔进这些在线工具中,就能得到对应的 fullchain 证书了!

证书链下载/证书链修复 (myssl.com)

👆 这是我随便在搜索引擎找的一个工具,可以试试,虽然我没试过(

如果谁有心的话也可以去提醒一下这个站点的站长让他补全证书链(

后记

在开始写本文之前,我发现 npm install 又恢复正常了,看了一下 log 是因为 install 的源不指向之前那个站点了,直至本文完成,那个站点的证书链依旧没有修复。

至于为啥 npm install 会指向那个源,我也不知道,而且看了一下那个源似乎还是国内的源,但我的 npm registry 是 https://registry.npmjs.org/ ,这就很奇怪了,难道自动判断了 ip 地址然后使用了国内 cdn 加速么?不懂。

总之现在可以正常 npm install 了,可喜可贺,可喜可贺。

npm:install?你配吗?

我:现在我配了。

作者: 梁小顺

脑子不太好用的普通人。 顺带一提性格也有点古怪。 在老妈子和厌世肥宅中来回切换。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据