加密解密

算法名称

  • RSA、DSA、ECC 非对称加密(公钥加密, 私钥解密)
  • AES、DES 对称加密(密文可通过秘钥还原成原始数据)
  • CRC32、MD5、SHA1 摘要算法(加签)
    • CRC32 Cyclic Redundancy Check,又称循环冗余校验,类似还有CRC64(出现碰撞的概率小),常用于校验网络上传输的文件
    • MD5 Message-Digest Algorithm 5,又叫摘要算法和哈希算法
    • SHA1 Secure Hash Algorithm,又叫安全散列算法
    • 区别
      • CRC的计算效率很高;MD5和SHA1比较慢
      • CRC一般用作通信数据的校验(毕竟效率高适用于通信数据校验)或数据库索引;MD5和SHA1用于安全(Security)领域,比如文件校验、数字签名等
  • 国密算法
算法类型国密算法类似的国际算法
公钥加密SM2RSA、ECC
摘要算法SM3SHA-256
对称加密SM4AES-128
  • 数据特征样例
    • 明文: Hello, 你好
    • Hex(16进制数, 可解密): 48656c6c6f2c20e4bda0e5a5bd
    • Base64(可解密): SGVsbG8sIOS9oOWlvQ==
    • SHA1(无法解密): b14dfdb172b5d329d7dab3454461936e6bc576a8
    • SHA256(无法解密): 5b7068e7c52ee83a61de877e832841c8e39c512353618900f2e0ca3813b67784
    • MD5(16位, 可小写, 无法解密): 3397650EC2C3E0F1
    • MD5(32位, 可小写, 无法解密): A1EDFA063397650EC2C3E0F1DA84CB58
    • AES/DES(假设密钥为123, 可解密, 多次加密结果不一样): U2FsdGVkX19bGwuDdngjwBedkOCu0RLpYyNWmYz5yAQ=
    • URL编码(可解密): Hello,%20%E4%BD%A0%E5%A5%BD

加密相关的概念

  • 对称加密
    • 这是加密文件常用的方式,加密的时候输入一个密码,解密的时候也用这个密码,加密和解密都用同一个密码,所以叫对称加密。常见的算法有AES3DES
  • 非对称加密
    • 非对称加密有两个不一样的密码,一个叫私钥,另一个叫公钥,用其中一个加密的数据只能用另一个密码解开,用自己的都解不了,也就是说用公钥加密的数据只能由私钥解开,反之亦然
    • 私钥一般自己保存,而公钥是公开的,同等加密强度下,非对称加密算法的速度比不上对称加密算法的速度,所以非对称加密一般用于数字签名和密码(对称加密算法的密码)的交换。常见的算法有RSADSAECC
  • 摘要算法
    • 摘要算法不是用来加密的,其输出长度固定,相当于计算数据的指纹主要用来做数据校验,验证数据的完整性和正确性。常见的算法有MD5SHA1SHA256CRC
  • 数字签名
    • 数字签名就是 “非对称加密+摘要算法”,其目的不是为了加密,而是用来防止他人篡改数据
    • 其核心思想是
      • 比如A要给B发送数据,A先用摘要算法得到数据的指纹,然后用A的私钥加密指纹,加密后的指纹就是A的签名,B收到数据和A的签名后,也用同样的摘要算法计算指纹,然后用A公开的公钥解密签名,比较两个指纹,如果相同,说明数据没有被篡改,确实是A发过来的数据
      • 假设C想改A发给B的数据来欺骗B,因为篡改数据后指纹会变,要想跟A的签名里面的指纹一致,就得改签名,但由于没有A的私钥,所以改不了,如果C用自己的私钥生成一个新的签名,B收到数据后用A的公钥根本就解不开

SSL/TLS

介绍

  • SSL(Secure Sockets Layer)和TLS(Transport Layer Security)的关系就像windows XP和windows 7的关系,升级后改了个名字而已
  • TLSv1是建立在SSLv3.0之上的,可以理解成SSLv3.1,中间还有TLSv1.1,目前一般推荐使用TLSv1.2,但是最新版本已近到了TLSv1.3
    • 具体使用得TLS协议版本有客户端优先选择,但是服务器可设置支持的协议版本。像XP系统下的谷歌(v49)就不支持TLSv1.2,但是XP系统下的火狐浏览器是支持的
  • 最初的SSL只支持TCP,现在已经可以支持UDP
  • HTTPS=HTTP+TLSFTPS=FTP+TLS。SSH和SSL/TLS是两个不同的协议,SSH并不依赖于SSL/TLS
  • 测试https访问 https://www.ssllabs.com/ssltest/analyze.html?d=test.aezo.cn

