升级博客到HTTPS

最近一直在搞flutter,遇见了使用flutter_webview_plugin支持webview的需求,打算把博客链接放上去,后来发现配置的网页URL必须是https的才行,由于之前站点一直没有升级https,因此需要首先解决这个问题。

下面是为站点申请免费的https证书的流程整理,实际操作之后,对于https的原理有了更清楚的认识。

<!--more-->

1. 升级流程

整个操作使用 Let’s Encrypt 提供的免费证书,然后下载到服务器上,最后,在nginx配置好证书路径就可以了。参考https://qii404.me/2017/12/05/https-your-site.html

1.1. 具体步骤

先安装脚本

git clone https://github.com/certbot/certbot
cd certbot
chmod +x certbot-auto

# certbot-auto 即为自动化脚本工具, 他会判断你的服务是nginx还是apache, 然后执行对应逻辑
./certbot-auto --help

然后执行自动命令

./certbot-auto certonly --webroot --agree-tos -v -t --email 645234650@qq.com -w /usr/share/nginx/html -d www.shymean.com

其中的参数含义

  • --email对应邮箱,会往该邮箱发送验证链接,貌似没啥用
  • -w 站点根目录,后续脚本会在该目录创建验证文件,并进行http访问
  • -d 需要加密的域名

正常成功的情况下,脚本完成后会提示

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/www.shymean.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/www.shymean.com/privkey.pem
   Your cert will expire on 2019-04-03. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot-auto
   again. To non-interactively renew *all* of your certificates, run
   "certbot-auto renew"

可以看见对应的密钥文件路径,然后配置nginx支持密钥即可

server {
    listen 443;
    server_name www.shymean.com

    ssl on;
    ssl_certificate /etc/letsencrypt/live/www.shymean.com/fullchain.pem
    ssl_certificate_key /etc/letsencrypt/live/www.shymean.com/privkey.pem
}

配置修改后sudo nginx -s reload重启,然后重新使用chrome访问博客链接https://shymean.com,就可以看见地址栏的小锁安全标识了。

1.2. 遇见的问题

环境报错

运行脚本时

OSError: Command /opt/eff.org/certbot/venv/bin/python2.7 - setuptools pkg_resources pip wheel failed with error code 1

参考issue,解决方案

sudo apt-get install letsencrypt

export LC_ALL="en_US.UTF-8"
export LC_CTYPE="en_US.UTF-8"

验证失败

The following errors were reported by the server: Domain: shymean.com Type: unauthorized Detail: Invalid response from http://shymean.com/.well-known/acme-challenge/WLMrgPQzIxK6OWn6yyIpUM_eEYNKI6ZtUL7OBb9LyTs [45.77.153.232]: 500

原因是我的后台使用koa实现的自定义路由,文件系统访问需要单独访问静态资源目录,需要重新配置目录,并生成.well-known/acme-challenge目录后成功

生成失败超过限制

第二个错误导致重试了很多次,域名生成频繁导致进入风控。将域名从shymean.com替换成www.shymean.com解决。参考 https://letsencrypt.org/docs/rate-limits/。

证书三个月后失效

需要手动renew证书,参考:如何为Let’s Encrypt颁发的SSL证书续期

cd ~/certbot
# 在某些时候提示 Couldn't download xxx,这是因为自动升级下载的依赖包时报了,可以添加--no-self-upgrade后缀
./letsencrypt-auto renew --email 645234650@qq.com --agree-tos --no-self-upgrade

注意在续期的时候需要重新验证域名,因此需要保证跟安装证书时相同的校验环境(我之前在申请证书时直接修改了nginx的配置,将其指向了一个包含校验文件的静态资源目录,因此续期时同样需要修改nginx配置,将www.shymean.com这个域名指向该校验目录)

续期之后,记得改回nginx配置,然后重启nginx,再次访问即可。

2. 理解https

上面的过程中存在了三个概念

  • 证书发布机构 letsencrypt
  • 服务器公钥 /etc/letsencrypt/live/www.shymean.com/fullchain.pem
  • 服务器私钥 /etc/letsencrypt/live/www.shymean.com/privkey.pem

下面整理一下现在访问https://www.shymean.com的加密流程,了解https是如何工作的

2.1. 握手阶段

