Featured image of post 使用 Step CLI 模拟 TLS 证书签发、证书链验证全流程操作

使用 Step CLI 模拟 TLS 证书签发、证书链验证全流程操作

在完全搞懂 TLS 证书原理的基础上,使用 step cli 这个比 OpenSSL 更加人性化的证书工具模拟 CA 生成、中间证书生成、叶子证书密钥对生成和 CSR 生成、证书签发、证书链打包和证书验证的全过程,帮助你进一步熟悉 TLS 证书的运作流程。

证书详解

TLS 证书大家并不陌生,每天上网我们都在访问 HTTPS 网站,所以说实际上每天都在接触也不为过。不少朋友可能自建过网站,也曾经使用过 acme 之类的工具申请过 Let’s Encrypt 的证书,为网站部署 HTTPS。但有关 TLS 证书里面的概念实在太多了,证书、证书秘钥、CSR 等不同的种类,以及 P12、DER、PEM 等不同的格式,大家肯定多多少少听说过也使用过,但恐怕很少有人真正系统地了解过。

最近翻到了一篇文章,从头到尾把有关证书的所有概念系统地阐释了一遍,语言也简单易懂,非常推荐阅读。如果你还对证书这东西一知半解的话,现在是时候学起来了!

文章链接:Everything you should know about certificates and PKI but are too afraid to ask

原文是英文的,如果阅读有困难可以直接开个沉浸式翻译

在读了以上这篇文章的基础上才能看懂本文剩下的部分哦。

Step CLI

在熟悉了证书整套运作原理的基础上,让我们使用 step cli 这个工具来做一次实验,来模拟有关证书操作的全流程吧。大家可能由于开发的需要,有自签过证书,一般都会使用 OpenSSL 的 cli 工具。但不得不说 OpenSSL 的 cli 非常反人类,用过的人都明白。这里推荐的 step cli 工具则是一个和 OpenSSL 一样强大的,但是交互十分简单的开源证书工具。我们接下来就使用它作为实验的器材。

下载地址:https://github.com/smallstep/cli/releases

证书实验

安装好 step cli 之后,新建一个文件夹作为我们的实验场所。输入step certificate可以看到支持的命令,每个命令都可以查看帮助,并且有非常详细和实用的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...
COMMANDS
      bundle         bundle a certificate with intermediate certificate(s) needed for certificate path validation
      create         create a certificate or certificate signing request
      format         reformat certificate
      inspect        print certificate or CSR details in human readable format
      fingerprint    print the fingerprint of a certificate
      lint           lint certificate details
      needs-renewal  Check if a certificate needs to be renewed
      sign           sign a certificate signing request (CSR)
      verify         verify a certificate
      key            print public key embedded in a certificate
      install        install a root certificate in the supported trust stores
      uninstall      uninstall a root certificate from the supported trust stores
      p12            package a certificate and keys into a .p12 file

Root CA(根证书)生成

为了进行接下来一系列的操作,我们需要使用certificate create子命令先生成一个 Root CA,包括证书和秘钥:

1
2
3
4
D:\projects\test-repo\cert-exp (master)
λ step certificate create --profile root-ca --no-password --insecure MyRoot root-ca.crt root-ca.key
Your certificate has been saved in root-ca.crt.
Your private key has been saved in root-ca.key.

为了方便起见我们这里就不设密码了。有关create命令的各种参数可以使用step certificate create -h查看到。我们这里创建的是名为 MyRoot 的证书,证书文件和密钥都存储在当前目录下。

我们可以使用certificate inspect命令查看证书的内容:

image-20241130094317023

图中圈出来的是几个关键的点。首先 Issuer(签发者)和 Subject(主体)是一样的,表明这是一个自签证书,所有 CA 都是自签的,自己验证自己的合法性。下面的 Basic Constraints 表明这是一个 CA,pathlen 为 1 表示它允许有一个中间证书。

中间证书生成

我们可以使用 Root CA 直接签发叶子证书(域名直接用的证书),但这不太符合安全标准。于是我们再模拟一下签发一个名为 MyIntermediate 的中间证书,之后再使用中间证书签发叶子证书。

1
2
3
4
D:\projects\test-repo\cert-exp (master)
λ step certificate create --profile intermediate-ca MyIntermediate intermediate.crt intermediate.key --ca root-ca.crt --ca-key root-ca.key --no-password --insecure
Your certificate has been saved in intermediate.crt.
Your private key has been saved in intermediate.key.

签发中间证书的时候需要使用--ca命令指定 Root CA 文件,代表我们使用 Root CA 对这个证书进行签名。

使用inspect检查intermediate.crt

image-20241130094856989

可以看到签发者是 MyRoot,主体是 MyIntermediate。下面的 Constraints 显示这是一个 CA,并且 pathlen 为 0,表示它只能签发叶子证书,不能再签发下一层中间证书了。这是由于我们 Root CA 的 pathlen 为 1,它签发中间证书的时候就会把这个值减去 1。

签发叶子证书

叶子证书也就是我们常用的域名证书,是可以直接部署到 Nginx、Apache 之类的软件上给网站使用的。下面我们来进行一次这样的过程。