证书概念

  • 私钥:私钥就是一个算法名称加上密码串,自己保存,从不给任何人看
  • 公钥:公钥也是一个算法名称加上密码串,一般不会单独给别人,而是嵌在证书里面一起给别人
  • CA:专门用自己的私钥给别人进行签名的单位或者机构
  • 申请签名文件:在公钥的基础上加上一些申请人的属性信息,比如我是谁,来自哪里,名字叫什么,证书适用于什么场景等的信息。然后带上进行的签名,发给CA(私下安全的方式发送),带上自己签名的目的是为了防止别人篡改文件
  • 证书文件证书由公钥加上描述信息,然后经过私钥签名之后得到。一般都是一个人(一般是CA)的私钥给另一个人的公钥签名;如果是自己的私钥给自己的公钥签名,就叫自签名
  • 签名过程
    • CA收到申请文件后,会走核实流程,确保申请人确实是证书中描述的申请人,防止别人冒充申请者申请证书,核实通过后,会用CA的私钥对申请文件进行签名
    • 签名后的证书包含:申请者的基本信息,CA的基本信息,证书的使用年限,申请人的公钥,签名用到的摘要算法,CA的签名
    • 签完名之后,证书就可以用了
  • 证书找谁签名合适
    • 别人认不认你的证书要看上面签的是谁的名,所以签名一定要找权威的人来签,否则别人不认,哪谁是权威的人呢?那就是CA,哪些CA是受人相信的呢?那就要看软件的配置,配置相信谁就相信谁,比如浏览器、操作系统等,安装好了之后里面就内置了很多信任的CA的证书,只要是那些CA签名的证书,操作系统/浏览器都会相信。而自己写的程序,可以由你自己指定信任的CA(即使用自签名证书);浏览器使用自签名证书时必须将CA证书添加为信任的证书,否则会有警告
  • 那么CA的证书又是谁签的名呢?一般CA都是分级的,CA的证书都是由上一级的CA来签名,而最上一级CA的证书是自签名证书
  • 以浏览器为例,说明证书的验证过程
    • 在TLS握手的过程中,浏览器得到了网站的证书
    • 打开证书,查看是哪个CA签名的这个证书
    • 在自己信任的CA库中,找相应CA的证书
    • 用CA证书里面的公钥解密网站证书上的签名,取出网站证书的校验码(指纹),然后用同样的算法(比如sha256)算出出网站证书的校验码,如果校验码和签名中的校验码对的上,说明这个证书是合法的,且没被人篡改过
    • 读出里面的CN,对于网站的证书,里面一般包含的是域名
    • 检查里面的域名和自己访问网站的域名对不对的上,对的上的话,就说明这个证书确实是颁发给这个网站的
    • 到此为止检查通过
    • 如果浏览器发现证书有问题,一般是证书里面的签名者不是浏览器认为值得信任的CA,浏览器就会给出警告页面,这时候需要谨慎,有可能证书被掉包了。如访问12306网站,由于12306的证书是自己签的名,并且浏览器不认为12306是受信的CA,所以就会给警告,但是一旦把12306的根证书安装到了你的浏览器中,那么下次就不会警告了,因为配置了浏览器让它相信12306是一个受信的CA

TLS握手过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+--------+                                                                                                +--------+
| | 1. ClientHello(发送TLS版本及密码套件/算法) | |
| |----------------------------------------------------------------------------------------------->| |
| | | |
| | 2. ServerHello(确认TLS版本及密码套件) | |
| | 3. Certificate(发送服务器证书) | |
| | 4. ServerKeyExchange (optional. 如DHE_RSA非对称加密算法需要发送一个消息给客户端生成premaster) | |
| | 5. CertificateRequest (optional. 如使用U盾访问银行网站需要) | |
| | 6. ServerHelloDone | |
| |<-----------------------------------------------------------------------------------------------| |
| Client | | Server |
| | 7. Certificate (optional. 发送客户端证书给服务器验证) | |
| | 8. ClientKeyExchange(生成premaster, RSA可直接生成, DHE_RSA需4.ServerKeyExchange中的消息) | |
| | 9. CertificateVerify (optional. 配合7.Certificate, 验证客户端证书对应的私钥确实是在客户端手里) | |
| | 10. Finished | |
| |----------------------------------------------------------------------------------------------->| |
| | | |
| | 11. Finished | |
| |<-----------------------------------------------------------------------------------------------| |
+--------+ +--------+

证书生成示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mkdir cert && cd cert
## CA机构(或者公司内部项目间进行互认可自己创建根证书):生成CA的私钥和证书(openssl参数说明见下文)
# 生成的过程中会要求填一些信息,除了`Common Name`(CN)要取一个容易区分的名字之外,如网站域名(如ca.com),其它都可以随便填写(可参考下文)
# -x509:专用于CA生成自签证书,如果不是自签证书则不需要此项
# -days:证书的有效期限,单位是day(天),默认是365天
openssl req -newkey rsa:2048 -nodes -sha256 -keyout ca.key -x509 -days 365 -out ca.crt

