公众号关注 「奇妙的 Linux 世界」
设为「星标」,每天带你玩转 Linux !
Synopsis
本文档会介绍如何用 cfssl 从零构建起一套 PKI,包括签发根证书(rootCA)、中间证书和叶子证书,以及通过 CRL 实现证书吊销列表。
Background
1、证书编码格式
PEM(Privacy Enhanced Mail)
通常用于数字证书认证机构(Certificate Authorities,CA)
,扩展名为.pem
, .crt
, .cer
, 和 .key
。 内容为 Base64 编码的 ASCII 码文件,有类似"-----BEGIN CERTIFICATE-----"
和 "-----END CERTIFICATE-----"
的头尾标记。 服务器认证证书,中级认证证书和私钥都习惯储存为 PEM 格式。Apache 和 nginx 等类似的服务器使用 PEM 格式证书。
DER(Distinguished Encoding Rules)
与 PEM 不同之处在于其使用二进制而不是 Base64 编码的 ASCII。扩展名为.der
,但也经常使用.cer
用作扩展名。
Install
Go >= 1.18 的话可以直接安装(其他安装方式可以参考官方 repo)
go install github.com/cloudflare/cfssl/cmd/...@latest
初始化配置
cfssl print-defaults config > config.json
会生成默认配置文件 config.json
,按照自己的需求修改一下:
(更多可参考官方代码库)
{
"signing": {
"default": {
"expiry": "87600h",
"crl_url": "https://s3.laisky.com/public/laisky.crl"
},
"profiles": {
"leaf": {
"expiry": "87600h",
"usages": ["signing", "key encipherment", "server auth"]
},
"intermediate": {
"expiry": "87600h",
"usages": ["cert sign", "crl sign"],
"ca_constraint": {
"is_ca": true
}
}
}
}
}
其中 profiles
可以包含多个配置:
intermediate
:表示中间 CA,用于签发下一级 CA 或叶子证书,一般不用于加密leaf
:叶子证书,仅用于加密
签发根证书
# 生成 csr.json 模版
cfssl print-defaults csr > csr.json
稍微修改一下 csr.json
:
(最重要的是 CN
、 hosts
和 key
)
{
"CN": "laisky.com",
"hosts": ["laisky.com", "*.laisky.com"],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"ST": "SH",
"L": "Shanghai",
"O": "Laisky"
}
],
"ca": {
"expiry": "87600h"
}
}
相关字段说明见 Creating a new CSR:
CN
: commonName,一般用作用户名(一般应唯一)O
: organizationName,一般用作用户组名OU
: organisationalUnit,C
: country,国家L
: localityName, 城市或城镇名ST
: stateOrProvinceName, 州/省 名STREET
: streetAddressDC
: domainComponent
签发根证书:
cfssl genkey -initca csr.json | cfssljson -bare ca
会生成三个文件:
ca-key.pem: 私钥
ca.csr: 生成证书用的 CSR 文件
ca.pem: 证书
ℹ️ 生成证书的流程为:
生成随机私钥
编写 CSR 文件
用私钥签发 CSR,生成证书
用 rootCA 签发次级 CA
建立一个 l2
文件夹,来放置二级证书相关的文件。
mkdir l2
cd l2
ℹ️ 证书(certificate)分为普通证书和 CA 证书。 CA 证书可以继续用来签发下一级证书,而普通证书只能用来协商加密。
具体看
X509v3 extensions:
中的如下字段:
X509v3 Key Usage:
CA 证书:
Certificate Sign, CRL Sign
普通证书:
Digital Signature, Key Encipherment
X509v3 Basic Constraints:
CA 证书:
CA:TRUE
普通证书:
CA:FALSE
首先准备好 csr.json
:
{
"CN": "l2.laisky.com",
"hosts": ["l2.laisky.com", "*.l2.laisky.com"],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"ST": "SH",
"L": "Shanghai",
"O": "Laisky"
}
]
}
cfssl 签发中间证书的流程和传统的 openssl 操作略有不同:
传统方式是会生成一个 CA 证书的 CSR,交给上一级 CA 签署
cfssl 则是生成一个普通 CSR,上一级 CA 根据
config.json
中的配置生成 CA 或叶子证书。(如果有特殊需求,也可以通过genkey
带上-initca
参数生成 CA CSR)
# 生成私钥和 CSR
cfssl genkey -config ../config.json -profile intermediate csr.json | cfssljson -bare
# 如果已经有私钥,也可以只生成 CSR
cfssl gencsr -key cert-key.pem csr.json | cfssljson -bare
会生成三个文件:
cert-key.pem
:私钥cert.csr
:证书 CSR
查看 CSR 内容:
(其中没有 CA:TRUE
,说明其是一个申请叶子证书的普通 CSR,不过没关系,cfssl 会在实际签发的时候根据配置文件来判断)
✗ openssl req -text -noout -verify -in cert.csr
# 也可用 cfssl certinfo -csr cert.csr
✗ openssl req -text -noout -verify -in cert.csr
Certificate request self-signature verify OK
Certificate Request:
Data:
Version: 1 (0x0)
Subject: C = CN, ST = SH, L = Shanghai, O = Laisky, CN = l2.laisky.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:db:54:d1:ba:e6:10:86:f0:84:1d:27:aa:46:75:
99:c3:09:33:03:65:4b:1e:24:bc:85:34:3a:4c:d4:
b7:84:f3:0c:a3:dd:41:27:4a:34:e0:d4:a0:1a:a8:
cf:a3:cf:a2:12:31:12:0b:78:f1:d4:da:5c:fc:5e:
9b:03:bb:c4:b2
ASN1 OID: prime256v1
NIST CURVE: P-256
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:l2.laisky.com, DNS:*.l2.laisky.com
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:46:02:21:00:bd:0c:a6:fd:7d:da:6e:87:8a:cf:7a:77:b3:
41:9e:e0:4d:a0:cf:0e:39:0a:f1:48:95:5a:57:c0:bb:13:f3:
37:02:21:00:81:aa:a7:a2:da:01:0e:96:48:ff:d6:17:05:f9:
aa:e9:59:56:c6:ff:3b:0f:ce:4a:f1:b6:4f:77:9c:41:f6:0a
ℹ️ x509 证书链:
次级 CA,生成自己私钥
次级 CA,将想要生成的证书内容写入 csr,交给上一级(本例中为 rootCA)
上一级通过自己的私钥、CA 按照 csr 内容签发下一级的证书
由 rootCA 签发证书:
cfssl sign \
-ca ../ca.pem \
-ca-key ../ca-key.pem \
-config ../config.json \
-profile intermediate \
-csr cert.csr | cfssljson -bare
会生成 cert.pem
证书文件,可以用 openssl 查看其内容:
(注意 CA:TRUE
)
✗ openssl x509 -in cert.pem -noout -text
# 也可以用 cfssl certinfo -cert cert.pem 查看
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
2b:dd:d6:75:ad:40:3c:a1:3e:fe:97:6d:63:15:44:24:6b:51:15:c5
Signature Algorithm: ecdsa-with-SHA256
Issuer: C = CN, ST = SH, L = Shanghai, CN = laisky.com
Validity
Not Before: Sep 13 02:18:00 2022 GMT
Not After : Sep 10 02:18:00 2032 GMT
Subject: C = CN, ST = SH, L = Shanghai, O = Laisky, CN = l2.laisky.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:db:54:d1:ba:e6:10:86:f0:84:1d:27:aa:46:75:
99:c3:09:33:03:65:4b:1e:24:bc:85:34:3a:4c:d4:
b7:84:f3:0c:a3:dd:41:27:4a:34:e0:d4:a0:1a:a8:
cf:a3:cf:a2:12:31:12:0b:78:f1:d4:da:5c:fc:5e:
9b:03:bb:c4:b2
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
14:5B:C1:43:9E:0F:B2:02:50:58:CB:BC:0F:EA:FD:C2:9F:73:51:80
X509v3 Authority Key Identifier:
63:4F:CF:04:42:60:CC:4A:C1:CA:CB:D6:36:77:BF:7C:1F:59:F7:FA
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:46:02:21:00:83:6a:41:55:90:b6:67:ae:91:18:f7:e6:8b:
f9:89:91:ca:83:3d:43:7d:54:fe:15:f0:5d:7f:19:23:05:ef:
9d:02:21:00:85:83:ee:7c:19:67:72:13:99:c9:ed:d9:d1:69:
b9:e7:df:e7:27:c4:43:29:ad:e5:1c:97:67:66:2b:b7:1a:15
签发叶子证书
用 CA 可以签发下级 CA,也可以签发叶子证书(leaf certificate)。叶子证书仅用于加密,不能再用来签发下级 CA 或证书。
# 创建 ./l2/l3
mkdir l3
cd l3
# 创建 csr.json
cat <<< '
{
"CN": "l3.laisky.com",
"hosts": [
"l3.laisky.com",
"*.l3.laisky.com"
],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"ST": "SH",
"L": "Shanghai",
"O": "Laisky"
}
]
}' > csr.json
# 生成私钥
cfssl genkey \
-config ../../config.json \
-profile leaf csr.json | cfssljson -bare
# 用 l2 CA 签发 l3 叶子证书
cfssl sign \
-ca ../cert.pem \
-ca-key ../cert-key.pem \
-config ../../config.json \
-profile leaf \
-csr cert.csr | cfssljson -bare
验证证书
通过 openssl:
# 用 rootCA 验证 l2 CA
✗ openssl verify -CAfile ca.pem l2/cert.pem
l2/cert.pem: OK
# 用 rootCA、l2CA 验证 l3 证书
✗ openssl verify -CAfile ca.pem --untrusted l2/cert.pem l2/l3/cert.pem
l2/l3/cert.pem: OK
1、为服务端配置证书链
前文中为了验证 l3 证书需要借助证书链的所有 CA 证书。有两种方式提供完整的证书链:
verifier 准备好所有的证书,然后去验证叶子证书
tester 准备好完整证书链,verifier 仅需准备 root CA
本节主要介绍第二种,也是实践中最为常见的方式。这种方式下的客户仅需要在新任根中添加 rootCA,即可验证所有派生的中间证书和叶子证书。
(如图所示,服务端一次性提供了完整证书链)
服务端(tester)Go 代码:
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"log"
"net/http"
"os"
)
func panicErr(err error) {
if err != nil {
panic(err)
}
}
func parseCert(cnt []byte) *x509.Certificate {
blk, _ := pem.Decode(cnt)
cert, err := x509.ParseCertificate(blk.Bytes)
panicErr(err)
return cert
}
func pem2der(p []byte) []byte {
b, _ := pem.Decode(p)
return b.Bytes
}
func main() {
caCertPem, err := os.ReadFile("../ca.pem")
panicErr(err)
l2CertPem, err := os.ReadFile("../l2/cert.pem")
panicErr(err)
l3CertPem, err := os.ReadFile("../l2/l3/cert.pem")
panicErr(err)
// l3Cert := parseCert(l3CertPem)
l3keyPem, err := os.ReadFile("../l2/l3/cert-key.pem")
panicErr(err)
l3key, err := x509.ParseECPrivateKey(pem2der(l3keyPem))
panicErr(err)
// 准备好完整证书链, l3 crt -> l2 crt -> root CA
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{
{
Certificate: [][]byte{pem2der(l3CertPem), pem2der(l2CertPem), pem2der(caCertPem)},
PrivateKey: l3key,
},
},
}
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("world\n"))
})
server := &http.Server{
Handler: mux,
Addr: ":10443",
TLSConfig: tlsCfg,
}
if err := server.ListenAndServeTLS("", ""); err != nil {
log.Println("exit, ", err.Error())
}
panicErr(err)
}
客户端 Go 代码:
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"io"
"log"
"net/http"
"os"
)
func panicErr(err error) {
if err != nil {
panic(err)
}
}
func parseCert(cnt []byte) *x509.Certificate {
blk, _ := pem.Decode(cnt)
cert, err := x509.ParseCertificate(blk.Bytes)
panicErr(err)
return cert
}
func main() {
// 因为服务端提供了完整的证书链,
// tester 仅需预先配置 rootCA 即可
caCertPem, err := os.ReadFile("../ca.pem")
panicErr(err)
capool := x509.NewCertPool()
capool.AddCert(parseCert(caCertPem))
cli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: capool,
},
},
}
resp, err := cli.Get("https://l3.laisky.com:10443/hello")
panicErr(err)
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
panicErr(err)
log.Printf("resp: %s", string(respBytes))
}
通过 CRL 吊销证书
ℹ️ 吊销证书一般有两种方式:
OCSP:提供一个服务端接口,实时请求
CRL 提供一个 CRL 文件,列举已吊销的证书。CRL 文件一般会以公网 URL 的形式提供,可以内嵌到证书文件中。在公网发布时,采用二进制的 DER 格式。
通过 cfssl 工具生成 CRL 文件有如下步骤:
获取要吊销的证书的 serial number(big int),将其转为 string 保存到文本文件,每一行对应一个序列号
通过 cfssl 生成 crl,然后用 openssl 转存为 pem 格式
# 本例中我们会将 l3 证书吊销。
# 将 l3 证书的序列号提取到文本文件
#
# 需要注意的是,openssl 等工具习惯用 16 进制表示序列号,
# 而 cfssl 用十进制来表示序列号。
cfssl certinfo -cert l2/l3/cert.pem | jq .serial_number | tr -d '"' > crl.txt
# 用 cfssl 生成二进制 DER 文件
# 对外用 URI 发布时,就发布该 DER 文件
#
# CRL 文件可以设置有效期,单位为秒,用于提示使用方,应该定期更新 CRL 文件。
cfssl gencrl crl.txt ca.pem ca-key.pem 604800 | base64 -d > crl.der
# 用 openssl 转存为 pem 格式
openssl crl -inform DER -out crl.pem
可以用 openssl 查看 crl 内容:
✗ openssl crl -text -noout -in crl.pem
# 也可通过 openssl crl -text -noout -in crl.der
Certificate Revocation List (CRL):
Version 2 (0x1)
Signature Algorithm: ecdsa-with-SHA256
Issuer: C = CN, ST = SH, L = Shanghai, CN = laisky.com
Last Update: Sep 13 06:51:36 2022 GMT
Next Update: Sep 20 06:51:36 2022 GMT
CRL extensions:
X509v3 Authority Key Identifier:
63:4F:CF:04:42:60:CC:4A:C1:CA:CB:D6:36:77:BF:7C:1F:59:F7:FA
Revoked Certificates:
Serial Number: 533C693318EF7E13EED4846CEA5C63ECAE9F7713
Revocation Date: Sep 13 06:51:36 2022 GMT
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:44:02:20:50:1b:27:c4:0b:23:63:4c:fb:c9:32:93:eb:17:
bd:26:c3:9d:f5:94:ab:cc:82:22:13:4a:2d:b1:17:9f:41:08:
02:20:59:ef:ba:34:f0:8c:00:0c:42:17:18:d7:e8:8b:a8:4d:
f9:11:06:65:d1:70:61:9f:ce:e3:52:30:c3:2c:88:ea
可以把 crl.der
文件上传到公网,然后将 URL 注入到证书中(在 config.json
中配置 crl_url
即可)。
1、Golang
即使服务端证书设置了 CRL URI,Go 原生客户端仍然不会自动检查,目前看来只能手动 check。
完善的检查方式为:
遍历服务端证书链,取得所有的
cert.CRLDistributionPoints
(CRL URI)遍历下载所有的 CRL 文件,加载所有的
crl.TBSCertList.RevokedCertificates
双重 for 循环,一一对比证书链的证书和 CRL 中的吊销列表的序列号
// check cert by crl.pem
//
// prepare crl.txt
//
// cfssl certinfo -cert l2/l3/cert.pem | jq .serial_number | tr -d '"' > crl.txt
//
// generate crl.pem:
//
// cfssl gencrl crl.txt ca.pem ca-key.pem 604800 | base64 -d | openssl crl -inform DER -out crl.pem
//
// check CRL:
//
// openssl crl -text -noout -in crl.pem
func checkCRL(certs ...*x509.Certificate) {
rawCRL, err := os.ReadFile("crl.pem")
panicErr(err)
crl, err := x509.ParseCRL(rawCRL)
panicErr(err)
if crl.TBSCertList.NextUpdate.Before(time.Now()) {
panic("crl is expired")
}
for _, c := range certs {
for _, rc := range crl.TBSCertList.RevokedCertificates {
if c.SerialNumber.Cmp(rc.SerialNumber) == 0 {
log.Panicf("cert revoked: %s - %s", c.DNSNames[0], c.SerialNumber.String())
}
}
}
log.Println("crl verified")
}
ℹ️ 也有观点认为没必要做在线吊销检查,见 No, don’t enable revocation checking
本文转载自:「 LaiSky 的博客 」,原文:https://url.hi-linux.com/FS2c8 ,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com。
最近,我们建立了一个技术交流微信群。目前群里已加入了不少行业内的大神,有兴趣的同学可以加入和我们一起交流技术,在 「奇妙的 Linux 世界」 公众号直接回复 「加群」 邀请你入群。
你可能还喜欢
点击下方图片即可阅读
点击上方图片,『美团|饿了么』大额外卖红包天天免费领
更多有趣的互联网新鲜事,关注「奇妙的互联网」视频号全了解!