方法有两种,一种是直接用 CA 签发叶子证书,生成证书和密钥。另一种是先生成叶子证书的 CSR 文件和秘钥,然后用这个 CSR 拿给 CA 去签发证书本身。显然后者更安全,也是最佳实践。不过我们可以两种都尝试一下。

直接签发叶子证书

1
2
3
4
D:\projects\test-repo\cert-exp (master)
λ step certificate create --profile leaf --ca intermediate.crt --ca-key intermediate.key --no-password --insecure blog.skyju.cc blog.skyju.cc.crt blog.skyju.cc.key
Your certificate has been saved in blog.skyju.cc.crt.
Your private key has been saved in blog.skyju.cc.key.

image-20241130095631268

可以看到签发者是我们的中间证书,主体是 blog.skyju.cc。下面有个 SAN(Subject Alternative Name)的部分标志着有效的 DNS 名称。使用 SAN 而非 Subject 字段来填充域名是现在的推荐做法,并且还可以签发泛域名证书,SAN 也是可以自定义的。我们下面用 CSR 方式签发证书的时候就来测试一下。

使用 CSR 签发叶子证书

首先需要生成密钥和 CSR:

1
2
3
4
D:\projects\test-repo\cert-exp (master)
λ step certificate create --csr --san "*.skyju.cc" --no-password --insecure blog.skyju.cc blog.skyju.cc.csr blog.skyju.cc.key
Your certificate signing request has been saved in blog.skyju.cc.csr.
Your private key has been saved in blog.skyju.cc.key.

image-20241130100503086

用这条命令我们生成了一个主体为 blog.skyju.cc,但是 SAN 的 DNS 名为*.skyju.cc 泛域名的 CSR 文件。那么实际上这个证书签发出来就不止 blog.skyju.cc 能用,而是*.skyju.cc 的所有域名都能用的。

现在我们用中间证书为这个 CSR 签发证书文件,需要使用到的是certificate sign子命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
D:\projects\test-repo\cert-exp (master)
λ step certificate sign blog.skyju.cc.csr intermediate.crt intermediate.key
-----BEGIN CERTIFICATE-----
MIIBuDCCAV+gAwIBAgIQQJJKTWC0d2wKVoK1H/Ry1zAKBggqhkjOPQQDAjAZMRcw
FQYDVQQDEw5NeUludGVybWVkaWF0ZTAeFw0yNDExMzAwMjA1MjdaFw0yNDEyMDEw
MjA1MjdaMBgxFjAUBgNVBAMTDWJsb2cuc2t5anUuY2MwWTATBgcqhkjOPQIBBggq
hkjOPQMBBwNCAARTXzjia9iCgK04+cWmSyIKMhGBZr64xXxnvvP2xOmud2/JAFg7
aqsZHw7yn06GyvG9nPVL5yqLtr0is66mvTjho4GJMIGGMA4GA1UdDwEB/wQEAwIH
gDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFJJVDMkX
ouuFY3xDLDYBkjvSBHGRMB8GA1UdIwQYMBaAFH4xhy4Wm/ANcykwqP1N5M90PCmp
MBUGA1UdEQQOMAyCCiouc2t5anUuY2MwCgYIKoZIzj0EAwIDRwAwRAIgDwTQzL/R
D/dgfCe8sHWSm3nQy/h1/Z/uGVtVWjXpy4oCIBw/bkMiab8J67YIPUse4OJQqfcW
6YBA1d7WXlK+cksA
-----END CERTIFICATE-----

D:\projects\test-repo\cert-exp (master)
λ step certificate sign blog.skyju.cc.csr intermediate.crt intermediate.key > blog.skyju.cc.crt

它这里直接把证书内容输出了,于是我们需要使用>把内容重定向到文件里。

检查一下:

image-20241130100700883

和我们刚才直接签发的效果一样,是一个有效的证书文件。

捆绑中间证书和叶子证书

如果我们想把证书部署到网站,直接部署这个blog.skyju.cc.crt是不行的,因为缺少一个中间证书文件。而操作系统信任的是根证书(Root CA)文件。有些浏览器不会自动补齐证书链,我们的域名就无法通过 TLS 证书链验证。所以为了最大的兼容性考虑,还需要把中间证书的证书文件和我们叶子证书的证书文件捆绑在一起再部署到 Nginx 之类的软件上。

我们需要使用certificate bundle这个子命令:

 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
D:\projects\test-repo\cert-exp (master)
λ step certificate bundle blog.skyju.cc.crt intermediate.crt blog.skyju.cc-bundle.crt
Your certificate has been saved in blog.skyju.cc-bundle.crt.