## 普通程序商:生成私钥(aezo.key)和证书签名申请文件(aezo.csr)
# 这里和上面的区别就是这里是-new生成一个证书签名申请文件,而上面用-x509生成一个自签名文件,其它的参数意义都一样,如网站域名(如aezo.cn)
# 可知CA的私钥和普通人的私钥没什么区别,唯一的区别就是CA用私钥自签名的证书受别人相信,而普通人的自签名证书别人不信,所以需要CA来给证书签名
openssl req -newkey rsa:2048 -nodes -sha256 -keyout aezo.key -new -out aezo.csr

## 使用CA的私钥对申请文件进行签名(从而得到证书文件 aezo.crt)
# 由于需要往生成的证书里写入签名者的信息,所以这里需要ca.crt,ca.key里面只有私钥的信息
openssl x509 -CA ca.crt -CAkey ca.key -in aezo.csr -req -days 365 -out aezo.crt -CAcreateserial -sha256
# 将 aezo.key(私钥) 和 aezo.crt (证书) 提交给用户; 部分常用可能还需要提供 ca.crt

## 查看证书内容
# 上面生成的证书文件格式都是pem格式,需通过下列命令查看
openssl x509 -text -noout -in ca.crt
# 可以看到Issuer对应的是ca.com(CA签名信息)
openssl x509 -text -noout -in aezo.crt
  • openssl参数说明
    • -newkey rsa:2048:生成一个长度为2048的采用RSA算法的私钥
    • -nodes:这个私钥在本地存储的时候不加密(可以通过其它参数来加密私钥,这样存储比较安全)
    • -sha256:生成的证书里面使用sha256作为摘要算法
    • -keyout ca.key:输出私钥到ca.key(或者取名key.pem)
    • -x509:证书文件格式为x509,目前TLS默认只支持这种格式的证书
    • -days 365:证书有效期1年
    • -out ca.crt:生成的证书文件保存到ca.crt(或者取名cert.pem)
  • 证书文件
    • 证书的CRT内容:”—–BEGIN CERTIFICATE—–”开头,”—–END CERTIFICATE—–”结尾
    • 证书的私钥内容:”—–BEGIN PRIVATE KEY—–”开头,”—–END PRIVATE KEY—–”结尾
  • 证书生成填写
