简介:ZXCA自信数字证书制作工具是一套面向个人用户与小微型组织的数字证书制作与管理软件包,围绕证书申请、签发、撤销、导入导出及文件加密、代码签名等场景,帮助读者在本地模拟CA流程,降低对外部证书服务的依赖。压缩包共8个文件,约4.84MB,以4个txt文本配置与说明、2个exe可执行程序、1个chm帮助文档和1个html页面为主,兼顾主工具与“文件小保镖”附属组件的使用查阅。已有628人学习下载,适合信息安全入门者、开发人员及需要自建测试证书环境的小团队参考。包内资料可帮助读者理解公钥私钥对、证书格式与OID/扩展项等基础概念,并借助可执行程序与帮助文档完成证书制作、文件加密保护及代码签名验证的动手实践,形成从概念认知到工具操作的完整学习路径。
1. 内网服务要上 HTTPS,卡住的往往不是加密算法而是那张证书
上周帮一个做设备网关的团队查 TLS 握手失败,加密套件、协议版本、端口全对,最后翻出来是他们随手用一条命令生成的证书:没有 SAN、有效期写了十年、还直接拿自签根当服务端证书用。这类问题的共性不在于会不会敲 openssl,而在于没人把「根证书怎么建、SAN 怎么写、根证书怎么进系统信任区、链怎么拼全」这套流程固化成可重复的动作。ZXCA 自信(自签)数字证书制作工具瞄准的就是这一段:把本地 CA 的创建、服务端/客户端证书的签发、信任链导出收敛到一套界面和命令里,让内网域名、局域网 IP、开发环境 HTTPS 不再靠复制粘贴祖传脚本。适合谁?做本地开发联调的后端、需要给测试设备下发证书的嵌入式团队、以及被NET::ERR_CERT_AUTHORITY_INVALID反复折磨过的运维同学。
2. 信任链拆解:ZXCA 在根证书和叶子证书之间做了什么
在动手点按钮之前,得先把自签证书能被信任的前提想明白,否则生成出来的东西看着正常、用起来处处报错。这一章把 ZXCA 背后那套 X.509 逻辑拆开,讲清它替你省掉了哪些手工步骤,以及哪些参数是你必须自己拍板的。
2.1 为什么不直接自签一张服务端证书
很多人第一次做本地 HTTPS 的做法是:一条命令直接生成一个自签证书,CN 填localhost,然后装进浏览器。短期能用,长期一定翻车。原因在于自签叶子证书同时扮演了「CA」和「服务端」两个角色,浏览器信任它等于信任了一个没有 basicConstraints 约束的实体,Chrome 和 Safari 对这类证书的处理越来越严,而且每换一个服务你就得重新信任一次。
正确姿势是两层结构:先建一个自签根 CA,它只负责签发、不直接对外提供服务;再用这个根去签叶子证书,叶子证书里带CA:FALSE和明确的 keyUsage。这样你只需要把根 CA 装进一次系统信任区,之后所有由它签出来的证书自动被信任。ZXCA 的价值就在这里——它把这套两层结构做成了默认流程,你创建一次根、后面反复签发,不用每次重来。
选型上还有第三种做法:用公共 CA 签内网域名。这不现实,公网 CA 不会给192.168.x.x或者.local签证书,ACME 那套也要求域名可验证。所以内网场景下,自建 CA 是唯一稳妥路线,区别只在于你用手写脚本还是用工具。
2.2 X.509 关键字段与工具参数的对应关系
工具界面上那些输入框,本质是在填 X.509 v3 的扩展字段。搞不清对应关系,就会出现「界面填了域名但浏览器仍报名称不匹配」这种事。下面这张表是我在实际排查时整理的映射,ZXCA 的参数命名大致遵循同一套语义,具体字段名以工具内提示为准。
| X.509 字段 | 作用 | 常见错误填法 | 后果 |
|---|---|---|---|
| Subject CN | 主体通用名 | 只填 CN 不填 SAN | 现代浏览器忽略 CN,直接判名称不匹配 |
| subjectAltName (SAN) | 实际校验的域名/IP 列表 | 只写*.corp.local | 不覆盖多级子域,a.b.corp.local失败 |
| basicConstraints | 是否 CA、路径长度 | 叶子证书误填 CA:TRUE | 被当成中间 CA,安全策略拦截 |
| keyUsage | 密钥用途位 | 漏掉 keyEncipherment | RSA 密钥交换场景握手失败 |
| extendedKeyUsage | 场景用途 | 只写 serverAuth | 双向 TLS 时客户端证书被拒 |
| validity | 有效期 | 写 3650 天 | 部分客户端对超长有效期告警或拒绝 |
| serialNumber | 序列号 | 固定值或全 0 | 证书吊销和去重失效 |
重点说 SAN。CN 字段在现代 TLS 校验里已经退化成展示用途,Chrome 从 58 之后就不再回退到 CN 匹配。所以你在 ZXCA 里填域名时,务必确认它写进的是 SAN 而不是只写进 CN。工具的「域名列表」输入框一般就是 SAN 列表,多个值用逗号或换行分隔,IP 地址也能直接写进去,这点比手写配置省心。
2.3 一次签发在底层走了哪几步
把流程拆开看,ZXCA 生成一套可用证书大致经历这些动作,理解顺序有助于你判断出错时该回退到哪一步:
- 生成根 CA 私钥(RSA 3072 或 ECDSA P-256),按你选的算法和位数。
- 用根私钥自签根证书,写入
basicConstraints=critical,CA:TRUE、keyUsage=critical,keyCertSign,cRLSign,有效期通常给 5 到 10 年。 - 生成叶子私钥。
- 构造 CSR(证书签名请求),把 SAN、keyUsage、extendedKeyUsage 放进请求的扩展里。
- 用根私钥对 CSR 签名,产出叶子证书,
basicConstraints=critical,CA:FALSE。 - 导出时把叶子证书和根证书拼成
fullchain,供 Nginx 这类需要完整链的服务使用。 - 单独导出根证书(
.crt/.pem)用于导入系统信任区。
这里有个容易忽略的点:第 6 步的链拼接。很多工具默认只导出叶子证书,服务端配置里如果只给叶子、不给中间/根,桌面浏览器可能因为缓存了根而正常,移动端和 Java 客户端就会报「无法构建证书路径」。ZXCA 如果在导出选项里提供「包含完整链」,务必勾上;没有的话就手动把根证书内容追加到叶子证书文件末尾。
另外提一句密钥存储。根 CA 私钥是整个信任体系的总钥匙,一旦泄露,任何拿到它的人都能签出被你系统信任的证书。ZXCA 如果支持给根私钥加密码保护(PKCS#8 加密),一定要开;导出后把根私钥文件和证书分开放,别一股脑塞进代码仓库。我见过把根私钥提交到 Git 的,那基本等于把自家大门的钥匙挂在门口。
3. 从零跑通:用 ZXCA 签出一张浏览器认、设备也认的证书
原理清楚了,接下来是照着能复现的部分。这一章按「建根 → 签叶子 → 装到服务 → 验证」四段走,命令以 ZXCA 自带 CLI 的常见参数形态为例,你实际执行时用zxca --help核对一下参数名,逻辑是通用的。
3.1 初始化根证书并导入系统信任区
第一步只在每台机器上做一次。先建根:
# 初始化一个本地根 CA,指定输出目录、算法和根证书有效期 zxca init \ --ca-name "Local Dev Root CA" \ --out ./zxca-ca \ --key-alg rsa \ --key-size 3072 \ --validity-days 3650 \ --key-password-file ./ca.pass这段命令做三件事:在./zxca-ca下生成根私钥和根证书;用 RSA 3072 位(兼顾兼容性和性能,嵌入式设备对 ECDSA 支持不齐时优先 RSA);根有效期给 10 年,避免频繁重签。--key-password-file是给根私钥加密用的,密码别写死在命令里,用文件或环境变量传入。
导入系统信任区按平台分:
# macOS:把根证书加入系统钥匙串并标记为始终信任 sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain ./zxca-ca/root.crt # Debian/Ubuntu:放进信任锚目录后刷新 sudo cp ./zxca-ca/root.crt /usr/local/share/ca-certificates/zxca-root.crt sudo update-ca-certificates # Windows(管理员 PowerShell):导入受信任的根证书颁发机构 Import-Certificate -FilePath .\zxca-ca\root.crt ` -CertStoreLocation Cert:\LocalMachine\Root三个平台的差异值得说清楚:macOS 的security add-trusted-cert必须带-r trustRoot,否则只是导入不授信;Linux 走update-ca-certificates才会把.crt链接进/etc/ssl/certs;Windows 要进LocalMachine\Root而不是CurrentUser,否则以服务账号跑的程序读不到。导入后可以用openssl verify或浏览器打开任意 HTTPS 内网站点确认不再报警。
提示:根证书装进系统信任区意味着本机所有程序都会信任它签发的一切证书。只在开发机、测试机上这么做,别在生产办公终端上长期保留,用完可以按平台反向移除。
3.2 为局域网域名和通配符签发服务端证书
根建好后,签叶子就是一条命令的事。假设你要给api.corp.local、admin.corp.local和局域网 IP192.168.10.20一起签:
# 用已有根 CA 签发服务端证书,SAN 同时覆盖域名和 IP zxca issue \ --ca ./zxca-ca \ --common-name "api.corp.local" \ --san "api.corp.local,admin.corp.local,192.168.10.20" \ --key-alg rsa \ --key-size 2048 \ --validity-days 397 \ --eku serverAuth \ --out ./certs/api参数逐个说:--san是核心,把所有需要访问的域名和 IP 一次列全,漏一个就多一次报错;--validity-days 397是跟着苹果和 Chrome 对 TLS 证书有效期上限走的,虽然自签证书不受公共 CA 那套强制约束,但设短一点能逼自己定期轮换,也避免某些设备固件对超长有效期直接拒绝;--eku serverAuth表示这张证书只用于服务端身份,如果还要做双向 TLS 的客户端认证,再补一张--eku clientAuth的。
通配符要谨慎。*.corp.local只匹配一级子域,api.corp.local能命中,v2.api.corp.local命不中。如果确实有多级子域需求,要么在 SAN 里显式列出,要么签*.api.corp.local这种二级通配,但别指望*.*.corp.local,那不合规范,工具也不会让你签。
签发完成后,输出目录里通常会有:api.key(叶子私钥)、api.crt(叶子证书)、api-fullchain.crt(叶子+根,给服务端用)。检查一下 fullchain 里是不是两段BEGIN CERTIFICATE,只有一段说明链没拼上。
3.3 装到 Nginx、Node 和 Java 的差异处理
同一套证书文件,不同运行时的接法不一样,这里是最容易「桌面浏览器正常、服务间调用失败」的地方。
Nginx 配置:
server { listen 443 ssl; server_name api.corp.local; # 证书用 fullchain,私钥单独给 ssl_certificate /etc/nginx/certs/api-fullchain.crt; ssl_certificate_key /etc/nginx/certs/api.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://127.0.0.1:8080; } }关键点是ssl_certificate指向 fullchain 而不是单张叶子证书。Nginx 不会自动去系统信任区找根,它只把配置文件里给的证书链发给客户端,链不全客户端就拼不出路径。
Node.js 有两种情况。作为服务端时直接用证书和私钥即可;作为客户端去调内网 HTTPS 服务时,Node 默认不读系统信任区,需要显式指定:
# 让 Node 在作为客户端时信任自签根 export NODE_EXTRA_CA_CERTS=./zxca-ca/root.crt node app.js也可以用代码里https.Agent({ ca: fs.readFileSync('root.crt') })的方式,粒度更细,适合只对特定请求放行。别用NODE_TLS_REJECT_UNAUTHORIZED=0图省事,那等于关掉整站校验,测试环境跑习惯了带到线上就是事故。
Java 客户端最独立,它有自己的cacerts信任库,完全不看系统信任区。要么用 keytool 导入根证书,要么用 PFX:
# 把根证书导入 JDK 的 cacerts 信任库 keytool -importcert -noprompt \ -alias zxca-root \ -file ./zxca-ca/root.crt \ -keystore "$JAVA_HOME/lib/security/cacerts" \ -storepass changeitchangeit是 JDK 默认口令,很多环境没改过。生产环境如果改过就用实际口令,或者更稳妥地用一个独立的 truststore 通过启动参数-Djavax.net.ssl.trustStore指定,避免动全局cacerts。
3.4 用 openssl 和浏览器双通道验证
装完别急着交付,先自证。命令行这一侧:
# 校验证书链和 SAN 是否覆盖目标域名 openssl verify -CAfile ./zxca-ca/root.crt ./certs/api.crt # 实际连一次服务,看握手和证书链 openssl s_client -connect api.corp.local:443 \ -servername api.corp.local -showcerts </dev/null 2>/dev/null \ | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"第一条返回OK说明链在根这一层能验证通过;第二条打印出来的 SAN 列表要和你填的完全一致,少一个 IP 都算没签全。浏览器这一侧,打开https://api.corp.local,点地址栏锁图标看证书详情,确认「颁发者」是你建的根 CA、「有效期」符合预期,没有黄色警告。
双通道都过,再拿一台没装过根证书的干净设备试一次——它应该报错,这才是正常状态。如果干净设备也直接通过,说明你的证书被某个中间代理替换了,或者服务端根本没启用你配的证书,属于另一类问题。
4. 参数怎么定:SAN、有效期、密钥算法的取舍与边界
参数填错是自签证书翻车的高发区,而且错法很隐蔽:生成不报错、装上不报错、用的时候才炸。这一章把几个必须自己拍板的参数拆开讲,给出我一般在什么场景下选什么值。
4.1 SAN 列表与通配符的边界
SAN 是唯一被现代客户端校验的字段,写全了天下太平,写漏了寸步难行。几个实操边界:
- IP 必须单独列进 SAN,类型是 IP 而不是 DNS。用域名访问和用 IP 访问是两条校验路径,只写域名,通过 IP 访问一定报名称不匹配。
- 通配符只吃一级。
*.corp.local覆盖a.corp.local,不覆盖a.b.corp.local,也不覆盖裸域corp.local。如果裸域也要用,就得把corp.local和*.corp.local同时写进 SAN。 - 端口不进 SAN。SAN 只管主机名,
api.corp.local:8443里的端口是服务端监听的事,写进 SAN 是无效的。 - 大小写不敏感但建议统一小写,避免日志里比对时出现视觉误判。
工具里如果 SAN 是一个自由文本框,多个值用逗号或换行分隔,注意别在逗号后漏空格导致解析出带前导空格的域名,个别实现会把它当成非法值直接截断。
4.2 有效期与序列号怎么规划
有效期这件事,公共 CA 被苹果和 Chrome 逼到了一年以内(目前是 397 天上限),自签证书不受这个强制约束,但建议跟齐。理由不是合规,而是轮换习惯:设三年五年,等到过期那天你大概率已经忘了这个根签过哪些服务,排查成本远高于一年一换。
序列号方面,合格的实现应该每次签发随机生成一个足够长的正整数,如果你发现工具签出来的证书序列号是固定值或者从 1 开始递增,那它在并发签发或者吊销场景下会有碰撞风险。ZXCA 这类工具正常会处理掉,但如果你在导出后做二次处理,别手改序列号。
还有一个隐性参数是根证书和叶子证书的有效期关系。叶子证书的有效期不能超过根的剩余有效期,否则根一过期,所有叶子同时失效且无法续。稳妥做法是根给 10 年、叶子给 1 年,留足余量。
4.3 RSA 与 ECDSA 的选择
| 维度 | RSA 2048/3072 | ECDSA P-256 |
|---|---|---|
| 兼容性 | 极好,老设备、老 JDK 都认 | 较好,但老旧嵌入式固件可能不支持 |
| 握手性能 | 一般 | 更好,证书更小、验签更快 |
| 密钥长度 | 2048 起步,3072 更稳 | 256 位等效 RSA 3072 |
| 适用场景 | 通用、对外兼容优先 | 内网服务、移动端、性能敏感 |
我一般的策略是:根用 RSA 3072(根只签一次,兼容性优先),叶子按消费方来定——服务端给浏览器和现代客户端用 ECDSA 没问题,但如果下游有 Java 8 早期版本、老款安卓设备、或者某些嵌入式 TLS 栈,改用 RSA 2048 更省心。混合部署也常见:同一套根,签一张 RSA 叶子给老设备、一张 ECDSA 叶子给新服务。
注意:换算法意味着换密钥对,已有的证书不能「升级」,只能重签。规划时把消费方清单列清楚,别等上线后才发现某类设备不认。
4.4 CRL 和 OCSP 在自签场景下到底要不要
公共 CA 靠 CRL(证书吊销列表)和 OCSP(在线证书状态协议)来宣告某张证书作废。自签场景下,这两套机制的意义大不相同。
CRL 需要你维护一个可公开访问的列表文件,并在证书里写crlDistributionPoints。内网环境里这个地址往往其他机器访问不到,客户端拿不到 CRL 就可能走「软失败」(继续信任)或者「硬失败」(直接拒绝),行为取决于客户端实现,很不确定。OCSP 更麻烦,要跑一个常驻的响应服务。
我的看法是:小规模内网、证书数量可控的情况下,别配 CRL/OCSP,靠短有效期 + 主动重签来解决吊销问题。证书泄露了就重签一张、把旧的下线,比维护一套吊销基础设施省事得多。真到了几十上百张证书、需要精细控制的规模,再考虑上 CRL,而且要确保分发点在内网可达。
5. 避坑与排查:五条把证书用废的血泪记录
这一章记录的是我和身边团队真实踩过的坑,按「现象 → 原因 → 解决」写,都是生成阶段看不出来、运行阶段才暴露的类型。照着对一遍,能省掉大半天。
5.1 浏览器侧的三个典型报错
坑一:NET::ERR_CERT_AUTHORITY_INVALID,装过根还是报
现象:根证书明明导入了系统信任区,浏览器打开内网站点依旧报颁发机构不受信任。
原因:多数是导入的位置不对。macOS 上用了security add-trusted-cert但漏了-r trustRoot,只导入了没授信;或者导进了登录钥匙串(login.keychain)而浏览器读的是系统钥匙串。Windows 上导进了「个人」而不是「受信任的根证书颁发机构」。
解决:按 3.1 节的命令重导,macOS 带-r trustRoot -k /Library/Keychains/System.keychain,Windows 进Cert:\LocalMachine\Root。导入后重启浏览器(Chrome 对信任区变更的感知有延迟),再不行就清一次浏览器缓存里的 SSL 状态。
坑二:名称不匹配,但 SAN 里明明写了
现象:报ERR_CERT_COMMON_NAME_INVALID,打开证书详情看 SAN 列表里确实有当前域名。
原因:一种是你访问的地址和 SAN 里的值差了一个字符,比如www前缀、尾部的点(api.corp.local.带根点)、或者大小写之外还混了全角字符。另一种是工具把域名写进了 CN 却没写进 SAN,而你看到的「详情」是 CN 的展示。
解决:用 3.4 节的openssl x509 -noout -text直接打印 SAN 扩展,逐字符比对,别信界面预览。确认是 CN/SAN 混淆的话,重签并在工具的 SAN 字段里显式填写。
坑三:证书「尚未生效」,但时间明明是对的
现象:报ERR_CERT_DATE_INVALID,系统时间和证书有效期看起来都正常。
原因:签发机和消费端的时钟有偏差。自签环境里,签发机如果跑在虚拟机里、休眠后时钟漂移,签出来的notBefore会比真实时间晚几分钟甚至几小时,消费端一校验就是「未来才生效」。反过来,消费端时钟慢了也会判过期。
解决:签发前先date对一下,虚拟机开 NTP 同步。已经签坏的证书没法改,只能重签。这类问题最玄学的地方在于同一张证书在不同机器上表现不一致,本质就是各机时钟不同步。
5.2 客户端侧的两个链路问题
坑四:桌面浏览器正常,移动端和 Java 服务报「无法构建证书路径」
现象:服务端配好后,Chrome 访问正常,手机 App 和 Java 客户端调用直接握手失败。
原因:服务端只下发了叶子证书,没带根证书,链不完整。桌面浏览器因为之前导入过根、或者有缓存,能自己补全;移动端和 Java 的信任库是独立的,缺一环就拼不出来。
解决:Nginx 的ssl_certificate指向 fullchain(叶子+根拼在一起),别指向单张叶子。验证方法是openssl s_client -showcerts看输出里是不是有两段证书。Java 客户端这边还要确认根证书已经用 keytool 导进它实际使用的 truststore。
坑五:通配符覆盖不到多级子域
现象:签了*.corp.local,api.corp.local访问正常,v2.api.corp.local报名称不匹配。
原因:通配符只匹配一级子域,这是 RFC 规定,不是工具 bug。
解决:把需要多级子域的服务单独列进 SAN,或者为多级子域单独签*.api.corp.local。如果业务上大量用多级子域,更省事的做法是规划一套扁平域名(api-v2.corp.local),别挑战通配符的边界。
排查这类问题有个通用习惯:先在命令行用openssl s_client拿服务端实际下发的证书,再openssl verify走一遍链验证,两步都过再看应用层日志。这样能把「证书问题」和「应用配置问题」快速切开,不至于在框架配置里瞎找半天。
6. 进阶:把签发塞进流水线并做一次闭环验证
手工签几次之后,重复劳动就来了,尤其是多环境多服务的团队。我的做法是把 ZXCA 的 CLI 包进一个签发脚本,把「根证书」和「服务证书」的生成做成幂等操作:根存在就复用、不存在才建;服务证书按 SAN 列表批量签,输出目录按服务名分。
#!/usr/bin/env bash # issue-all.sh:按服务清单批量签发,根不存在时自动初始化 set -euo pipefail CA_DIR=./zxca-ca OUT_DIR=./certs SAN_LIST=("api.corp.local,192.168.10.20" "admin.corp.local,192.168.10.21") # 根证书不存在才初始化,避免覆盖已有信任锚 if [ ! -f "$CA_DIR/root.crt" ]; then zxca init --ca-name "Local Dev Root CA" --out "$CA_DIR" \ --key-alg rsa --key-size 3072 --validity-days 3650 fi # 逐个服务签发,目录名取 SAN 第一项 for i in "${!SAN_LIST[@]}"; do name="svc-$i" zxca issue --ca "$CA_DIR" \ --common-name "${SAN_LIST[$i]%%,*}" \ --san "${SAN_LIST[$i]}" \ --validity-days 397 --eku serverAuth \ --out "$OUT_DIR/$name" done # 闭环验证:每张证书都跑一次链校验,失败即中断 for crt in "$OUT_DIR"/*/*.crt; do openssl verify -CAfile "$CA_DIR/root.crt" "$crt" done这段脚本的价值不在于省了几次点击,而在于最后那段openssl verify循环——它把「签出来」和「能用」之间那道缝给补上了。很多团队签发脚本跑完就当成功,结果证书链不完整、SAN 漏项的问题一路带到联调阶段才发现。加上这一步,有问题在 CI 里就挂出来,不用等到有人拿着手机来找你。
再往前一步,可以把证书的到期时间纳入监控。用openssl x509 -noout -enddate -in xxx.crt拿到到期日,比对一个阈值,快过期就告警。自签证书最容易出的事故不是签错,是忘了换——某天早上服务集体 502,翻半天日志发现是三个月前就该轮换的根过期了。我吃过一次这个亏,当时根证书的有效期随手写了 90 天,签完就撒手不管,到期当天所有内网 HTTPS 服务同时握手失败,排查从「网络问题」一路查到「证书过期」花了两个多小时。从那以后我每次建根都强制在签发脚本里加一句到期提醒,并且把根有效期和叶子有效期拉开至少一个数量级,给自己留足反应时间。希望帮到你。
本文还有配套的精品资源,点击获取