D:\projects\test-repo\cert-exp (master)
λ cat blog.skyju.cc-bundle.crt
-----BEGIN CERTIFICATE-----
MIIBuTCCAV+gAwIBAgIQTu4WKgFliJNAWn0ayIQBiTAKBggqhkjOPQQDAjAZMRcw
FQYDVQQDEw5NeUludGVybWVkaWF0ZTAeFw0yNDExMzAwMjA1NDJaFw0yNDEyMDEw
MjA1NDJaMBgxFjAUBgNVBAMTDWJsb2cuc2t5anUuY2MwWTATBgcqhkjOPQIBBggq
hkjOPQMBBwNCAARTXzjia9iCgK04+cWmSyIKMhGBZr64xXxnvvP2xOmud2/JAFg7
aqsZHw7yn06GyvG9nPVL5yqLtr0is66mvTjho4GJMIGGMA4GA1UdDwEB/wQEAwIH
gDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFJJVDMkX
ouuFY3xDLDYBkjvSBHGRMB8GA1UdIwQYMBaAFH4xhy4Wm/ANcykwqP1N5M90PCmp
MBUGA1UdEQQOMAyCCiouc2t5anUuY2MwCgYIKoZIzj0EAwIDSAAwRQIhAIoaaE6P
dgQxwe/yxCcMObEhKDxPqbYnP/0azvSofpZEAiARxN0BXwM5kZ1ze+LGg+ERI8FE
OFmgmp6+0dsJWwN17Q==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBjjCCATSgAwIBAgIQMENcIBy+me/6x49TZhLDpDAKBggqhkjOPQQDAjARMQ8w
DQYDVQQDEwZNeVJvb3QwHhcNMjQxMTMwMDE0NjUyWhcNMzQxMTI4MDE0NjUyWjAZ
MRcwFQYDVQQDEw5NeUludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEH
A0IABN2RYufmRm+/T+bDswiZ7ad4E6jUiWQI9PSySc+txRR3AE0Fk+l9ImwcGvfl
J67W+f5oSga4o7N+WR8w33dHOJijZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMB
Af8ECDAGAQH/AgEAMB0GA1UdDgQWBBR+MYcuFpvwDXMpMKj9TeTPdDwpqTAfBgNV
HSMEGDAWgBRv0ckqxbsxjjU1pYzUUIp5IP2+ajAKBggqhkjOPQQDAgNIADBFAiBt
NJRcIMtSpdi8KKzgFNYDDbkDQCTIhLSlzFY3EyL/xAIhAIZv/g/ybSe5PajdVv5V
YEdQ6Lch6JR18nGK0bo+pltc
-----END CERTIFICATE-----

使用step certificate inspect --bundle可以检查完整的证书链:

1
step certificate inspect blog.skyju.cc-bundle.crt --bundle

输出效果这里就不展示了,实际上就是叶子证书后面跟一个中间证书。

验证证书有效性

使用certificate verify子命令可以验证证书有效性:

1
2
D:\projects\test-repo\cert-exp (master)
λ step certificate verify blog.skyju.cc-bundle.crt --host blog.skyju.cc --roots root-ca.crt

这里我们使用--host代表待验证的域名,--roots代表使用的根证书证书文件。如果根证书已经被添加到了操作系统中的话,这边的--roots其实是不用加的。

没有任何输出,代表验证有效。

现在我们改一下--host的参数做一下实验:

1
2
3
4
5
6
D:\projects\test-repo\cert-exp (master)
λ step certificate verify blog.skyju.cc-bundle.crt --host another.skyju.cc --roots root-ca.crt

D:\projects\test-repo\cert-exp (master)
λ step certificate verify blog.skyju.cc-bundle.crt --host google.com --roots root-ca.crt
failed to verify certificate: x509: certificate is valid for *.skyju.cc, not google.com

验证another.skyju.cc有效,是因为我们的证书是泛域名证书;而验证google.com无效是因为我们的证书不属于google.com

现在我们试一下验证没有 bundle 过的证书文件:

1
2
3
D:\projects\test-repo\cert-exp (master)
λ step certificate verify blog.skyju.cc.crt --host blog.skyju.cc --roots root-ca.crt
failed to verify certificate: x509: certificate signed by unknown authority

看到出错了,这是因为certificate verify命令不会自动补全证书链,所以跨了一个中间证书验证是不成功的。对此,我们只能先用根证书验证中间证书,再用中间证书验证叶子证书:

1
2
3
4
5
D:\projects\test-repo\cert-exp (master)
λ step certificate verify intermediate.crt --roots root-ca.crt

D:\projects\test-repo\cert-exp (master)
λ step certificate verify blog.skyju.cc.crt --host blog.skyju.cc --roots intermediate.crt

顺便提一句,如果我们在生产环境中签发真实的 TLS 证书,但由于某些原因缺失了中间证书,可以使用一些在线的 TLS 证书链补全工具帮助我们自动找出中间证书然后 bundle。比如说这个工具

另一个工具:mkcert

step cli 已经比 OpenSSL 易用很多了,但我们有时候做开发需要一些更傻瓜式的工具。这里推荐另一个开源工具 mkcert,可以一键创建根证书,把根证书安装到操作系统和浏览器,一键直接签发对应域名的叶子证书。

项目地址:https://github.com/FiloSottile/mkcert

image-20241130102241707

step cli 的好处是支持一些更复杂的操作,比如设置证书过期时间、查看证书信息等等。大家可以自由选择使用。

Licensed under CC BY-NC-SA 4.0