本文是网络基建实验的其中一章。有关其他实验,请参照该文章目录。
电子证书认证机构(CA,Certificate Authority)是组织内网络基础设施建设的重要一环。建立一个私有的局域网CA,可以以较小的成本提供较高的内网安全性。
本文记录了使用OpenSSL搭建可用的根CA及二级CA,并签发可用证书的过程。
背景资料
CA
#TODO
X.509证书体系
#TODO
椭圆曲线数字签名算法 (ECDSA)
ECDSA是当下非常流行,且安全度相对较高的一种基于椭圆曲线的数字签名算法。 由于无需考虑兼容性问题,本实验将全程采用ECDSA对私钥进行数字签名。
使用如下命令查看OpenSSL支持的EC签名方式:
$ openssl ecparam -list_curves
...
secp256k1 : SECG curve over a 256 bit prime field
secp384r1 : NIST/SECG curve over a 384 bit prime field
secp521r1 : NIST/SECG curve over a 521 bit prime field
...
prime256v1: X9.62/SECG curve over a 256 bit prime field
...
我们通常选取 P-256 , P-384 这两种椭圆曲线对私钥进行数字签名。
在OpenSSL里面,P-256 对应的是 prime256v1,而 P-384 对应 secp384r1。
对于其他的一些经常出现的椭圆曲线而言:secp256k1: 它并不是 P-256 的标准实现,但是你可以经常在区块链项目中看见它。
它没有什么用,也没有特定优化,所以尽量不要选择它。P-521 : secp521r1 则没有任何实际意义——
主流浏览器(指Chrome)在2015年已经放弃了对其的支持,且其本质上只是加长的 P-256。
如果 P-256 被攻破了,那么 P-521 也将毫无意义,而且其效率远低于 P-256。
实验目的
建立起以根CA为核心的CA证书系统以解决内网证书签发问题。
实验步骤
本实验在一台安装有 OpenSSL 1.1.1n 的 Debian 11 主机上进行。
默认所有CA证书均采用 P-384 签名,所有普通证书均采用 P-256 签名。
建立根CA
本实验以 /opt/pki/CA 作为根CA的存放目录。
文件树
$ export ROOT_CA_DIR=/opt/pki/CA
$ mkdir -p ${ROOT_CA_DIR}/{newcerts,certs,csr,crl,private}
$ touch ${ROOT_CA_DIR}/{ca.cnf,crlnumber,index,serial}
$ echo "01" > ${ROOT_CA_DIR}/serial
$ cd ${ROOT_CA_DIR}
newcerts: 存放新发布的证书certs: 存放已发布的证书和旧证书csr: 存放证书请求文件 (非必需)crl: 存放吊销证书private: 存放证书私钥
ca.cnf: Openssl的CA配置文件crlnumber: 吊销证书编号index: 存放颁发证书的数据库文件serial: 下一个颁发证书的序列号
$ tree ${ROOT_CA_DIR}
/opt/pki/CA
|-- ca.cnf
|-- certs
|-- crl
|-- crlnumber
|-- csr
|-- index
|-- newcerts
|-- private
`-- serial
制订根证书的 ca.cnf 文件
用作建立根证书示例的 ca.cnf 文件如下。
[ ca ]
default_ca = CA_Default # 指定该CA配置文件采用的配置
[ CA_Default ]
name_opt = ca_default
cert_opt = ca_default
dir = /opt/pki/CA # !!所有有关于CA的文件存放位置
certs = $dir/certs
crl_dir = $dir/crl
serial = $dir/serial
database = $dir/index
unique_subject = no # 设置为yes则database文件
# 中的subject列不能出现重
# 复值,建议为no
new_certs_dir = $dir/newcerts
certificate = $dir/certs/ca.crt # CA证书存放位置,签署用
private_key = $dir/private/ca.key # CA私钥存放位置,签署用
crlnumber = $dir/crlnumber
crl = $dir/crl.pem
RANDFILE = $dir/private/.rand
x509_extensions = usr_cert # 使用 [ usr_cert ]
# 作为x509扩展插件
# 影响是否能签署CA证书
copy_extensions = copy
default_days = 365 # !!默认证书有效时长
default_crl_days = 30
default_md = sha384 # !!默认摘要算法
# P-384对应SHA-384
# P-256对应SHA-256
preserve = no # 兼容IE则yes,否则no
policy = default_policy # 使用[ default_policy ]
# 作为签署策略
[ default_policy ]
# Policy一共有 match, supplied, optional三种
# - match : 申请的证书该DN字段必须和CA证书的相关DN字段一致
# - supplied : 该DN字段必须存在,不要求一致
# - optional : 该DN字段可选,不要求存在
countryName = match
stateOrProvinceName = match
localityName = match
organizationName = match
commonName = supplied
organizationalUnitName = optional
emailAddress = optional
[ req ]
default_bits = 2048
default_keyfile = privkey.pem
# 证书申请时的默认DN字段,对应
# [ req_distinguished_name ] 的内容
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca # 使用 [v3_ca] 作为
# x509扩展插件
string_mask = utf8only
utf8 = yes
prompt = yes # NO时必须存在默认DN字段
# YES时则申请时可手动输入DN字段
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name (full name)
localityName = Locality Name (eg, city)
organizationName = Organization Name
organizationalUnitName = Organizational Unit Name (eg, section)
commonName = Common Name
# 默认DN字段
# 此处字段的存在情况应当与 [ default_policy ] 中的一致
countryName_default = CN
stateOrProvinceName_default = Some_Province
localityName_default = Some_City
organizationName_default = An Organzition
commonName_default = An Organzition Root CA
organizationalUnitName_default = CA Services
[ usr_cert ]
# 来自于[ CA_Default ]的x509_extension设定
# 影响证书签发时的设定
# !!这张CA证书能否签发其他的CA证书
# 设定为TRUE时,可以签发CA证书,不可以签发普通证书
# 设定为FALSE时则反之
basicConstraints = CA:TRUE
[ v3_ca ]
# 来自于[ req ]的x509_extension设定
# 影响证书申请时的设定
# !!此处决定这张证书是否为CA证书
# 设定为TRUE时,则为CA证书
# 设定为FALSE时,则为普通证书
basicConstraints = CA:TRUE
[ req_attributes ]
# 无实际作用,可以忽略
生成根CA证书私钥
$ openssl ecparam \
-name secp384r1 \
-genkey \
-noout \
-out private/ca.key
创建根CA证书请求文件
$ openssl req -new \
-key private/ca.key \
-out csr/ca.csr \
-config ca.cnf
对证书请求文件进行校验:
openssl req \
-text -noout -verify \
-in csr/ca.csr
根CA证书自签
如果之前在 ca.cnf 的 [ req ] 中设置了 prompt = no,
会直接以 [ req_distinguished_name ] 中的默认值进行自签。
$ openssl ca -selfsign \
-in csr/ca.csr \
-out certs/ca.crt \
-days 365 \
-config ca.cnf
对生成证书进行校验:
$ openssl x509 \
-text -noout \
-in certs/ca.crt
添加根CA到操作系统当中
证书必须为 *.crt,否则不会被update命令识别。
# cp certs/ca.crt /usr/local/share/ca-certificates/your-ca-name.crt
# update-ca-certificates
注意:部分浏览器使用自带的CA,如Firefox,需要再在浏览器中添加根CA。
建立二级CA
本实验以 /opt/pki/SecondaryCA 作为二级CA的存放目录。
文件树
$ export SECONDARY_CA_DIR=/opt/pki/SecondaryCA
$ mkdir -p ${SECONDARY_CA_DIR}/{newcerts,certs,csr,crl,private}
$ touch ${SECONDARY_CA_DIR}/{ca.cnf,crlnumber,index,serial}
$ echo "01" > ${SECONDARY_CA_DIR}/serial
$ cd ${SECONDARY_CA_DIR}
制订二级证书的 ca.cnf 文件
cp ${ROOT_CA_DIR}/ca.cnf ${SECONDARY_CA_DIR}/ca.cnf
参照根证书的 ca.cnf 文件,对如下区域进行了修改:
...
[ CA_Default ]
...
dir = /opt/pki/SecondaryCA # !!所有有关于CA的文件存放位置
...
[ req_distinguished_name ]
...
commonName = An Organzition Secondary CA
...
[ usr_cert ]
basicConstraints = CA:FALSE # !! 该证书**不能**签发CA证书
# 如果此处设定为 CA:TRUE,则可以
# 继续签发下一级CA证书
[ v3_ca ]
basicConstraints = CA:TRUE # !! 该证书**是**CA证书
...
生成二级CA证书私钥
$ openssl ecparam \
-name secp384r1 \
-genkey \
-noout \
-out private/ca.key
创建二级CA证书请求文件
生成证书请求文件时,使用二级CA的配置,即二级CA的 [ req ] 字段:
由于[ req ] 的 x509_extension 即 [v3_ca] 设置了 basicConstraints = CA:TRUE ,
此处申请的是一张 CA证书 。
$ openssl req -new \
-key private/ca.key \
-out csr/ca.csr \
-config ca.cnf
使用根证书对二级CA证书进行签名
签名时使用的是签名证书的配置文件,此处为根证书 ${ROOT_CA_DIR}/ca.cnf ;
由于根证书的 [ CA_Default ] 配置的 x509_extension 设置了
basicConstraints = CA:TRUE,故根证书可用于签署CA证书。
$ openssl ca \
-in csr/ca.csr \
-out certs/ca.crt \
-config ${ROOT_CA_DIR}/ca.cnf
使用二级CA签发证书
由于在二级CA的配置文件中签署部分我们配置了
basicConstraints = CA:FALSE,
故此CA可以签发普通证书。
现在我们使用此二级CA签发 foo.example.com 的证书:
本实验在 /opt/pki/foo.example.com 中进行。
$ export CERT_NAME=foo.example.com
$ export NORMAL_CERT_DIR=/opt/pki/${CERT_NAME}
制订证书请求所用的配置文件
编辑 {NORMAL_CERT_DIR}/req.cnf :
[ req ]
prompt = no # 以配置好的字段直接申请
distinguished_name = server_distinguished_name
req_extensions = req_ext
x509_extensions = v3_req
attributes = req_attributes
[ server_distinguished_name ]
# 此处应当与CA配置文件当中所配置的字段一致性和存在性一致
countryName = CN
stateOrProvinceName = Some_Province
localityName = Some_City
organizationName = An Organzition
commonName = foo.example.com
organizationalUnitName = An Organzition Some App
[ v3_req ]
basicConstraints = CA:FALSE # 申请普通证书
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
[ req_attributes ]
[ req_ext ]
subjectAltName = @alternate_names # SAN扩展
[ alternate_names ]
DNS.1 = foo.example.com
DNS.2 = bar.foo.example.com
申请证书的时候,commonName字段需要填写域名。
SAN扩展字段影响了该证书可以解析的域名范围。
生成私钥、请求文件、签发证书
$ cd ${NORMAL_CERT_DIR}
# 生成证书私钥
$ openssl ecparam \
-name prime256v1 \
-md sha256 \
-genkey \
-noout \
-out ${CERT_NAME}.key
# 生成证书请求文件 [使用请求配置文件]
$ openssl req -new \
-key ${CERT_NAME}.key \
-out ${CERT_NAME}.csr \
-config ${NORMAL_CERT_DIR}/req.cnf
# 签发证书 [使用二级CA的配置文件]
$ openssl ca \
-in ${CERT_NAME}.csr \
-out ${CERT_NAME}.pem \
-config ${SECONDARY_CA_DIR}/ca.cnf
整合证书链
由于证书颁发机构是二级CA,所以服务器需要根CA和二级CA的证书,需要对证书进行聚合操作:
$ cat ${CERT_NAME}.pem \
${SECONDARY_CA_DIR}/certs/ca.crt \
${ROOT_CA_DIR}/certs/ca.crt | \
tee ${CERT_NAME}_ALL.pem
其顺序以申请证书为首,高层级的CA排在上方,根CA排在最下方。
本例便是以证书-二级CA-根CA的方式排列的。
环境清理
结束后不要忘记清理无用的环境变量:
$ unset ROOT_CA_DIR SECONDARY_CA_DIR NORMAL_CERT_DIR CERT_NAME
小结
本实验采用OpenSSL建立了实验性质的根CA和二级CA, 并使用二级CA签发了带有SAN扩展的证书, 初步实现了架设证书认证机构的需求。 然而,本实验有以下不足点仍然值得进一步探索:
- 未能做到限定CA证书的用途
- 签发操作繁琐,无法做到使用API自助签发
- 未能保证根证书私钥安全性
- 证书签发机构未能与其他组件进行联动
以及其他的,由于经验不足和负载压力不够,暂时并未考虑到的事务。
弯路
初步是想用 cfssl 进行公钥认证机构的搭建的。Cloudflare的工具非常好用,
但是文档非常的少,在处理申请认证二级CA证书的时候,
并不能确定是否真的符合根CA可以申请二级CA证书的条件,
于是弃用cfssl改用较为臃肿复杂的OpenSSL。
但不得不提,在集群当中,cfssl凭借其强大的基于json的证书申请配置,
有着相当好的IaC,即“基础设施即代码”的可用性,
相比OpenSSL更加适合集群的证书申请架构。
不过,对于CA这种几乎是一次性的证书申请而言,采用OpenSSL也是较为稳妥的方式。
虽然繁杂,但是过程明了,且文档清晰,国内外的参考实例也非常多,
非常适合搭建实验性质的CA机构。
在搭建CA的过程当中,选择证书的数字签名方式也走了不少弯路。
由于ECDSA相比RSA在相同强度下具有更高的效率,故这次搭建首选ECDSA,
然而却犯了“过度”的问题,凭借一己之见强行使用P-521,
并没有和SHA-512摘要算法相结合。在搜索过相关的椭圆曲线使用情况,以及参考了LetsEncrypt论坛的相关内容后,决定以P-384作为CA证书签名算法,
而以P-256这种效率最高的方式作为普通证书的签名算法。
之后的努力方向大致是先以其他的基础设施搭建为主, 在基础设施较为完备之后再考虑对CA机构进行优化。
参考资料
RFC5480
openssl.cnf参数详解
如何使用OpenSSL建立自己的CA
如何建立根CA及二级CA
选取椭圆曲线加密方式的指南-2022
Letsencrypt论坛对支持P-521的讨论