下面梳理了握手阶段的流程,在这个过程中,我们可以看见证书发布机构、服务器公钥、服务器私钥三个概念的作用。

  • 客户端访问服务器,会将自己支持的加密算法,告知服务器
  • 然后服务器会返回
    • 证书,证书包含服务器的身份信息,包括网站地址、公钥、证书颁发机构
    • 客户端支持的一种对称加密算法C,作为后续报文包的加密
    • 客户端支持的一种hash算法
  • 客户端收到服务器的响应
    • 验证证书是否由权威的机构发布,如果证书合法或者客户端信任了不合法的证书,则进行下一步
    • 客户端随机产生一串序列号S,使用证书里面的公钥进行加密,生成一条消息M;再使用服务端返回的hash算法,以及随机序列号S进行加密,生成一条消息验证值V
    • 将消息和消息验证值返回给服务器
  • 服务器接收到客户端发送的消息M和消息验证值V
    • 使用服务器的私钥解密上述公钥加密的消息M,得到随机序列号S
    • 使用服务器选择的hash算法,对S进行加密,判断得到的结果与客户端发送的消息验证值V是否一致,一致则说明消息未被篡改,随机序列号S可以信任
    • 最后,使用该随机序列号S,加上之前选择的对称加密算法C,加密一段握手消息T,该消息包含握手结束的信息,返回给客户端。
  • 客户端收到响应后,使用选择的对称加密算法C,以及随机序列号S进行解密,判断握手是否结束

2.2. 对称加解密传输阶段

有对称算法的效率对比起对称算法来说,要低得多得多;因此非对称加密往往只用在HTTPS的握手阶段,用于确保对称加密密钥的可靠性。

由于私钥仅保存在服务器端,其他中间层都无法获得密钥S,而伪造的S通过hash算法可以进行验证,从而保证服务端得到的是可信任的S。

握手结束后,客户端和服务器端使用握手阶段产生的随机数S作为对称加密算法的密钥,然后挑选出来的对称加密算法C进行对称加解密的传输

2.3. 握手阶段伪代码

可见上述三个概念的作用

  • 证书发布机构,判断证书是否合法
  • 公钥用于客户端加密对称加密算法的密钥S,私钥服务端解密S,保证S的可靠性

下面对握手阶段的整个流程进行了伪代码的梳理


// 客户端请求
var 客户端支持的算法 = client.获取支持的加密和hash算法();
client.发送(客户端支持的算法);


// 服务端发送证书
var { 客户端支持的算法} = server.解析响应()
var { 对称加密算法, hash算法 } = server.选择一种客户端支持的加密算法(客户端支持的算法);
var 证书 = server.获取证书(发证机构, 公钥);
server.发送(证书, hash算法, 对称加密算法);

// 客户端收到证书
var { 证书, hash算法, 对称加密算法 } = client.解析响应();
if(client.检测证书合法性(证书)){
    var {公钥} = client.获取证书公钥(证书)
    var 对称算法密钥 = client.生成对称算法密码(random())

    var 消息M = client.使用公钥加密密钥(公钥, 对称算法密钥)
    var 消息验证值V = client.生成hash验证值(hash算法, 对称算法密钥);

    client.发送(消息M, 消息验证值V)
}else {
    连接失败;
}

// 服务端收到消息和验证值
var { 消息M, 消息验证值V } = server.解析响应();
var 解密结果R = server.使用私钥解密(消息M)
var 消息验证值V1 = server.生成hash验证值(hash算法, 解密结果R);
if (消息验证值V1 == 消息验证值V){
    var 对称算法密钥 = 解密结果R; 

    var 握手终止报文 = server.生成报文(对称加密算法, 对称算法密钥, "握手终止消息");
    server.发送(握手终止报文);
}else {
    客户端发送的对称算法密钥 != 服务端收到的对称算法密钥
    连接失败
}

// 客户端接收到握手终止消息
var { 报文 } = client.解析响应();
let { 握手终止报文 } = client.解析报文(对称加密算法, 对称算法密钥, 报文);
if (握手终止报文){
    对称算法密钥传输成功
    开始对称加解密传输
}else {
    对称算法使用对称算法密钥解密失败
}

握手阶段的主要功能,就是确定后续通讯的对称加密算法和对应对称算法的密钥,当服务器和客户端都有了对称加密算法和对称加密密钥S时,就可以进行对称加解密之间的传输了。

3. 小结

之前知道https的大致原理,却一直没有动手去把站点升级到https,主要的原因就是觉得目前博客不涉及私密信息,没有升级https的必要性。此外由于服务器本身性能较差,访问也比较少,因此就此耽搁了。

但是从动手升级后重新了解到https的原理这件事可以看见,很多东西,还是需要多动手去尝试才行,纸上谈兵是大忌~