玖叶教程网

前端编程开发入门

在2023年运行自己的根证书颁发机构

问题陈述

如今,任何想要拥有自己的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吧。它甚至可能开箱即用。

如果你像我一样,只是想选择不那么常规的路(因为理解所有这些东西会有所不同),以下是要求:

  • 一个用于证书颁发机构的x509证书
  • 每个内部服务的x509证书,或者只是一个单例的*.int.wejn.org
  • 一种在某处提供CA证书的方法

最后一个超出了本文的范围,但基本上你需要一个静态网站,以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证书

证书分为两部分:

  • RSA密钥
  • 具有适当扩展的x509证书

以下是我使用的方法(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 = [email protected]
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签名的主机证书,我们需要:

  • 一个RSA密钥
  • 一个证书签名请求(CSR)
  • 使用CA密钥对CSR进行签名

以下是我对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" <<EOF
[ req ]
req_extensions = v3_req
distinguished_name = req_distinguished_name
prompt = no

[ v3_req ]
# We are NOT a CA, this is for server auth, and these are the altnames
basicConstraints = critical,CA:FALSE
# We are, however, a certificate for server authentication (important!)
extendedKeyUsage=serverAuth
subjectAltName = @alt_names

[alt_names]
EOF

I=1
for AN in $ALTNAMES; do
        echo "DNS.$I = $AN" >> "$1.cnf"
        I=$[$I + 1]
done

cat >> "$1.cnf" <<EOF

[ req_distinguished_name ]
C = CH
L = Zurich
O = int.wejn.org host cert
CN = $CN
EOF

# Create the CSR
openssl req -new -key "$1.key" -sha512 -passin env:PASS -config "$1.cnf" \
        -out "$1.csr"

# Sign the CSR by the CA, resulting in `$1.crt`; needs env;CAPASS
./sign.sh "$1.csr"

# Optional: put both cert and key into a single `$1.pem` file
#ruby -pe 'next unless /BEGIN/../END/' "$1.crt" "$1.key.unsecure" > "$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 <whatever>.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 <<EOT
[ ca ]
default_ca              = CA_own
[ CA_own ]
dir                     = .
certs                   = \$dir
new_certs_dir           = \$dir/ca.db.certs
database                = \$dir/ca.db.index
serial                  = \$dir/ca.db.serial
RANDFILE                = \$dir/ca.db.rand
certificate             = \$dir/ca.crt
private_key             = \$dir/ca.key
# all hail our fruity overlords:
default_days            = 365
default_crl_days        = 30
# need sane message digest, too:
default_md              = sha512
preserve                = no
policy                  = policy_anything
copy_extensions         = copy
x509_extensions         = v3
[ v3 ]
basicConstraints = critical, CA:FALSE

[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional
EOT

#  sign the certificate
if [ "x$CAPASS" = "x" ]; then
	echo "No \$CAPASS present, will have to specify pass"
	PASSIN=""
else
	echo "Reading pass from \$CAPASS"
	PASSIN="-passin env:CAPASS"
fi

echo "CA signing: $CSR -> $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,并且需要每年更换主机证书-因为苹果这样要求。

但是由于nameConstraints的存在,即使CA密钥被泄露,至少应该会更加安全一点。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言