1
2
3
4
5
6
7
Country Name (2 letter code) [XX]:CN
State or Province Name (full name) []:Shanghai
Locality Name (eg, city) [Default City]:Huangpu
Organization Name (eg, company) [Default Company Ltd]:AEZO
Organizational Unit Name (eg, section) []:JAVA
Common Name (eg, your name or your server's hostname) []:smalle
Email Address []:test@qq.com

HTTPS

  • 证书检测在线工具 可以查看包括二级证书
  • 使用 HTTPS 时,所有的 HTTP 请求和响应数据在发送到网络之前,都要进行加密。网络分层如下

https-net

  • 不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,会有以下风险
    • 窃听风险(eavesdropping):第三方可以获知通信内容
    • 篡改风险(tampering):第三方可以修改通信内容
    • 冒充风险(pretending):第三方可以冒充他人身份参与通信
  • SSL 是个二进制协议,与 HTTP 完全不同,其流量是承载在另一个端口上的(SSL 通常是由端口 443 承载的)
    • 如果URL的方案为http,客户端会打开服务器80端口的连接
    • 如果URL的方案为https,客户端就会打开一条到服务器端口443(默认情况下) 的连接,然后与服务器“握手”,以二进制格式与服务器交换一些 SSL 安全参数, 附上加密的 HTTP 命令
  • 服务器公钥放在服务器的数字证书之中
  • 清除谷歌证书缓存:访问chrome://net-internals/#hsts,在Delete domain security policies中输入域名删除证书,然后重新打开浏览器

Let’s Encrypt免费证书使用

申请Lets证书(基于certbot)

Linux安装和自动更新证书

  • 简单测试(建议参考下文: 结合自动验证DNS脚本进行配置) ^3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
## 安装
## ***建议参考下文: 结合自动验证DNS脚本进行配置***
sudo yum install certbot python3-certbot-nginx # 第二个包为基于nginx自动安装证书的插件, 其他版本如python2-certbot-nginx
certbot -h # 查看帮助
certbot --help all # 查看所有帮助

## 参数说明
run # 获取并安装证书到当前的Web服务器,默认选项,`./certbot run` 和 `./certbot` 效果一致
certonly # 获取或续期证书,但是不安装
renew # 在证书快过期时,续期之前获取的所有证书
-d DOMAINS # 一个证书支持多个域名,用逗号分隔,也可 -d xxx -d yyy

--apache # 使用 Apache 插件来认证和安装证书
--standalone # 运行独立的 web server 来验证,临时在服务器启动一个服务监听80端口
--nginx # 使用 Nginx 插件来认证和安装证书
--webroot # 如果目标服务器已经有 web server 运行且不能关闭,可以通过往服务器的网站根目录放置文件的方式来验证
--manual # 通过交互式方式,或 Shell 脚本手动获取证书。之后需要将证书手动设置到nginx

certificates # 显示从 Certbot 获取的证书的信息
revoke # 撤销证书,certbot revoke --cert-path /path/to/fullchain.pem [options]
delete # 删除证书
register # 创建一个 Let's Encrypt ACME 账户

## (可选)注册Let's Encrypt账号,需要同意协议并设置邮箱。如果此处不注册,可在申请证书的时候设置邮箱
certbot register

## (忽略)手动获取证书(建议参考下文去掉--manual使用自动)
certbot certonly --manual -d test.aezo.cn
# 1.执行后交互命令行会提示需要验证域名,确认生成证书的机器的IP是域名解析到的IP地址(y)
# 2.再确认给定连接可以正常返回再执行下一步。如需要访问 http://test.aezo.cn/.well-known/acme-challenge/0ye4Z1RWQSJmLZxHMGunNknbozOFW2rMNZpz8-ODabQ 返回 abDhqPN_fTSdSsThZoDa1ez6B64LoGXdG5tXAtRLeAc.UTd9IUMKNv88HZklaYypIp4B0Pjpvhv-taL12btuDSs (因此需要在服务器增加此url)
# 3.提示 Congratulations 表示获取证书成功。证书和key均保存在 /etc/letsencrypt/live/test.aezo.cn (共四个文件)
# 4.手动配置nginx证书见下文

## 自动获取并安装证书到nginx.conf,配置时会提示是否自动将http重定向https(输入1/2选择)。如果nginx.conf中有中文可能设置失败
certbot --nginx -d test.aezo.cn -d *.aezo.cn
# 也可基于配置文件自动安装证书(之后命令行输入通过逗号分割需要安装的域名序号)
# 如果nginx配置文件不为/etc/nginx/nginx.conf,则使用--nginx-server-root指定nginx配置文件路径
certbot --nginx --nginx-server-root=/www/server/nginx/conf

## 自动更新脚本
crontab -e
# 在每天凌晨3点运行,该命令将检查服务器上的证书是否将在未来30天内过期,如果是,则进行更新
0 3 * * * /usr/bin/certbot renew --quiet # --quiet 指令告诉 certbot 不要生成输出
# 将在每两个月的凌晨 1:10 执行
# 10 1 * */2 * /usr/bin/certbot renew --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
  • 结合自动验证DNS脚本进行配置通配符证书(阿里云需要AccessKey账号)(2403)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
## 参考:https://www.cnblogs.com/trblog/p/14690908.html
## 将EPEL添加到CentOS 7,并安装snapd(install速度比较慢)
yum install epel-release
yum install snapd
systemctl enable --now snapd.socket
ln -s /var/lib/snapd/snap /snap

## 安装certbot
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot

## 自动验证DNS脚本
# 不管是申请还是续期,只要是通配符证书,只能采用 dns-01 的方式校验申请者的域名,也就是说 certbot 操作者必须手动添加 DNS TXT 记录
# certbot 提供了一个 hook,可以编写一个 Shell 脚本,让脚本调用 DNS 服务商的 API 接口,动态添加 TXT 记录,这样就无需人工干预了
# git clone https://gitee.com/mirrors_ywdblog/certbot-letencrypt-wildcardcertificates-alydns-au.git
cd /opt
git clone https://github.com/ywdblog/certbot-letencrypt-wildcardcertificates-alydns-au # 速度慢可使用上面的仓库
cd certbot-letencrypt-wildcardcertificates-alydns-au
chmod 0777 au.sh
# 修改配置
# 场景的com/cn根域名已经预置在domain.ini中了,如果是其他后缀可添加进去
# 如果是阿里云的域名,则修改 ALY_KEY/ALY_TOKEN 的Token(可使用主账号的AccessKey;或者创建一个子账号,然后增加 AliyunDNSFullAccess 云解析权限,然后使用子账号的AccessKey)
vi au.sh

## 申请证书测试(dry-run)
# 回车命令后,输入邮箱,回车后输入Y,回车后再输入N(不share email)
# manual-auth-hook 和 manual-cleanup-hook 用于添加和删除DNS TXT记录(申请的时候需要添加,获取到证书后,此TXT记录就没啥用了,可删除)
# 更多参数参考:https://github.com/ywdblog/certbot-letencrypt-wildcardcertificates-alydns-au
certbot certonly \
-d example.com \
-d *.example.com \
--manual \
--preferred-challenges dns \
--dry-run \
--manual-auth-hook "/opt/certbot-letencrypt-wildcardcertificates-alydns-au/au.sh python aly add" \
--manual-cleanup-hook "/opt/certbot-letencrypt-wildcardcertificates-alydns-au/au.sh python aly clean" \
--pre-hook "systemctl stop nginx.service" \
--post-hook "systemctl start nginx.service"

## 正式申请证书
# 去掉 --dry-run 参数即可

## 部署证书
# (七牛上传)证书路径在 /etc/letsencrypt/live,复制 fullchain.pem 和 privkey.pem 到普通用户目录,并设置文件普通用户权限,然后下载上传到如七牛即可
# nginx中配置参考下文:手动配置nginx证书

## 增加自动续期参考上文
crontab -e
0 3 * * * /usr/bin/certbot renew --quiet
  • 手动配置nginx证书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
# listen 80;
server_name test.shop.cn;
root /home/www;
index index.html index.htm;

listen 443 ssl; # 一定需要对外开放443端口

ssl_certificate /etc/letsencrypt/live/test.aezo.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/test.aezo.cn/privkey.pem;
}
server {
server_name test.shop.cn;
listen 80;

return 301 https://$host$request_uri;
}
  • 设置到期7天前进行续期(默认是30天), 创建脚本certbot-renew.sh(执行使用certbot-renew.sh example.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash  

# 设定提前续期的天数
DAYS_BEFORE_EXPIRY=7

# 获取当前日期和证书到期日期
EXPIRATION_DATE=$(sudo openssl x509 -in /etc/letsencrypt/live/$1/cert.pem -noout -enddate | cut -d '=' -f 2)
EXPIRATION_TIMESTAMP=$(date -d "$EXPIRATION_DATE" +%s)
CURRENT_TIMESTAMP=$(date +%s)

# 计算剩余时间(天数)
let DIFF=($EXPIRATION_TIMESTAMP-$CURRENT_TIMESTAMP)/86400

# 如果证书将在指定天数内过期,进行续期
if [ "$DIFF" -le "$DAYS_BEFORE_EXPIRY" ]; then
/usr/bin/certbot renew --quiet
fi
  • 测试https访问 https://www.ssllabs.com/ssltest/analyze.html?d=test.aezo.cn
  • 常见错误

Windows安装

  • Certbot 已于 2024 年 2 月停止对 Windows 的支持
  • v2.9.0
  • Windows 10以上基于 WSL

Linux申请Lets证书(基于acme.sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 参考:https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E

## 安装 acme.sh
# 方式一:安装目录:~/.acme.sh/;会自动设置定时任务续签证书
curl https://get.acme.sh | sh -s email=my@example.com
# 方式二:大陆建议手动下载仓库安装(服务器访问github慢)
git clone https://gitee.com/neilpang/acme.sh.git
cd acme.sh
./acme.sh --install -m my@example.com
cd ~/.acme.sh

## 生成证书
# acme.sh 实现了 acme 协议支持的所有验证协议. 一般有两种方式验证: http 和 dns 验证
# http 方式(推荐): 需要在你的网站根目录下放置一个文件, 来验证你的域名所有权, 完成验证
# 手动 dns 方式: 手动在域名上添加一条 txt 解析记录, 验证域名所有权
# 使用http 方式,此命令会自动解析nginx配置,在域名根目录创建验证文件,完成验证后删除验证文件
acme.sh --issue -d example.com --nginx

## copy/安装证书
# 默认生成的证书都放在安装目录下: ~/.acme.sh/
# 可复制到nginx证书目录,或者通过命令进行复制并重启nginx
acme.sh --install-cert -d example.com \
--key-file /path/to/keyfile/in/nginx/key.pem \
--fullchain-file /path/to/fullchain/nginx/cert.pem \
--reloadcmd "service nginx force-reload"

## 查看已安装证书信息
acme.sh --info -d example.com

## 更新证书
# 安装acme.sh后,便会自动创建cronjob,可使用`crontab -l`查看

Windows申请Lets证书(基于win-acme)

  • 基于win-acme,下载win-acme.v2.2.9.1701.x64.trimmed.zip
  • 参考: https://blog.csdn.net/vasing_/article/details/148135054
  • 生成证书操作流程
    • 执行wacs.exe文件
    • Create new cerificate (full options)
    • Manual input
    • 输入host name (即需要认证的域名)
    • 选择默认认证方式
      • [http-01] Save verification files on (network) path 通过路径验证(需提前配置.well-known验证路径,参考下文,此时只能配置 80端口,先不要配置 443端口)
        • 当提示输入 Path: (网站根路径) 时,输入下文 nginx 配置中指定的 root 目录路径 (例如 D:/path/to/newdomain_webroot)
      • serve verification files from memory 通过内存验证服务器(需要在实际服务器上运行上述exe。会先访问Lets自动提交工单,然后Lets会访问配置的域名,会在内存生成一个随机码进行验证。需保证 80 端口不被占用)
      • [dns-01] Create verification records manually (auto-renew not possible) 基于DNS进行验证(可在工作机上运行上述exe。选择后继续往下操作,最后会生成一个DNS解析值;如生成test.aezo.cn的证书,则他需要解析出一个 _acme-challenge.test,类型为TXT,值为随机生成的一串字符;解析好后,稍等片刻等域名解析生效后再继续执行后续步骤进行验证)
    • rsa key
    • pem encoded files(Apache, nginx, etc.)
    • 输入文件存放路径(需提前创建)
      • 最终会生成类似 newdomain.example.com-chain.pem (完整证书链, 对应 ssl_certificate) 和 newdomain.example.com-key.pem (私钥, 对应 ssl_certificate_key)
    • No store steps
    • 输入通知邮箱
    • 输入 y 打开协议文件
    • 输入 y 同意协议
  • 将生成的文件设置到nginx(参考上文手动配置nginx证书)
  • 查看托管的证书
    • 再次执行wacs.exe文件
    • A: Manage renewals (1 total) 可查看托管的自动更新证书
  • 路径验证nginx配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
server_name newdomain.example.com;

# ACME HTTP-01 域名验证处理
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
# win-acme 会在这个目录下创建 .well-known/acme-challenge/ 子目录并写入验证文件
root D:/path/to/newdomain_webroot;
}

# 将所有其他 HTTP 请求永久重定向到 HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}

证书过期导致RestTemplate(SpringBoot)访问接口失败

  • 报错:unable to find valid certification path to requested target
  • 自定义RestTemplate同时支持访问http与https
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Bean
public RestTemplate restTemplate() throws KeyStoreException, NoSuchAlgorithmException {
final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
// 设置过期时间
factory.setConnectionRequestTimeout(5000);
factory.setReadTimeout(5000);
factory.setReadTimeout(5000);

final SSLContextBuilder builder = new SSLContextBuilder();
// 全部信任 不做身份鉴定
builder.loadTrustMaterial(null, (X509Certificate[] x509Certificate, String s) -> true);

// 客户端支持SSLv2Hello,SSLv3,TLSv1,TLSv1
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(builder.build(), new String[]{"SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.2"}, null, NoopHostnameVerifier.INSTANCE);

//为自定义连接器注册http与https
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", new PlainConnectionSocketFactory()).register("https", socketFactory).build();

PoolingHttpClientConnectionManager phccm = new PoolingHttpClientConnectionManager(registry);
phccm.setMaxTotal(500);
final CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).setConnectionManager(phccm).setConnectionManagerShared(true).build();
factory.setHttpClient(httpClient);
final RestTemplate restTemplate = new RestTemplate(factory);
return restTemplate;
}

