ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Windows下用OpenSSL搭建自签名证书链:根CA、中间CA与业务证书生成全记录

Windows下用OpenSSL搭建自签名证书链:根CA、中间CA与业务证书生成全记录 内网环境、本地联调、开发机HTTPS总会走到同一步得有一张能用的证书。直接拿openssl req -x509签一张自签名证书很简单十几秒就能搞定但一旦环境里不止一个服务、不止一台机器每加一个服务就要在每个客户端里手工信任一张新证书后面维护会变成一个无底洞。更好的做法是搭一条自签名证书链自己生成一个根CA通过根CA签发中间CA再用中间CA去签发业务证书。客户端只需要信任根CA这一张后续新增域名、新增服务都由中间CA自动扩展整条链验证一次通过。这篇东西把Windows环境下用OpenSSL生成根CA、中间CA和服务端证书的完整过程拆开揉碎附带参数解释和排坑记录。1. 证书链为什么值得多花这几分钟1.1 自签单证书与证书链的本质差异单张自签证书是所有证书方案里最省事的一种OpenSSL一行命令就能生成。它的问题不在“自签”本身而在于信任关系是点对点的app1.example.com用一张证书app2.example.com又得另一张gitlab.example.com还得第三张。每张证书各自的私钥都在自己手里客户端需要把所有证书挨个导入“受信任的根证书颁发机构”后面任何一张过期或更换又得重新铺一遍。证书链是把信任关系收敛成一个锚点客户端最终信任的只有根CA业务证书长得什么样、什么时候签发、什么时候吊销全部由上层CA来背书。只要根CA的私钥没有泄露整条链内部怎么变化都不需要客户端感知。这也是真实公网HTTPS的模型浏览器里预置了几十个根CA你访问任何一家网站最终验证追到的都是某一个根。1.2 两级CA的底层逻辑你可以在根CA下面直接签发业务证书实际操作里绝大多数人也会建议你多签一个中间CA再让中间CA签发叶子证书。原因有两个层面。从安全角度说根CA私钥如果泄露等于整个信任体系作废所有依赖它的证书都得重建。把根CA私钥离线保存、平时不参与签名日常签名动作都用中间CA的私钥完成即使中间CA出问题可以单独吊销它根CA的信任锚还在其它分支不受牵连。从管理角度说中间CA相当于一个“权限边界”不同用途甚至可以签不同中间CA比如一套给内部服务用、一套给无线网络用吊销或轮换时只影响自己的那一片。证书链的签发关系也遵循这个层级根CA自签自己是证书链的终点中间CA由根CA签发叶子证书由中间CA签发。验证时从叶子证书逐级向上找签发者最终能在系统信任根里找到匹配的根CA验证通过。1.3 什么样的情况才真正需要证书链不是所有场景都需要证书链。如果你的目的是在本机跑一个临时服务比如给某个工具配个HTTPS单张自签证书完全够用。什么时候建议上证书链三个典型信号比较明显一是服务数量超过三五个二是要覆盖多个域名甚至通配符域名三是有多人或多台的协作环境比如团队内的统一开发网关。出现这些信号时证书链带来的后续维护成本优势就会大幅高于搭建那几分钟的付出。2. Windows准备OpenSSL先解决工具链2.1 最省事的安装包方式Windows没有内置OpenSSL第一步就是装一个。对大多数没有特殊诉求的人来说直接用Slproweb发布的Windows安装包最省事它提供Win64 OpenSSL和Win32 OpenSSL两个版本。选择时注意和你后续要跑的软件位数匹配现在新机器基本都是64位直接选Win64的LTS版本。安装过程很简单只有一个地方要留意安装到最后一步会有把OpenSSL的bin目录加入PATH的选项建议勾选。如果当时没勾后面每次执行要在命令里写完整路径很影响效率。装完检查一下安装目录默认通常在C:\Program Files\OpenSSL-Win64bin目录下能找到openssl.exe。2.2 编译安装路线的前置条件有些场景没法用安装包比如公司服务器不允许装第三方软件、或者你需要特定版本和特定编译参数这时候要走源码编译。Windows上编译OpenSSL并非单单点一下configure就行它依赖Perl和NASM两类工具。很多人在这一步遇到的报错就是Perl is needed by OpenSSL。这表示当前环境里没有安装Perl或者系统找到了但不是OpenSSL要求的版本。Windows下建议直接装Strawberry Perl装完在命令行执行perl -v能正常输出版本号即可。NASM是汇编编译器用于生成优化过的汇编代码从官网下载绿色版后把nasm.exe所在目录也加入PATH。两者都准备好之后再执行perl Configure VC-WIN64A --prefixD:\OpenSSL nmake nmake install编译需要一段时间过程中如果提示缺少某个依赖先回过来检查PATH配置多数问题都是工具没被找到而不是源码本身的问题。2.3 安装后立刻做的两个小检查不管用哪种方式装完先确认版本和配置目录是否正常避免后面命令执行一半才发现环境不对。在命令窗口执行openssl version如果能输出类似OpenSSL 3.x.x的内容说明主程序可用。接着执行openssl version -d这条命令会显示OpenSSL默认配置文件的路径比如/usr/local/ssl/openssl.cnf或者Windows下的安装目录。这一步很重要因为OpenSSL很多命令在没有显式指定配置文件时会去读默认配置如果默认配置不存在或路径不对生成证书时扩展属性很容易丢失。3. 从零生成完整证书链的实操记录3.1 先规划证书目录与身份信息开始敲命令前先把证书结构和目录安排好。我的习惯是在D:\ssl-lab下建三个子目录ca、intermediate、server分别放根CA、中间CA和业务证书的相关文件。这样后续每个层级文件不会混在一起。还有一个需要提前想清楚的是证书中的Distinguished Name也就是国家、省份、组织这些字段。虽然客户端验证时通常只关心证书链和域名但DN信息会成为证书身份的一部分签出来的证书会带显示这些内容比如在浏览器里点开小锁就能看到。测试环境随便填无所谓一旦要放内部公共环境建议按实际组织信息来填方便别人识别。填的时候有四个核心字段C代表国家代码ST是省份O是组织名CN是关键必须填证书要绑定的域名或标识。根CA的CN建议体现“Root CA”字样中间CA则体现“Intermediate CA”叶子证书CN与域名保持一致否则后面排查时会把自己绕晕。3.2 生成根CA私钥与自签名证书先进入ca目录cd D:\ssl-lab\ca生成根CA私钥。我在这里用4096位并加上AES256加密保护命令行会提示设置密码。这个密码在后续每次用私钥签发时都要输入介意输入步骤的也可以去掉-aes256但生产环境强烈不建议裸奔openssl genrsa -aes256 -out ca.key 4096有了私钥接着用req命令生成自签名根证书。这里的关键点是必须在配置文件里声明这个证书是CA证书否则用它签出来的下级证书在客户端验证时会因为basicConstraints限制而失败。在ca目录下新建root_ca.cnf[ req ] default_bits 4096 distinguished_name dn x509_extensions v3_ca prompt no [ dn ] C CN ST Beijing O My Lab CN My Lab Root CA [ v3_ca ] subjectKeyIdentifier hash authorityKeyIdentifier keyid:always basicConstraints critical, CA:true keyUsage critical, keyCertSign, cRLSign配置文件中prompt no表示不弹出交互提问DN信息直接读取配置段落。basicConstraints critical, CA:true是这一层最关键的约束明确当前证书是CA。执行openssl req -x509 -new -key ca.key -sha256 -days 3650 -out ca.crt -config root_ca.cnf-x509表示直接输出自签名证书而不是CSR-days 3650是有效期十年。根CA的有效期应尽量长一点毕竟它是整个信任体系的锚未来所有下级证书的过期时间都不能超过它。3.3 签发中间CA中间CA没有自签步骤需要先生成CSR再由根CA对这个CSR签名。先切到intermediate目录创建中间CA的私钥和CSRcd D:\ssl-lab\intermediate openssl genrsa -aes256 -out intermediate.key 3072配置文件intermediate_ca.cnf[ req ] default_bits 3072 distinguished_name dn prompt no [ dn ] C CN ST Beijing O My Lab CN My Lab Intermediate CA生成CSRopenssl req -new -key intermediate.key -out intermediate.csr -config intermediate_ca.cnf接下来用根CA为中间CA的CSR签名。签名时不能直接拿x509 -req裸跑必须通过-extfile传入扩展文件否则生成出来的证书没有任何CA约束。创建intermediate_ext.cnf[ intermediate_ca ] subjectKeyIdentifier hash authorityKeyIdentifier keyid:always basicConstraints critical, CA:true, pathlen:0 keyUsage critical, keyCertSign, cRLSignpathlen:0的含义是中间CA下面不能再签CA了只能签普通叶子证书。这个限制能防止中间CA私钥泄露后被人继续扩出更多CA层级。执行openssl x509 -req -in intermediate.csr -CA ..\ca\ca.crt -CAkey ..\ca\ca.key -CAcreateserial -out intermediate.crt -days 3650 -sha256 -extfile intermediate_ext.cnf -extensions intermediate_ca-CAcreateserial会在根CA目录生成一个ca.srl序列号文件用来记录下一次签发用的唯一序列号。第一次签时它会自动创建后面再签会自增。3.4 用中间CA签发服务端证书这是整条链日常使用频率最高的环节。服务端证书的CN、SAN、密钥用途都必须与业务场景严格匹配。在server目录下创建CSR配置server_csr.cnf[ req ] default_bits 2048 distinguished_name dn prompt no [ dn ] C CN ST Beijing O My Lab CN app.example.com执行cd D:\ssl-lab\server openssl req -new -newkey rsa:2048 -nodes -keyout app.example.com.key -out app.example.com.csr -config server_csr.cnf-nodes表示不加密私钥。服务端证书私钥在nginx这类程序里要能被启动脚本直接读取通常不加密否则每次重启都得手工输密码自动化运维会非常痛苦。签发这步是证书链能不能被浏览器接受的关键只看CN已经不够了。现代浏览器对没有SAN扩展的证书一律视为无效也就是说签发时必须写入服务器域名和IP。创建server_ext.cnf[ server_cert ] basicConstraints critical, CA:FALSE keyUsage critical, digitalSignature, keyEncipherment extendedKeyUsage serverAuth subjectAltName alt_names [ alt_names ] DNS.1 app.example.com DNS.2 *.app.example.com IP.1 127.0.0.1 IP.2 192.168.31.10extendedKeyUsage serverAuth限制证书只能用于服务器身份认证防止证书被拿去当客户端证书或做其它用途。执行签发openssl x509 -req -in app.example.com.csr -CA ..\intermediate\intermediate.crt -CAkey ..\intermediate\intermediate.key -CAcreateserial -out app.example.com.crt -days 825 -sha256 -extfile server_ext.cnf -extensions server_cert注意这里的-CA和-CAkey指向上层的中间CA不是根CA。用中间CA签发这也是证书链设计的目的。3.5 构造证书链文件并verify验证服务端软件比如nginx通常需要提供一个“叶子证书所有中间证书”的链文件而根证书一般不需要放在站点证书里。拼接文件时顺序有讲究叶子证书必须在前中间CA证书随后不能颠倒。用cmd执行type app.example.com.crt ..\intermediate\intermediate.crt app.example.com.chain.crt这个文件后续会配置到ssl_certificate指令里。拼接完成后先用OpenSSL的verify命令完整验证一遍确保没有明显错误openssl verify -CAfile ..\ca\ca.crt -untrusted ..\intermediate\intermediate.crt app.example.com.crt-CAfile指向客户端最终信任的根CA-untrusted传入验证路径上需要的中间证书把叶子证书放在最后一个参数。输出结果为app.example.com.crt: OK说明验证通过。如果只写openssl verify app.example.com.crt大概率报error 20 unable to get local issuer certificate因为OpenSSL根本不知道签发者是谁。4. 证书链接入应用与系统信任配置4.1 让Windows信任根CA证书链生成之后最关键的信任落地动作是把根CA证书安装到Windows受信任的根证书颁发机构。建议在安装前先双击打开ca.crt确认证书信息正常然后点击“安装证书”存储位置选“本地计算机”这样本机所有用户都能使用。下一步选择“将所有证书都放入下列存储”点击浏览勾选“受信任的根证书颁发机构”。如果希望客户端设备在访问时能正确构建链光导入根CA其实不够最好再导入一次中间CA证书到“中间证书颁发机构”否则在个别应用里可能会因为找不到中间证书而跳过根验证报错。Windows下有图形界面的导入方式也有命令行方式。管理员PowerShell执行Import-Certificate -FilePath .\ca.crt -CertStoreLocation Cert:\LocalMachine\Root Import-Certificate -FilePath .\intermediate.crt -CertStoreLocation Cert:\LocalMachine\CA这里有个实操经验证书导入之后不需要重启系统但有些老应用会缓存证书列表如果测试时仍然报不信任可以先重启一下浏览器或对应进程。4.2 nginx等服务软件的证书链配置以nginx为例配置HTTPS站点时的证书链字段写法如下server { listen 443 ssl; server_name app.example.com; ssl_certificate D:/ssl-lab/server/app.example.com.chain.crt; ssl_certificate_key D:/ssl-lab/server/app.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; }上面ssl_certificate指向的必须是拼接后的链文件而不是单独的叶子证书。如果你只配置叶子证书很多客户端在验证时拿不到中间证书会报证链不完整。Java系的服务比如Spring Boot/Tomcat则通常需要将证书和私钥导入PKCS12格式再配置可以使用如下命令转换openssl pkcs12 -export -out keystore.p12 -inkey app.example.com.key -in app.example.com.chain.crt -name app.example.com转换时密码务必记住Tomcat或Spring Boot配置里要用到。4.3 配置后的客户端访问验证服务配好之后不要急着打开浏览器点确认先用命令行做一个快速握手验证。在Windows命令行里执行curl.exe -v https://app.example.com如果域名还没有解析到本机可以加--resolve参数做临时映射curl.exe -v --resolve app.example.com:443:127.0.0.1 https://app.example.com观察输出中的SSL部分如果出现SSL certificate verify ok说明证书链校验通过。如果报了证书错误大概率是根CA没装进系统信任库或者证书链文件拼接顺序不对。用OpenSSL s_client命令也可以查看服务端实际下发的证书链这个命令在排查时价值很高openssl s_client -connect app.example.com:443 -servername app.example.com -showcerts -CAfile ..\ca\ca.crt输出中Server certificate之后列出的就是服务端发送的整条证书链逐一核对签发关系一目了然。5. 高频报错与排查思路实录5.1 unexpected eof while reading到底是什么原因很多人在Windows上刚配好自签HTTPS服务用curl一访问就报curl: (35) OpenSSL/3.6.3: error:0a000126:SSL routines::unexpected eof while reading这个报错翻译成人话是TLS握手过程中对端在应该继续发送数据时直接关闭了连接。它不是证书链本身问题的专属报错但它确实经常出现在自签证书调试的场景中。最常见的原因有三个一是证书链文件配置错误导致SSL服务压根没起来二是服务端和客户端对TLS版本或加密套件协商不一致三是访问的端口根本不对比如把HTTP服务当成HTTPS在访问。排查时先看服务端进程是否正常启动nginx或Java日志是否报证书相关错误。如果服务端进程都没起来先去解决证书路径和私钥不匹配问题。如果服务端是正常的则用openssl s_client -connect 127.0.0.1:443做本地握手观察是在哪一步断开。还有一种情况容易被忽略你的服务端证书和私钥不匹配比如证书是A域名的、私钥是B域名的nginx启动时不一定立刻报错但首次TLS握手就会非预期断开。5.2 verify命令报错对照速查OpenSSL的verify命令报错是验证证书链时最直接的诊断信息。下面几个数字在实际工作中出现频率极高我整理成一个速查表方便对照。报错编号含义常见触发原因处理建议error 18self-signed certificate客户端直接拿到一张自签证书不是由可信CA签发检查是否未拼接链文件或者根CA是否已导入客户端信任库error 19self-signed certificate in certificate chain链条中间出现了自签证书检查服务端发送的证书链中间部分是否混入了错误文件error 20unable to get local issuer certificate无法找到证书的上层签发者验证时漏传中间证书或客户端信任库没有根CAerror 10certificate has expired证书已到期或系统时间不对检查证书有效期和本机时间再补充一个很隐蔽的路径openssl verify时返回OK但浏览器仍报不安全这时要优先怀疑系统信任库里的旧证书残留。比如根CA生成过两次系统里同时装了第一版和第二版浏览器如果匹配到了已吊销或已过期的那版旧根认证也会失败。排查方法是打开certmgr.msc在受信任的根证书颁发机构里找同名证书清理旧版本后重启浏览器重新测试。5.3 浏览器永远显示不安全的真正元凶浏览器场景下自签证书链报不安全的原因95%集中在三件事。第一件事客户端没有信任这根根CA。很多人在Windows上手动导入根CA后再用Chrome访问就神奇地通过了问题就在这里。Chrome在Windows下用的是系统证书库但Firefox用的是自己独立的证书库如果你在用Firefox需要单独在Firefox设置里导入根证书。第二件事证书里没有SAN扩展。现代浏览器从Chrome 58开始就逐步不信任只依赖CN字段匹配域名的证书而是要求SAN里显式列出域名或IP。如果server证书签发时没写alt_names浏览器右键看证书会提示“使用者备用名称”为空。这也是为什么我在前面反复强调签发时必须带扩展文件。这里有个检查命令openssl x509 -in app.example.com.crt -noout -text | findstr /C:Subject Alternative Name如果输出里没有内容或者没有列出你要访问的域名这张证书在浏览器里必然会报不安全。三件事是证书链中间证书没有正确下发。服务端只发送叶子证书没有发送中间CA证书客户端本地恰好又不认识中间CA验证链建不起来。这种情况通过openssl s_client -showcerts能很直观地看到服务端下发的证书只有一张还是两张。解决方案就是把链条文件拼对nginx等web服务器使用拼接后的chain文件。还有一个我踩过几次的小坑是用通配符证书时SAN里写了*.example.com但浏览器访问的是裸域example.com。通配符只匹配一级子域不匹配根域。需要同时把DNS.1写成example.comDNS.2写成*.example.com两边都覆盖才不会有漏网之鱼。按我个人的习惯所有证书链操作完成之后都会把openssl verify、浏览器访问、curl访问三个检查都跑一遍确认没问题再交付给业务方。这三个检查各自覆盖不同的验证路径verify命令测试的是逻辑链完整性浏览器测试的是系统信任库对接情况curl测试的是实际网络传输中的证书链下发。三关都过了这张证书链基本就可以放心使用了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进