如何用 CFSSL 从零开始快速构建一套私有 PKI

公众号关注 「奇妙的 Linux 世界」

设为「星标」,每天带你玩转 Linux !

f986bc425f3057245d4e214a3573dde6.png

Synopsis

本文档会介绍如何用 cfssl 从零构建起一套 PKI,包括签发根证书(rootCA)、中间证书和叶子证书,以及通过 CRL 实现证书吊销列表。

fe54269ed4d707957a5ee208066fe774.png

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

(最重要的是 CNhostskey

{
  "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: streetAddress

  • DC: domainComponent

签发根证书:

cfssl genkey -initca csr.json | cfssljson -bare ca

会生成三个文件:

  • ca-key.pem: 私钥

  • ca.csr: 生成证书用的 CSR 文件

  • ca.pem: 证书

ℹ️ 生成证书的流程为:

  1. 生成随机私钥

  2. 编写 CSR 文件

  3. 用私钥签发 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 操作略有不同:

  1. 传统方式是会生成一个 CA 证书的 CSR,交给上一级 CA 签署

  2. 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 证书链:

  1. 次级 CA,生成自己私钥

  2. 次级 CA,将想要生成的证书内容写入 csr,交给上一级(本例中为 rootCA)

  3. 上一级通过自己的私钥、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 证书。有两种方式提供完整的证书链:

  1. verifier 准备好所有的证书,然后去验证叶子证书

  2. tester 准备好完整证书链,verifier 仅需准备 root CA

本节主要介绍第二种,也是实践中最为常见的方式。这种方式下的客户仅需要在新任根中添加 rootCA,即可验证所有派生的中间证书和叶子证书。

a9bfa2d9a594a8753b557a28d9dc3ae0.png

(如图所示,服务端一次性提供了完整证书链)

服务端(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 吊销证书

ℹ️ 吊销证书一般有两种方式:

  1. OCSP:提供一个服务端接口,实时请求

  2. CRL 提供一个 CRL 文件,列举已吊销的证书。CRL 文件一般会以公网 URL 的形式提供,可以内嵌到证书文件中。在公网发布时,采用二进制的 DER 格式。

92bdaee4ca37e4f7c02e18a7f753f387.png

通过 cfssl 工具生成 CRL 文件有如下步骤:

  1. 获取要吊销的证书的 serial number(big int),将其转为 string 保存到文本文件,每一行对应一个序列号

  2. 通过 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。

完善的检查方式为:

  1. 遍历服务端证书链,取得所有的 cert.CRLDistributionPoints(CRL URI)

  2. 遍历下载所有的 CRL 文件,加载所有的 crl.TBSCertList.RevokedCertificates

  3. 双重 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。

892d5bf2b1da4ab7bb92b22d70bc3067.gif

最近,我们建立了一个技术交流微信群。目前群里已加入了不少行业内的大神,有兴趣的同学可以加入和我们一起交流技术,在 「奇妙的 Linux 世界」 公众号直接回复 「加群」 邀请你入群。

c9383942f5c04101851ba601bf7f1750.png

你可能还喜欢

点击下方图片即可阅读

40a1dc29e2d5fb52b7217d5c4da44c48.png

如何使用 WireGuard 组建非对称路由

593711efe4739e0ae92e4c5720880f4f.png

点击上方图片,『美团|饿了么』大额外卖红包天天免费领

f49c2ee082ef66764d121412b73c16dc.png

更多有趣的互联网新鲜事,关注「奇妙的互联网」视频号全了解!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值