最近在折腾一套内网系统的国密改造最实际的需求就是证书不能再依赖外部CA必须在自己手上的CentOS 7服务器里把国密证书体系从零建起来。为了搞明白国密证书、GmSSL、私有CA、完整证书链这一整套东西我把根CA、中间CA、服务器证书签发和验证整条链路都跑了一遍中间也踩了不少编译和配置的坑。这篇文章就是一份能直接照着操作的实战记录不讲虚的全部是命令和步骤。内容围绕“用GmSSL在CentOS 7上构建私有CA并签发完整国密证书链”展开。GmSSL是支持国密SM2/SM3/SM4算法的开源密码库可以把它理解成一套“国密版OpenSSL”。文章适合两类人一类是负责政务、金融或者内网系统国产化改造的运维和安全工程师另一类是正在学习国密算法和PKI体系想找一套完整闭环练手的人。只要跟着做就能在本地生成一条从根CA到中间CA再到服务器证书的完整链条并且能通过GmSSL自己的命令完成验证。1. 为什么要在CentOS 7上自建国密CA1.1 国密证书解决的实际问题传统PKI体系里用的基本都是RSA、ECDSA、SHA-256这些国际算法对应的证书也是由国际CA机构签发。但在国产化替代的浪潮里很多政务、金融、能源系统会明确要求使用国密算法体系SM2是椭圆曲线公钥密码算法负责签名和密钥交换SM3是密码杂凑算法相当于国密版的SHA-256SM4是分组对称加密算法。这里面的关键点是国密算法从算法标准到实现方式都是自主可控的而一套完全自主的私有CA意味着证书的签发、吊销、信任关系全部掌握在自己手里不依赖任何外部机构。那为什么不直接用OpenSSL来搞OpenSSL虽然也陆陆续续加入了SM2/SM3/SM4支持但在很多CentOS 7自带的OpenSSL版本里SM2证书签发和SM3摘要的处理并不完整尤其对SM2 ID这类细节支持不到位导致折腾起来非常费劲。GmSSL是OpenSSL的国密分支EVP层、命令行工具、配置文件都原生支持国密算法最核心的优势是单条命令、单套配置就能完成从密钥生成到证书签发的全部工作不用到处打补丁。1.2 方案选型GmSSL vs OpenSSL补丁我在做方案对比时实际考虑了三条路继续用系统自带OpenSSL尝试手动适配SM2和SM3。这条路的坑最深因为CentOS 7自带的OpenSSL版本相对老SM2在底层并不是原生支持很多操作要么直接报错要么需要自己写很复杂的引擎配置。在OpenSSL 3.x上编写自定义provider或者加载第三方国密引擎。这个方案对OpenSSL 3.x才比较友好但CentOS 7默认的软件源里根本没有OpenSSL 3.x需要自己找RPM或源码编译工程量瞬间翻倍。而且让Nginx、Java等上层应用去适配这些provider又是一件麻烦事。直接用GmSSL。GmSSL本身就是一个完整的命令行工具集命令风格和OpenSSL几乎一致生成密钥用gmssl ecparam签发证书用gmssl ca只是内部算法换成了国密。唯一的风险是它会安装到独立前缀目录不会污染系统自带的OpenSSL这反而是优点。最终我选了GmSSL理由很简单在CentOS 7上做国密证书链它是一个完整、自洽、不用缝合的工具链。其他方案要么太折腾要么有兼容性隐患。1.3 CA层级结构与双证书概念一套标准的私有CA至少要分两级根CA和中间CA。根CA是整个信任链的锚点一旦根CA私钥泄露底下所有证书都会失效所以根CA私钥要越少使用越好。中间CA负责日常签发服务器证书和客户端证书就算中间CA被攻破吊销中间CA证书后根CA还可以继续签发新的中间CA不会导致整个体系崩溃。证书链的完整路径就是服务器证书 → 中间CA证书 → 根CA证书。这里还要提一下国密体系里常见的“双证书”概念。在政务、金融等严格合规场景会要求每个实体同时拥有签名证书和加密证书两张证书分别用于身份认证和数据加密。不过底层构建CA和签发证书的流程完全一样只需要在签发时使用不同的证书模板和keyUsage即可。这篇文章先把一条完整证书链跑通双证书只是重复流程里多加一张证书的事。2. CentOS 7环境准备与GmSSL安装2.1 系统依赖与基础初始化我用的是一台CentOS 7.9 x86_64最小化安装机器内存2G磁盘20G完全满足实验要求。编译GmSSL之前先把基础工具装齐yum -y install gcc make perl perl-devel pcre-devel zlib-devel wget tar这里重点提一下perl和pcreconfigure阶段会用到perl来生成Makefile而某些GmSSL的配置脚本会检查pcre库。另外我强烈建议在编译前把系统时间校对一遍证书生效期和有效期的计算依赖系统时间内网机器如果时间差太远刚签出来的证书就可能出现“not yet valid”的报错。可以用date -s手工设置或者配置一个可用的NTP源。如果你所在的是一台离线内网机器yum装不了包那就得在能联网的机器上先把这些RPM包下载好传到内网后用yum localinstall *.rpm批量安装。还有一种更省事的方式找一台与内网同架构x86_64、同glibc版本的联网机器用Docker起一个CentOS 7基础容器在容器里编译好GmSSL再把整个/opt/gmssl目录打包传到内网解压使用。这个办法我在一个完全隔离的机房环境里实践过省去了在内网逐条安装依赖的麻烦。2.2 下载并编译安装GmSSLGmSSL在GitHub上有官方仓库建议下载release版本的tar包而不是直接拉master分支。我用的是基于OpenSSL 1.1.0的GmSSL 2.x系列命令风格和OpenSSL保持一致做CA实验最顺手。cd /opt wget https://github.com/GmSSL/GmSSL/archive/refs/tags/v2.5.4.tar.gz tar -zxvf v2.5.4.tar.gz cd GmSSL-2.5.4 ./config --prefix/opt/gmssl --openssldir/opt/gmssl/ssl make -j4 make install注意--prefix参数我特意把GmSSL装到/opt/gmssl而不是默认的/usr/local目的就是和系统自带的OpenSSL彻底隔离避免openssl和gmssl两个命令混在一起。编译结束后把GmSSL的bin目录加进PATHecho export PATH/opt/gmssl/bin:$PATH /etc/profile.d/gmssl.sh source /etc/profile.d/gmssl.sh gmssl version如果能输出版本信息说明安装成功。这里有个经验之谈不要用make install覆盖系统的/usr/bin/openssl否则系统的yum、curl这些工具可能因为OpenSSL版本不兼容而崩溃。2.3 常见编译问题与离线镜像方案编译GmSSL时最容易遇到的问题集中在configure阶段Cant locate ExtUtils/MakeMaker.pm这是没装perl-devel导致的直接yum -y install perl-devel就能解决。还有pcre.h: No such file or directory这是缺pcre-devel。另外GmSSL编译过程中会调用flex和bison如果源码版本较老且需要重新生成解析器也要提前装上flex bison。虽然不一定每次都会触发但提前装好能省一次返工。前面提到的离线编译Docker方案我多说几句。我在centos:7官方镜像基础上下载GmSSL源码在容器里完成./config make make install然后把/opt/gmssl目录打成tar包拷贝到内网服务器后直接解压使用。实测下来只要宿主机的glibc版本不低于编译机的glibc版本就不存在可执行文件跑不起来的问题。这个方法避开了离线环境里装gcc、perl、make这一大堆依赖的麻烦算是个非常实用的旁路方案。3. 搭建两级私有CA与生成根证书3.1 规划证书目录骨架证书体系一定不能图省事把所有文件堆在一个目录里。我按照根CA、中间CA、服务器证书分成三个层级建了相对规范的目录结构mkdir -p /opt/gmca/{root/{private,newcerts,crl},intermediate/{private,newcerts,crl},server} chmod 700 /opt/gmca/root/private /opt/gmca/intermediate/private touch /opt/gmca/root/index.txt touch /opt/gmca/intermediate/index.txt echo 1000 /opt/gmca/root/serial echo 1000 /opt/gmca/intermediate/serial echo 1000 /opt/gmca/root/crlnumber echo 1000 /opt/gmca/intermediate/crlnumber目录结构说明private目录专门放私钥权限必须设为700只允许root访问。newcerts目录存放CA签发的所有证书副本以后吊销证书时用得上。index.txt是CA的数据库文件GmSSL签发证书时会自动往里面追加记录。serial和crlnumber存放证书序列号和CRL序列号必须以十六进制数字写入写成1000就是从十进制的4096开始主要是为了避免序列号过短造成某些系统的兼容问题。这套目录结构是从OpenSSL的CA标准实践里继承过来的放到GmSSL下完全兼容。我个人习惯把私钥、证书、配置文件全部规划好再动手后面每一步的操作都有的放矢不至于签完证书找不到文件。3.2 编写根CA配置文件GmSSL的ca命令和OpenSSL一样必须依赖配置文件。我专门为根CA写了一份root.cnf核心内容如下[ ca ] default_ca CA_default [ CA_default ] dir /opt/gmca/root database $dir/index.txt new_certs_dir $dir/newcerts certificate $dir/root.crt serial $dir/serial crlnumber $dir/crlnumber private_key $dir/private/root.key default_md sm3 default_days 3650 preserve no policy policy_strict [ policy_strict ] countryName match stateOrProvinceName match organizationName match organizationalUnitName optional commonName supplied emailAddress optional [ req ] default_bits 256 distinguished_name req_distinguished_name string_mask utf8only [ req_distinguished_name ] countryName Country Name (2 letter code) countryName_default CN organizationName Organization Name organizationName_default Example Root CA commonName Common Name commonName_default Example Root CA [ v3_ca ] basicConstraints critical, CA:TRUE keyUsage critical, keyCertSign, cRLSign subjectKeyIdentifier hash authorityKeyIdentifier keyid:always, issuer几个关键点说明一下default_md sm3这代表后续所有证书签名摘要算法都用SM3这是国密证书硬性要求不能用SHA256代替。policy_strict里countryName、stateOrProvinceName、organizationName被设为match表示下级证书请求的这些字段必须和CA一致避免内部证书体系里出现乱七八糟的机构信息。v3_ca里basicConstraints critical, CA:TRUE是根CA的必修项没有这个扩展根证书不具备签发下级证书的资格。中间CA的intermediate.cnf和根CA基本一样只有两处不同一是目录路径全部指向/opt/gmca/intermediate二是v3_ca扩展里要加上pathlen:0限制中间CA不能再签发下一级CA只能签发最终实体证书[ v3_ca ] basicConstraints critical, CA:TRUE, pathlen:0 keyUsage critical, keyCertSign, cRLSign subjectKeyIdentifier hash authorityKeyIdentifier keyid:always, issuer3.3 生成根CA的SM2私钥与自签名根证书根CA的私钥使用SM2椭圆曲线算法生成gmssl ecparam -genkey -name SM2 -out /opt/gmca/root/private/root.key chmod 600 /opt/gmca/root/private/root.keyecparam -genkey -name SM2就是生成一条SM2椭圆曲线私钥它的公钥、私钥同时保存在输出的PEM文件里。这个命令和OpenSSL生成RSA私钥的genrsa是同一个逻辑只是算法换成SM2。然后基于这个私钥生成自签名根证书gmssl req -new -x509 \ -config /opt/gmca/root/root.cnf \ -key /opt/gmca/root/private/root.key \ -out /opt/gmca/root/root.crt \ -days 3650 \ -sm3这里用-x509表示直接生成自签名证书而不是签发请求-sm3明确指定摘要算法。执行完可以检查一下证书内容gmssl x509 -in /opt/gmca/root/root.crt -text -noout重点关注输出里的Public Key Algorithm是否显示SM2以及Signature Algorithm是否是SM3。如果显示的是RSA或者SHA256说明GmSSL没有正确使用国密算法要回头检查密钥生成步骤。3.4 生成中间CA并签发中间证书中间CA的私钥同样用SM2生成gmssl ecparam -genkey -name SM2 -out /opt/gmca/intermediate/private/intermediate.key chmod 600 /opt/gmca/intermediate/private/intermediate.key生成CSR签名请求gmssl req -new \ -config /opt/gmca/intermediate/intermediate.cnf \ -key /opt/gmca/intermediate/private/intermediate.key \ -out /opt/gmca/intermediate/intermediate.csr \ -subj /CCN/OExample Root CA/CNExample Intermediate CA \ -sm3然后用根CA给这个CSR签发证书签发时必须明确指定使用v3_ca扩展模板让它带上CA属性gmssl ca -config /opt/gmca/root/root.cnf \ -in /opt/gmca/intermediate/intermediate.csr \ -out /opt/gmca/intermediate/intermediate.crt \ -extensions v3_ca \ -notext \ -batch \ -md sm3-batch参数是让命令不去交互式询问确认适合脚本化执行。签完之后用同样的gmssl x509 -text检查中间证书确认CA:TRUE, pathlen:0已经写入。这一步的扩展项特别关键我在排查问题时就遇到过因为-extensions没有指定或者指向了错误的扩展块导致中间证书没有CA属性后面装配证书链时验证直接报错。4. 签发服务器证书并组装完整证书链4.1 生成服务器SM2私钥和CSR服务器证书的私钥应当每台服务器独立生成不要多台服务器共用同一份密钥。我用下面这组命令gmssl ecparam -genkey -name SM2 -out /opt/gmca/server/server.key chmod 600 /opt/gmca/server/server.key生成CSR时要特别注意证书里的常用名称和扩展域名。现在很多浏览器和服务端校验时SANsubjectAltName扩展比CN字段更重要。CSR请求默认不带SAN需要在生成请求时加上扩展配置。我单独建了一个server_ext.cnf文件[ req ] distinguished_name req_distinguished_name req_extensions v3_req [ req_distinguished_name ] commonName server.example.com [ v3_req ] subjectAltName alt_names [ alt_names ] DNS.1 server.example.com DNS.2 www.example.com然后生成CSRgmssl req -new \ -config /opt/gmca/server/server_ext.cnf \ -key /opt/gmca/server/server.key \ -out /opt/gmca/server/server.csr \ -subj /CCN/OExample Org/CNserver.example.com \ -sm3这里有个细节如果用-subj指定了subject那么配置文件里req_distinguished_name部分的主要用途就变成了提供默认值和扩展模板真正需要读取的是v3_req里的SAN配置。4.2 使用中间CA签发服务器证书中间CA的配置模板里需要加入服务器证书使用的扩展块。在intermediate.cnf里追加[ server_cert ] basicConstraints critical, CA:FALSE keyUsage critical, digitalSignature, keyEncipherment extendedKeyUsage serverAuth subjectKeyIdentifier hash authorityKeyIdentifier keyid, issuerkeyUsage这里按照常见TLS证书模板写为digitalSignature和keyEncipherment需要特别说明的是在纯国密SM2体系里更严谨的keyUsage还应该包含keyAgreement具体看你的应用层是用SM2做密钥交换还是依赖其他协商算法。所以我建议在实际落地时先问清楚上层国密SSL网关或中间件对keyUsage的要求再决定填哪几项。执行签发gmssl ca -config /opt/gmca/intermediate/intermediate.cnf \ -in /opt/gmca/server/server.csr \ -out /opt/gmca/server/server.crt \ -extensions server_cert \ -md sm3 \ -days 825 \ -batch825天是一个相对常见的服务器证书有效期选择主要是旧版本的一些客户端对超过两年有效期的证书有兼容问题825天刚好卡在两年加三个月以内算是行业实操里积累出来的经验值。如果你的系统没有这类兼容约束直接定为365天或730天也更便于管理。4.3 组装证书链文件并在GmSSL中验证证书链的组装顺序是从服务器证书开始依次拼接中间证书和根证书cat /opt/gmca/server/server.crt \ /opt/gmca/intermediate/intermediate.crt \ /opt/gmca/root/root.crt /opt/gmca/server/server-chain.pem这个server-chain.pem就是服务器需要加载的完整证书链文件。在Nginx里配置SSL证书时ssl_certificate指向的就是这个文件它和私钥文件配合完成TLS握手。验证证书链是否完整用GmSSL自带的verify命令gmssl verify -CAfile /opt/gmca/root/root.crt \ -untrusted /opt/gmca/intermediate/intermediate.crt \ /opt/gmca/server/server.crt输出OK表示验证通过。这里解释一下参数-CAfile提供根CA证书-untrusted提供中间CA证书最后是要验证的服务器证书GmSSL会把证书链从服务器证书一路追到根证书沿途检查签名、有效期、扩展项。如果缺了中间证书验证会报unable to get local issuer certificate这个报错在排查TLS连接问题时太常见了。还要检测证书链文件本身是否格式正确gmssl crl2pkcs7 -nocrl -certfile /opt/gmca/server/server-chain.pem | gmssl pkcs7 -print_certs -noout这条命令可以一次性列出证书链里的每一级证书名称如果只有服务器证书而缺失中间CA或根CA打印结果里就会少内容。4.4 证书格式转换及部署提示有些业务平台要求证书必须为DER格式或PKCS#12格式。DER格式转换gmssl x509 -in /opt/gmca/server/server.crt -outform DER -out /opt/gmca/server/server.derPKCS#12格式常用于Java环境导入密钥库gmssl pkcs12 -export \ -in /opt/gmca/server/server-chain.pem \ -inkey /opt/gmca/server/server.key \ -out /opt/gmca/server/server.p12转换过程中会要求设置导出密码这个密码要妥善保存。真正部署时Nginx开启SSL的配置大致是这样server { listen 443 ssl; server_name server.example.com; ssl_certificate /opt/gmca/server/server-chain.pem; ssl_certificate_key /opt/gmca/server/server.key; }需要明确的是普通Nginx的官方版本默认调用的还是系统OpenSSL不一定能直接完成国密TLS握手。如果业务必须走完整国密SSL通常要配合GmSSL的Nginx补丁或者使用支持国密算法的SSL网关。这部分轮到自己适配时至少有了一条可以反复验证的证书链排查范围就小多了。5. 实战中的坑点排查与运维建议5.1 常见报错速查与排查思路整个流程走下来最常遇到的报错基本是这几类我直接整理成排查表报错信息可能原因解决办法unknown digest sm3调用了系统openssl而不是gmssl检查PATH确认which gmssl指向/opt/gmssl/bin/gmsslunable to write random state当前目录或HOME目录不可写GmSSL无法保存随机数种子文件切换到可写目录或设置RANDFILE/opt/gmca/.rndunable to get local issuer certificate证书链缺少中间CA证书组装server-chain.pem时带上中间证书并用verify验证BasicConstraints: CA is false中间CA签发时没用v3_ca扩展模板给gmssl ca加-extensions v3_ca重新签发requested key type not supportedSM2私钥格式或曲线名称不正确确认gmssl ecparam -name SM2而不是RSA或其它曲线not yet valid系统时间和证书有效期的start时间对不上校准系统时钟检查gmssl x509 -text里的Not Before字段这几类问题里最隐蔽的是最后一种。我在内网机器上签完证书业务端怎么连都报证书无效最后才发现是机器时间是两年前的时间。证书有效期从签发时刻开始计算系统时间错了一天证书在业务端看来就是过期的。5.2 SM2 ID跨工具验证的细节坑SM2算法有一个特有的参与方标识ID默认值是1234567812345678。用GmSSL签发和验证的证书GmSSL会一直使用默认ID所以整个闭环里不会出问题。但如果将来把这个证书拿到Java的BouncyCastle库或者第三方国密SDK里做验签对方有可能使用自定义的SM2 ID。签名方和验签方ID一旦不一致验签就会失败而报错信息通常很模糊。解决办法是在GmSSL的配置文件里显式声明sm2_id[ CA_default ] sm2_id 1234567812345678并确保下游业务方使用相同的ID。这是国密体系移植时最容易被忽视的一个差异点在方案设计初期最好和所有对接方统一好。5.3 私钥保护、备份与吊销机制私钥文件权限必须严格限制我一般直接把私钥设为600属主root整个private目录设700。生产环境里私钥还应考虑离线介质备份比如加密拷贝到移动硬盘或保险柜。根CA私钥尤其要重视最理想的状态是签署完中间证书后把根CA私钥从在线服务器上移走只在需要签发新的中间CA时才接入。定期备份index.txt和serial同样重要。它们记录着哪些证书有效、下一个序列号是多少服务器一旦宕机重建没有这两个文件证书管理就乱套了。我习惯每次签发完一批证书后把整个/opt/gmca目录打一个带日期的tar包放到独立存储上。吊销证书的命令也很简单gmssl ca -config /opt/gmca/intermediate/intermediate.cnf \ -revoke /opt/gmca/server/server.crt \ -crl_reason superseded生成CRL证书吊销列表gmssl ca -config /opt/gmca/intermediate/intermediate.cnf -gencrl -out /opt/gmca/intermediate/intermediate.crlCRL文件要定期重新生成并发布到业务系统可访问的位置客户端在验证证书时如果发现证书序列号在CRL里就会拒绝信任。5.4 证书生命周期与续期管理私有CA一旦建成证书维护就是日常工作了。服务器证书的续期流程和首次签发基本一致生成新私钥和CSR或者复用原私钥但更推荐更换新私钥用中间CA重新签发然后把新的server-chain.pem部署到业务上。我的建议是每次续期都换新私钥这样即使旧私钥已经泄露新证书也不会受影响。同时要给证书加上监控在到期前30天就发告警。可以用crontab跑一段简单的脚本用gmssl x509 -enddate -noout取出有效期再和当前时间对比到期前自动触发续期提醒。证书过期不像服务宕机那么显眼但造成的业务中断时间一点都不比宕机少。写在最后整套GmSSL私有CA搭建流程我前前后后跑了好几遍最大的体会是根CA和中间CA不能懒省事合并成一级因为证书体系一旦铺开日常签发、吊销、续期都会集中在中间CA上保留根CA作为离线锚点只会让后续运维更从容。另外GmSSL命令行的报错信息有时候比较含蓄遇到问题先确认自己用的是不是/opt/gmssl/bin/gmssl再看配置路径和扩展模板多数坑都能绕过去。这套流程跑通后后面再做国密双证书、国密Nginx适配都是在同一套CA链上做扩展底子结实了上层就不慌。