SM国密算法

  • Java可使用 Bouncy Castle(开源加密库,支持 SM 系列)
  • 国密算法类型
    • SM2:基于椭圆曲线密码学的公钥算法(非对称)
    • SM3:安全散列算法(类似 SHA-256)
    • SM4:对称加密算法(类似 AES)
  • 参考: http://www.yiim.net/gmc/
  • 引入Bouncy Castle
1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;

import java.security.SecureRandom;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;

public class SMUtil {
// SM2算法参数
private static final ECDomainParameters SM2_DOMAIN_PARAMS;

// SM4算法参数(32位, 可进行修改)
private static final byte[] KEY = Hex.decode("01234567890123456789012345678901");
private static final byte[] IV = Hex.decode("00000000000000000000000000000000");

static {
Security.addProvider(new BouncyCastleProvider());
// SM2默认曲线 sm2p256v1
X9ECParameters spec = GMNamedCurves.getByName("sm2p256v1");
SM2_DOMAIN_PARAMS = new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN(), spec.getH());
}

// ========== SM2: 非对称加密/解密 ==========

/**
* 生成SM2密钥对(公钥+私钥),返回十六进制字符串
* @return 包含公钥和私钥的Map
*/
public static Map<String, String> generateKeyPair() {
// 初始化密钥生成器
ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator();
ECKeyGenerationParameters keyGenerationParameters = new ECKeyGenerationParameters(SM2_DOMAIN_PARAMS, new SecureRandom());
keyPairGenerator.init(keyGenerationParameters);

// 生成密钥对
AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair();
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();

// 转换为十六进制字符串
String privateKeyHex = Hex.toHexString(privateKey.getD().toByteArray());
// false表示非压缩格式
String publicKeyHex = Hex.toHexString(publicKey.getQ().getEncoded(false));

Map<String, String> keyMap = new HashMap<>();
keyMap.put("privateKey", privateKeyHex);
keyMap.put("publicKey", publicKeyHex);
return keyMap;
}

