问题陈述
如今,任何想要拥有自己的X509证书的人都有免费的替代方案,比如ZeroSSL或Let's Encrypt。
但是,如果只是为了内部服务,其中一些甚至与互联网隔离开来怎么办呢?更重要的是,如果你不想为每3个月的续订而烦恼无法验证服务器身份,或者想要一个简单方便的通配符x509证书怎么办呢?
那么…如何考虑使用古老的解决方案,自建根CA呢?最好能够被主流浏览器完全接受,包括iOS上的浏览器。
让我们深入探讨一下。
背景/历史课程
很久以前,当我还是个小男孩的时候…我们用sign.sh这个脚本以及少量的openssl genrsa、openssl req -new -x509和openssl req -new来完成整个过程。
或者至少我是这么认为的,因为在过去的多年里,我一直用默认值default_days = 3650来运行它。虽然在Linux的Firefox上至今仍然运行良好,但当你尝试在苹果设备上使用以这种方式生成的服务器证书时,你会遇到一堵墙。
事实证明,苹果真的不希望你使用有效期超过398天的服务器证书,还有许多其他限制。简而言之:2048位以上,SHA2摘要,忽略CN,altnames为王,keyusage=serverAuth。
基于此,以下是截至撰写时在Firefox和最新的MacOS/iOS上有效的方法。
解决方案
这些“自建x509根证书”的指南不胜枚举。如果你懒得动手,就用mkcert吧。它甚至可能开箱即用。
如果你像我一样无法验证服务器身份,只是想选择不那么常规的路(因为理解所有这些东西会有所不同),以下是要求:
最后一个超出了本文的范围,但基本上你需要一个静态网站,以application/x-x509-ca-cert的MIME类型输出PEM编码的证书。关于这个问题,我们以后再深入讨论。
从顶层来看,我希望有这样一个东西:
# _gen_all.sh
## generate CA key+cert
./cacert.sh
## generate host key+cert for a single host
./hostcert.sh snowflake.int.wejn.org
## generate wildcard host key+cert with two alt names
./hostcert.sh int.wejn.org '*.int.wejn.org'
最终将生成ca.crt、ca.key、snowflake.int.wejn.org.{crt,key}和int.wejn.org.{crt,key}等工作正常的文件。
生成CA证书
证书分为两部分:
以下是我使用的方法(cacert.sh):
#!/bin/bash
if [ -f "ca.cnf" ]; then
echo "CA already exists."
exit 1
fi
umask 066
# Generate a CA password, because openssl (reasonably) wants to protect
# the key material... and dump it to `ca.pass`.
export CAPASS=$(xkcdpass -n 64)
if [ -z "$CAPASS" ]; then
echo "Error: password empty; no xkcdpass?"
exit 1
fi
echo "$CAPASS" > "ca.pass"
# Generate the 4096 bit RSA key for the CA
openssl genrsa -aes256 -passout env:CAPASS -out "ca.key" 4096
# Strip the encryption off it; IOW, now they're are two things worth
# protecting -- the `ca.pass` and `ca.key.unsecure`.
openssl rsa -in "ca.key" -passin env:CAPASS -out "ca.key.unsecure"
# At this point, you can decide whether to memorize `ca.pass` and
# delete it along with `ca.key.unsecure`, or protect `ca.key.unsecure`
# with your life, and maybe forget all about `ca.key` and `ca.pass`.
#
# (I'm sure you would have no trouble rewriting this to do away with
# the `ca.pass` and `xkcdpass` dependency altogether)
# Configure the CSR with necessary fields
cat > "ca.cnf" <<'EOF'
[ req ]
x509_extensions = v3_req
distinguished_name = req_distinguished_name
prompt = no
[ v3_req ]
# This is the money shot -- we are the cert authority (CA:TRUE),
# and there are no other CAs below us in the chain (pathlen:0),
# and the constraint is non-negotiable (critical)
basicConstraints = critical, CA:TRUE, pathlen:0
## This is optional but maybe needed for some platforms
#extendedKeyUsage = serverAuth, clientAuth, emailProtection
# Let's do the nameConstraints thing, because it works on iOS16
# and recent Firefox. So constrain all leaf certs to `int.wejn.org`
# and its subdomains, but not `critical` in case it's not supported
# by some device.
# h/t https://news.ycombinator.com/item?id=37538084
keyUsage = critical, keyCertSign, cRLSign
nameConstraints = permitted;DNS:int.wejn.org
[ req_distinguished_name ]
C = CH
L = Zurich
O = int.wejn.org CA
CN = ca.int.wejn.org
emailAddress = ca@int.wejn.org
EOF
# Do the deed -- generate the `ca.crt`, with 10 year (3650 days) validity
openssl req -new -x509 -days 3650 -sha512 -passin env:CAPASS -config ca.cnf
-key ca.key -out ca.crt -text
生成主机证书
我假设我们继续使用上面的ca.pass和ca.key,以便使sign.sh按照最初的编写方式工作。
要生成由CA签名的主机证书,我们需要:
以下是我对hostcert.sh的看法:
#!/bin/bash
# Read the CA password, used by `sign.sh` later
export CAPASS=$(cat ca.pass)
if [ -f "$1.cnf" ]; then
echo "Host: $1 already exists."
exit 1
fi
if [ -z "$1" ]; then
echo "Error: No hostname given"
exit 1
fi
umask 066
# Generate the certificate's password, and dump it.
export PASS=$(xkcdpass -n 64)
if [ -z "$PASS" ]; then
echo "Error: password empty; no xkcdpass?"
exit 1
fi
echo "$PASS" > "$1.pass"
# Figure out what the hostname / altnames are, and confirm.
echo "$1" | fgrep -q "."
if [ $? -eq 0 ]; then
CN="$1"
ALTNAMES="$@"
else
CN="$1.int.wejn.org"
ALTNAMES="$1.int.wejn.org"
fi
echo "CN: $CN"
echo "ANs: $ALTNAMES"
echo "Enter to confirm."
read A
# Generate the RSA key, unlock it into the "unsecure" file
openssl genrsa -aes256 -passout env:PASS -out "$1.key" ${SSL_KEY_SIZE-4096}
openssl rsa -in "$1.key" -passin env:PASS -out "$1.key.unsecure"
# Construct the CSR data
cat > "$1.cnf" <> "$1.cnf"
I=$[$I + 1]
done
cat >> "$1.cnf" < "$1.pem"
当然,还有当前的sign.sh脚本,附带一些注释:
#!/bin/sh
##
## sign.sh -- Sign a SSL Certificate Request (CSR)
## Copyright (c) 1998-2001 Ralf S. Engelschall, All Rights Reserved.
##
# argument line handling
CSR=$1
if [ $# -ne 1 ]; then
echo "Usage: sign.sign .csr"; exit 1
fi
if [ ! -f $CSR ]; then
echo "CSR not found: $CSR"; exit 1
fi
case $CSR in
*.csr ) CERT="`echo $CSR | sed -e 's/.csr/.crt/'`" ;;
* ) CERT="$CSR.crt" ;;
esac
# make sure environment exists
if [ ! -d ca.db.certs ]; then
mkdir ca.db.certs
fi
if [ ! -f ca.db.serial ]; then
echo '01' >ca.db.serial
fi
if [ ! -f ca.db.index ]; then
cp /dev/null ca.db.index
fi
# create an own SSLeay config
cat >ca.config < $CERT:"
openssl ca -batch -config ca.config $PASSIN -out $CERT -infiles $CSR
echo "CA verifying: $CERT CA cert"
if [ -f ca-chain.pem ]; then
openssl verify -CAfile ca-chain.pem $CERT
else
openssl verify -CAfile ca.crt $CERT
fi
# cleanup after SSLeay
rm -f ca.config
rm -f ca.db.serial.old
rm -f ca.db.index.old
# die gracefully
exit 0
运行玩具示例
决定时刻到了,年轻人:
$ ls
cacert.sh hostcert.sh sign.sh
$ chmod a+x *.sh
$ ./cacert.sh
Generating RSA private key, 4096 bit long modulus (2 primes)
..++++
..............................++++
e is 65537 (0x010001)
writing RSA key
$ ./hostcert.sh snowflake.int.wejn.org
CN: snowflake.int.wejn.org
ANs: snowflake.int.wejn.org
Enter to confirm.
Generating RSA private key, 4096 bit long modulus (2 primes)
..............................++++
............++++
e is 65537 (0x010001)
writing RSA key
Reading pass from $CAPASS
CA signing: snowflake.int.wejn.org.csr -> snowflake.int.wejn.org.crt:
Using configuration from ca.config
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
countryName :PRINTABLE:'CH'
localityName :ASN.1 12:'Zurich'
organizationName :ASN.1 12:'int.wejn.org host cert'
commonName :ASN.1 12:'snowflake.int.wejn.org'
Certificate is to be certified until Sep 15 13:47:28 2024 GMT (365 days)
Write out database with 1 new entries
Data Base Updated
CA verifying: snowflake.int.wejn.org.crt CA cert
snowflake.int.wejn.org.crt: OK
$ ./hostcert.sh int.wejn.org '*.int.wejn.org'
CN: int.wejn.org
ANs: int.wejn.org *.int.wejn.org
Enter to confirm.
Generating RSA private key, 4096 bit long modulus (2 primes)
.............................++++
.............................................++++
e is 65537 (0x010001)
writing RSA key
Reading pass from $CAPASS
CA signing: int.wejn.org.csr -> int.wejn.org.crt:
Using configuration from ca.config
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
countryName :PRINTABLE:'CH'
localityName :ASN.1 12:'Zurich'
organizationName :ASN.1 12:'int.wejn.org host cert'
commonName :ASN.1 12:'int.wejn.org'
Certificate is to be certified until Sep 15 13:48:05 2024 GMT (365 days)
Write out database with 1 new entries
Data Base Updated
CA verifying: int.wejn.org.crt CA cert
int.wejn.org.crt: OK
看起来它是工作的
$ ls -w 80
cacert.sh int.wejn.org.crt
ca.cnf int.wejn.org.csr
ca.crt int.wejn.org.key
ca.db.certs int.wejn.org.key.unsecure
ca.db.index int.wejn.org.pass
ca.db.index.attr sign.sh
ca.db.index.attr.old snowflake.int.wejn.org.cnf
ca.db.serial snowflake.int.wejn.org.crt
ca.key snowflake.int.wejn.org.csr
ca.key.unsecure snowflake.int.wejn.org.key
ca.pass snowflake.int.wejn.org.key.unsecure
hostcert.sh snowflake.int.wejn.org.pass
int.wejn.org.cnf
$ openssl verify -CAfile ca.crt int.wejn.org.crt snowflake.int.wejn.org.crt
int.wejn.org.crt: OK
snowflake.int.wejn.org.crt: OK
$ egrep '(Public|bit|Alternative|DNS|v3.e|Sign|Vali|Not)' snow*.crt
Signature Algorithm: sha512WithRSAEncryption
Validity
Not Before: Sep 16 13:47:28 2023 GMT
Not After : Sep 15 13:47:28 2024 GMT
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:snowflake.int.wejn.org
Signature Algorithm: sha512WithRSAEncryption
$ grep -A1 'Alternative' int.wejn.org.crt
X509v3 Subject Alternative Name:
DNS:int.wejn.org, DNS:*.int.wejn.org
注意:使用4096位RSA和SHA512(SHA2系列)进行加密,具有TLS Web服务器身份验证密钥用途,正确的Subject Alternative Name,有效期为一年。
无法在此处展示它是否在设备上实际工作。但是在我的设备上是有效的。我发誓。
显然,ca.crt需要作为根CA导入到每个设备上,并且解释如何导入的指南对于每个操作系统/浏览器来说都是不同的。但是上面概述的静态网站在简化导入过程方面起了很大作用10,尤其在iOS上。 :)
使用CA进行流氓操作
由于添加了nameConstraints,不再可能进行流氓操作并为其他不相关的域名颁发证书。这使得整个过程更加安全。
看看这个:
$ ./hostcert.sh int2.wejn.org '*.int2.wejn.org'
CN: int2.wejn.org
ANs: int2.wejn.org *.int2.wejn.org
Enter to confirm.
[...]
$ openssl verify -CAfile ca.crt int2.wejn.org.crt
C = CH, L = Zurich, O = int.wejn.org host cert, CN = int2.wejn.org
error 47 at 0 depth lookup: permitted subtree violation
error int2.wejn.org.crt: verification failed
证书已经颁发,但路径验证失败。在这里是使用OpenSSL,但在最终用户设备上会以同样的方式失败。很巧妙!
结束语
这是我对在2023年运行自己的根证书颁发机构的精彩世界的简短探索…一个能够被苹果设备和Linux浏览器接受的机构。
显然,这种方法的一个明显缺点是需要保护一堆秘密11,并且需要每年更换主机证书-因为苹果这样要求。
娜娜项目网每日更新创业和副业项目
网址:nanaxm.cn 点击前往娜娜项目网
站 长 微 信: nanadh666
但是由于nameConstraints的存在,即使CA密钥被泄露,至少应该会更加安全一点。
娜娜项目网每日更新创业和副业项目
网址:nanaxm.cn 点击前往娜娜项目网
站 长 微 信: nanadh666