问题陈述

如今,任何想要拥有自己的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

声明: 本站内容转载于网络,版权归原作者所有,仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任,若侵犯到你的版权利益,请联系我们,会尽快删除处理!