/**
* SM2加密
* @param publicKeyHex 公钥(十六进制字符串)
* @param plainText 明文
* @return 加密后的十六进制字符串
*/
public static String encrypt(String publicKeyHex, String plainText) throws InvalidCipherTextException {
// 公钥字节数组
byte[] publicKeyBytes = Hex.decode(publicKeyHex);
// 明文字节数组
byte[] plainTextBytes = plainText.getBytes();

// 解析公钥
ECPoint ecPoint = SM2_DOMAIN_PARAMS.getCurve().decodePoint(publicKeyBytes);
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(ecPoint, SM2_DOMAIN_PARAMS);

// 初始化加密引擎(使用C1C3C2格式)
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));

// 执行加密
byte[] encryptedData = sm2Engine.processBlock(plainTextBytes, 0, plainTextBytes.length);
return Hex.toHexString(encryptedData);
}

/**
* SM2解密
* @param privateKeyHex 私钥(十六进制字符串)
* @param cipherTextHex 加密后字符串(十六进制字符串)
* @return 解密后的明文
*/
public static String decrypt(String privateKeyHex, String cipherTextHex) throws InvalidCipherTextException {
// 私钥字节数组
byte[] privateKeyBytes = Hex.decode(privateKeyHex);
// 密文字节数组
byte[] cipherTextBytes = Hex.decode(cipherTextHex);

// 解析私钥
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(
new java.math.BigInteger(1, privateKeyBytes),
SM2_DOMAIN_PARAMS
);

// 初始化解密引擎(使用C1C3C2格式)
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(false, privateKeyParameters);

// 执行解密
byte[] decryptedData = sm2Engine.processBlock(cipherTextBytes, 0, cipherTextBytes.length);
return new String(decryptedData);
}

