起因
前不久刚配了新电脑,想在新电脑上继续开发之前电脑上没有写完的项目,结果拉下来一 npm install 之后就:
我就觉得很奇怪,明明是一样的工程,一样的 package.json,一样的 package-lock.json,怎么换了一台电脑和一个网络环境就错误了?
于是我开始想办法解决这个问题。
猜测 1:网络环境出了问题
已知:
- 我之前开发的时候在另一个城市,现在在家,网络运营商都不是同一个地方,有可能是运营商的解析有问题
- 可能是我的代理设置有问题,导致出现了一些奇怪的错误
所以接下来我做了两件事:
- 搜索我的代理是否会中途修改 https 协议的头或者内容,但答案是不会,搜了很多也没有相关案例,所以这个思路应该是不对的。
- 拜托朋友在他的机子上试着 npm install 一下,结果存在同样的问题,看来并不是网络环境导致的问题
猜测 2:根据错误信息,可能是证书的问题
可以看到报错的内容有 UNABLE_TO_VERIFY_LEAF_SIGNATURE
字样,在搜索引擎上搜搜关键词后,找到这么几个在 stackOverflow 上的回答:
node.js – Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE Phonegap Installation – Stack Overflow
在我按照回答的指示做之后,就可以正常 install 成功了。
但我很疑惑这是为什么,我想知道更深层的原因,而不止于此,所以我就看了一下这个是用来干嘛的,在 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 一样,无法验证这个站点的证书。
这时候我就觉得奇怪了,我决定用自己的本站点来尝试一下。
然后,我傻眼了:
为什么我自己的证书也没法验证???我不是已经配置了 SSL 证书了吗??还是说命令行在整我??
可是,我在通过浏览器访问的时候一直都没有提示我的证书有问题啊??(不然也不会这么久都没发现了)
看来我的站点和 npm install 的这个目标站点都有同样的表现:
- 通过浏览器可以正常访问,成功验证证书
- 用命令行工具或者脚本则无法正常访问,提示无法验证站点证书。
很有可能它们遇到的是同一个问题。
解谜篇
其实在刚才验证思路的过程中,我找到了这么一个答案:
node.js – Error: unable to verify the first certificate in nodejs – Stack Overflow
不过一开始我觉得看不懂,而且我也不太懂这个所谓的 Certificate chain 是什么东西,就没怎么看,但在后面的搜索和尝试过程中,我发现似乎问题就在这里。
在仔细查看资料之后,我终于搞懂了证书验证失败的原因:
证书链不完整。
证书链
什么是证书链呢?首先可能得打个比方。
一般来说,朋友的朋友会比陌生人可靠,因为朋友提供了可信度。证书也是一样的。
假设有个究极阳角现充叫大斌,是所有人的好朋友,大家都信任他,那么他介绍的人,大家也一样信任。
然后某一天,你遇到了一个陌生人,但你不知道这个陌生人是否可信,于是问他是谁介绍来的。这个陌生人说他是小杨介绍来的,然后你又去问小杨是谁介绍来的,小杨说是大斌介绍来的,因为你信任大斌,所以你就认为这个陌生人也是可信的。
然后第二天你又遇到了一个新的陌生人,问他是谁介绍来的,他说是小刘介绍来的,然后你又问小刘是谁介绍来的,小刘说是小冰介绍来的,但是你找不到小冰是谁介绍来的,所以你选择不信任这个陌生人。
证书链也是这样的,根证书就是这个大家都无条件信任的人,根证书自己给自己保证可信,也给其他人提供凭证。
当你访问一个站点的时候,会先去看是哪个机构给予这个证书凭证,然后再去找这个机构的证书的发行机构,再往上找它的发行机构……一直到找到根证书为止。因为根证书大家都信任,所以客户端也就信任了这个站点。
但是,如果一直往上找,最终没有找到根证书发行机构,而是在中间发行机构断掉了,则证书链不完整,这个站点的证书无法被正确验证,请求也会变成不安全的,在对安全比较严格的情况下请求就会失败。
👆 这个网站可以查询目标站点的证书链是否完整。一般来说,生成网页证书的时候,会有两种,一种是只包含该站点证书的证书,另一种则是完整包含了从该站点到根证书的证书链证书(一般会被叫做 fullchain)。如果只使用前者,则会有证书链断掉的问题。
然后我又试了一下我的站点:
至此,问题的根本原因就找到了。
等等,不是还有个问题么?为什么浏览器可以正确验证证书,而命令行工具不行?
有关这个问题我也搜了一下,提到的是有两种方式
- 浏览器在访问站点时会自动缓存中间证书,所以可以直接使用缓存。
- 浏览器会自动尝试在可信渠道内搜索中间证书,并自动补全证书链。
而这两点似乎并不是命令行工具的内置功能,所以命令行工具一般是不会自动自动补全证书链的。
但是有关浏览器的这两个说法,相关的资料太少,也可能是我搜索能力不足,没有找到比较权威的资料证明这两个观点,姑且当作直觉上容易接受的解释吧,等以后找到了比较权威的资料之后我再补上。
修复
既然这是服务端的问题,那 npm 指向的那个站点我是没有办法了,但是我自己的站点还是可以修修的。
我的站点证书用的是 acme.sh,GitHub:acmesh-official/acme.sh: A pure Unix shell script implementing ACME client protocol (github.com)
在生成证书的时候,会同时生成一个 fullchain.cer,在 nginx 配置中只要指定这个为证书就行了。
目前已修复完毕。
如果你的证书提供商没有给你提供 fullchain 证书,也没有关系,因为一般来说机构的证书都是公开的,网上也有不少的证书链补全工具。只要把你的证书扔进这些在线工具中,就能得到对应的 fullchain 证书了!
👆 这是我随便在搜索引擎找的一个工具,可以试试,虽然我没试过(
后记
在开始写本文之前,我发现 npm install 又恢复正常了,看了一下 log 是因为 install 的源不指向之前那个站点了,直至本文完成,那个站点的证书链依旧没有修复。
至于为啥 npm install 会指向那个源,我也不知道,而且看了一下那个源似乎还是国内的源,但我的 npm registry 是 https://registry.npmjs.org/ ,这就很奇怪了,难道自动判断了 ip 地址然后使用了国内 cdn 加速么?不懂。
总之现在可以正常 npm install 了,可喜可贺,可喜可贺。
npm:install?你配吗?
我:现在我配了。