// ========== SM3: 摘要算法 ==========
public static String encryptSM3(String input) {
SM3Digest digest = new SM3Digest();
byte[] inputBytes = input.getBytes();
digest.update(inputBytes, 0, inputBytes.length);
byte[] hash = new byte[digest.getDigestSize()];
digest.doFinal(hash, 0);
return Hex.toHexString(hash);
}

// ========== SM4: 对称加密/解密 ==========

public static String encryptSM4(String plainText) throws Exception {
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new SM4Engine()));
cipher.init(true, new ParametersWithIV(new KeyParameter(KEY), IV));
byte[] input = plainText.getBytes("UTF-8");
byte[] output = new byte[cipher.getOutputSize(input.length)];
int len = cipher.processBytes(input, 0, input.length, output, 0);
len += cipher.doFinal(output, len);
return Hex.toHexString(output, 0, len);
}

public static String decryptSM4(String cipherTextHex) throws Exception {
byte[] cipherText = Hex.decode(cipherTextHex);
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new SM4Engine()));
cipher.init(false, new ParametersWithIV(new KeyParameter(KEY), IV));
byte[] output = new byte[cipher.getOutputSize(cipherText.length)];
int len = cipher.processBytes(cipherText, 0, cipherText.length, output, 0);
len += cipher.doFinal(output, len);
return new String(output, 0, len, "UTF-8");
}

// 测试方法
public static void main(String[] args) throws Exception {
testSM2();

testSM3();

testSM4();
}

private static void testSM2() throws Exception {
// 1. 生成密钥对
Map<String, String> keyMap = generateKeyPair();
String publicKey = keyMap.get("publicKey");
String privateKey = keyMap.get("privateKey");
// 如: 04bbdee7df2df02d7d759242a6d943039ec0f8db08839974ba6fde4fb5a70cbf95b7bc57d774202fea06838d357fb27f30911e4c02689ef016cce2daca8996fe7f
System.out.println("生成公钥:" + publicKey);
// 如: 25eaaa2df9e16738df2b3fcf2d1eb44728f7c81d582ba0038511828b4aaf315a
System.out.println("生成私钥:" + privateKey);

// 2. 加密
String plainText = "Hello SM2 国密加密";
String cipherText = encrypt(publicKey, plainText);
// 如: 0459af265d2745b4148db67542c1111cef5c376ac37f975163b51595b3e13ec5c78d4a5584c61efca77153773c7cb9231e04ca73f13341b96da0325f0d4b8e4e95f961d21cb48a760f04ccb72f4261d222ef7b6128410cc181359b10ab24faa31d74dfe2bb6daac6e29dc580dd6658ee1641586b6e799b
System.out.println("SM2加密后:" + cipherText);

// 3. 解密
String decryptedText = decrypt(privateKey, cipherText);
// Hello SM2 国密加密
System.out.println("SM2解密后:" + decryptedText);
}

private static void testSM3() {
String data = "国密SM3测试";
String hash = encryptSM3(data);
// 633cdef3666d0b7dfa1b9d7f5b2dbb4ac2a43d1e06bad8e73b1c9d02aefcfe7a
System.out.println("SM3 Hash: " + hash);
hash = encryptSM3(data);
// 633cdef3666d0b7dfa1b9d7f5b2dbb4ac2a43d1e06bad8e73b1c9d02aefcfe7a
System.out.println("SM3 Hash: " + hash);
}

private static void testSM4() throws Exception {
String plainText = "国密SM4对称加密";
String encrypted = encryptSM4(plainText);
String decrypted = decryptSM4(encrypted);
// 3ad3e57b0fc8985a06e8d0d897896d3f695ac909251bdddd83ef8f74d161ad6c
System.out.println("SM4加密后: " + encrypted);
// 国密SM4对称加密
System.out.println("SM4解密后: " + decrypted);
}
}

AES/DES

简介

  • 密码学中的高级加密标准 (Advanced Encryption Standard,AES),又称高级加密标准Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。高级加密标准已然成为对称密钥加密中最流行的算法之一。该算法为比利时密码学家Joan Daemen和VincentRijmen所设计,结合两位作者的名字,以Rijndael命名之 [1]
  • 加密方式如
    • AES/CBC/NOPadding
    • AES/CBC/PKCS5Padding 128位(16字节),jdk默认支持,建议使用
    • AES/CBC/PKCS7Padding 256位(32字节),jdk默认不支持
  • 说明
    • 类似有 DES/CBC/PKCS5Padding
    • 上述命名意义分别为:AES为算法名称,CBC为加密模式,PKCS5Padding为填充方式(PKCS5Padding是PKCS7Padding在填充块大小为8个字节时的特殊情况,本质上是一样的)
    • 使用CBC模式,需要一个向量iv,可增加加密算法的强度
    • 一般在对内容加密时,需要先将内容进行编码,如Base64。因为,不是所有的字节数组都可以new String(),然后在通过String.getBytes()还原

JAVA 实现

  • java默认不支持PKCS7,如果非要指定PKCS7需要借助BouncyCastle类和安装扩展包

    • BouncyCastle

      1
      2
      3
      4
      5
      6
      7
      <!-- AES/CBC/PKCS7Padding 加解密 -->
      <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
      <dependency>
      <groupId>org.bouncycastle</groupId>
      <artifactId>bcprov-jdk15on</artifactId>
      <version>1.55</version>
      </dependency>
    • 安装扩展包

      • oracle官方下载(JDK1.8)
      • 下载之后得到local_policy.jarUS_export_policy.jar两个jar包,把这两个jar包放到jre/lib/security目录下替换原来的两个jar包即可
      • 如果是128位(16字节)则无需安装扩展包
  • 示例(基于jdk1.8测试)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import org.apache.tomcat.util.codec.binary.Base64;
import sun.misc.BASE64Decoder;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.spec.AlgorithmParameterSpec;

public class AesU {
public static void main(String args[]) throws Exception {
System.out.println(encrypt("aezo.cn")); // U7fKj3r+hCydAkG20p0ZOw==
System.out.println(decrypt("U7fKj3r+hCydAkG20p0ZOw==")); // aezo.cn
System.out.println(decrypt("7gerc9kKbi7d7/rskLzq/H/+9Zb9lqa/XhiWkgeaThw=")); // aezo.cn
System.out.println(encrypt2("aezo.cn")); // 7gerc9kKbi7d7/rskLzq/H/+9Zb9lqa/XhiWkgeaThw=
System.out.println(decrypt2("U7fKj3r+hCydAkG20p0ZOw==")); // aezo.cn
}

private static final String CHARSET_NAME = "UTF-8";
private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; // "AES/CBC/PKCS7Padding"
private static final String KEY = "aabcw334^#^&#*^$W1233qwreqwr12 "; // 秘钥 32字节
private static final String IV = "abc8j*Ghg7!rNI84"; // 偏移 16字节

// static {
// // 使用"AES/CBC/PKCS7Padding"时需要开启
// Security.addProvider(new BouncyCastleProvider());
// }

/**
* 加密
* @param content
* @return
* @throws Exception
*/
public static String encrypt(String content) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(CHARSET_NAME), "AES");
AlgorithmParameterSpec paramSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, paramSpec);
byte[] result = cipher.doFinal(content.getBytes(CHARSET_NAME));
return Base64.encodeBase64String(result);
}

/**
* 解密
* @param content
* @return
* @throws Exception
*/
public static String decrypt(String content) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(CHARSET_NAME), "AES");
AlgorithmParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return new String(cipher.doFinal(Base64.decodeBase64(content)), CHARSET_NAME);
}

/**
* AES加密+BASE64
* @param content
* @return
* @throws Exception
*/
public static String encrypt2(String content) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
int blockSize = cipher.getBlockSize();

byte[] dataBytes = content.getBytes();
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}

byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);

SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());

cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(plaintext);

return new sun.misc.BASE64Encoder().encode(encrypted);
}

/**
* 解密
* @param content
* @return
* @throws Exception
*/
public static String decrypt2(String content) throws Exception {
byte[] encrypted = new BASE64Decoder().decodeBuffer(content);

Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());

cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

byte[] original = cipher.doFinal(encrypted);
return new String(original);
}
}

JS 实现

SHA1

1
2
3
4
5
6
7
8
9
10
11
public static String sha1(String str)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
if (null == str || str.length() == 0) {
return null;
}
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));
byte[] md = mdTemp.digest();

return "{SHA}" + Utf8.decode(java.util.Base64.getEncoder().encode(md));
}

Hex(16进制)

1
2
3
4
5
6
7
8
9
10
11
import org.bouncycastle.util.encoders.Hex;

public static void main(String[] args) {
String str = "Hello, 你好";
String hex = Hex.toHexString(str.getBytes(StandardCharsets.UTF_8));
// 48656c6c6f2c20e4bda0e5a5bd
System.out.println("hex = " + hex);
String plain = new String(Hex.decode(hex), StandardCharsets.UTF_8);
// Hello, 你好
System.out.println("plain = " + plain);
}

参考文章

ChatGPT开源小程序