macOS 与 iOS 使用客户端证书访问 mTLS 网站

本文介绍如何在 macOS 和 iOS 设备上配置客户端证书,以访问启用双向 TLS(mTLS)的 HTTPS 网站。示例使用私有 CA、step-ca、Nginx、PKCS#12 和 PIV 智能卡。文中的域名、证书名称、文件名和 IP 地址均为示例,请替换为自己的实际值。

1. mTLS 认证流程

普通 HTTPS 主要由客户端验证服务器证书;mTLS 还要求服务器验证客户端证书。

基本流程:

  1. 客户端访问 HTTPS 站点。
  2. Nginx 在 TLS 握手期间请求客户端证书。
  3. 浏览器或系统选择客户端身份,并使用对应私钥完成签名。
  4. Nginx 根据配置验证客户端证书、证书链和有效期。
  5. 验证通过后,连接进入 HTTP 层,由站点策略决定是否允许访问。

客户端身份和服务器 CA 信任是两个独立配置:

  • 客户端身份:客户端证书及与之匹配的私钥,用于向服务器证明身份。
  • 服务器 CA 信任:客户端信任签发网站服务器证书的 CA,用于验证 HTTPS 服务器身份。

安装客户端证书不会自动让设备信任私有 CA。

2. macOS:使用 PIV 硬件密钥

如果客户端私钥保存在支持 PIV 的硬件密钥中,可以让私钥留在硬件设备内,避免导出私钥文件。浏览器是否能使用该身份,取决于 macOS、智能卡中间件、密钥设备和浏览器的支持情况。

一般检查步骤:

  1. 插入 PIV 硬件密钥。
  2. 确认 macOS 能识别智能卡及其证书。
  3. 打开浏览器并访问 mTLS 网站。
  4. 如果浏览器请求客户端证书,选择对应的身份。
  5. 按提示输入 PIN 或完成硬件操作。
  6. 结合页面结果和服务器日志确认认证是否成功。

可用以下命令检查系统状态:

security list-smartcards
sc_auth identities
sudo security smartcards token -l

不同 macOS 版本、硬件和中间件的输出可能不同。命令没有输出并不能单独证明证书无效。

3. macOS:将 CRT、PEM 和 KEY 导入钥匙串

如果已有客户端证书和对应私钥文件,可以将它们打包成 PKCS#12 文件(.p12),再导入 macOS 钥匙串。

示例文件:

  • client.crt:客户端证书。
  • client.key:与证书匹配的私钥。
  • intermediate-ca.crt:中间 CA 证书,可选。
  • root-ca.crt:根 CA 证书,用于验证证书链或配置服务器信任。

请在受控目录中操作,并避免将私钥或 PKCS#12 文件提交到代码仓库。

3.1 检查客户端证书

openssl x509 -in client.crt -noout \
  -subject -issuer -serial -dates

检查证书用途:

openssl x509 -in client.crt -noout -ext extendedKeyUsage

证书应包含 TLS Web Client Authentication,用途 OID 为 1.3.6.1.5.5.7.3.2。

验证证书与私钥是否匹配:

openssl x509 -in client.crt -pubkey -noout |
  openssl pkey -pubin -outform DER |
  shasum -a 256

openssl pkey -in client.key -pubout -outform DER |
  shasum -a 256

两条命令输出的摘要应一致。

如果有中间 CA,可以验证证书链:

openssl verify \
  -CAfile root-ca.crt \
  -untrusted intermediate-ca.crt \
  client.crt

如果没有中间 CA:

openssl verify -CAfile root-ca.crt client.crt

证书链有多级中间 CA 时,应提供完整且正确的中间证书链。

3.2 生成兼容 macOS 钥匙串的 PKCS#12 文件

部分 PKCS#12 文件虽然密码正确,仍可能因加密参数兼容性问题而无法被钥匙串导入。可以显式指定传统 PKCS#12 加密参数进行兼容性导出。

如果有中间 CA 证书:

umask 077

openssl pkcs12 -export \
  -inkey client.key \
  -in client.crt \
  -certfile intermediate-ca.crt \
  -name "Client Certificate" \
  -keypbe PBE-SHA1-3DES \
  -certpbe PBE-SHA1-3DES \
  -macalg sha1 \
  -out client-macos.p12

如果没有中间 CA,删除 -certfile intermediate-ca.crt 这一行。

执行时设置一个非空的导出密码。建议先使用英文字母和数字,减少密码编码差异造成的排查干扰。

验证文件:

openssl pkcs12 -info -in client-macos.p12 -noout

输入刚刚设置的密码。如果 OpenSSL 可以读取文件,但钥匙串仍然拒绝导入,可优先检查 PKCS#12 加密参数兼容性。

这里的 SHA-1 和 3DES 仅用于 PKCS#12 文件封装兼容性,不代表应在 TLS 连接中启用旧算法。它们不会改变客户端证书的公钥、私钥或扩展用途。

3.3 导入钥匙串

打开钥匙串访问:

open -a "Keychain Access"

然后:

  1. 选择 login 钥匙串。
  2. 选择“文件 → 导入项目”。
  3. 选择 client-macos.p12。
  4. 输入导出密码。
  5. 在“我的证书(My Certificates)”中找到对应身份。
  6. 展开证书,确认其下存在对应私钥。

也可以通过命令行导入:

security import ./client-macos.p12 \
  -k "$HOME/Library/Keychains/login.keychain-db" \
  -f pkcs12

检查可用于 SSL 客户端认证的身份:

security find-identity -v -p ssl-client \
  "$HOME/Library/Keychains/login.keychain-db"

如果没有有效身份,检查证书和私钥是否匹配、证书是否有效,以及是否具备客户端认证用途。

4. 配置服务器 CA 信任

如果网站使用私有 CA 签发服务器证书,客户端还必须信任正确的根 CA。

先检查根证书信息和指纹:

openssl x509 -in root-ca.crt -noout \
  -subject -issuer -fingerprint -sha256

确认来源和指纹后,再将根 CA 导入相应设备的证书信任存储。若需要系统范围的信任,可在 macOS 的 System 钥匙串中安装,并按实际用途配置信任;这通常需要管理员权限。

不要将客户端证书当作根 CA,也不要把 CA 私钥安装到客户端设备。服务器应正确发送必要的中间证书。

5. 使用 curl 验证 mTLS

在测试浏览器之前,可以用 curl 验证证书、私钥和服务器配置。

curl -v \
  --cert client.crt \
  --key client.key \
  --cacert root-ca.crt \
  https://mtls.example.test/

如果需要指定服务器 IP,同时保留正确的主机名和 SNI:

curl -v \
  --resolve mtls.example.test:443:192.0.2.10 \
  --cert client.crt \
  --key client.key \
  --cacert root-ca.crt \
  https://mtls.example.test/

示例 IP 192.0.2.10 是文档专用地址,请替换为实际服务器 IP。

如果服务器要求客户端发送中间 CA 证书,可将客户端证书与中间证书合并:

cat client.crt intermediate-ca.crt > client-chain.pem

然后将 --cert client.crt 替换为 --cert client-chain.pem。

常见结果:

  • HTTP 200:请求成功,通常表示 TLS 和应用请求均已通过。
  • HTTP 401/403:可能已完成 TLS,但被应用层授权策略拒绝。
  • certificate required:服务器要求客户端证书,但没有收到可用证书。
  • unknown ca:服务端可能不信任客户端证书的签发 CA。
  • certificate verify failed:检查服务器证书链、根 CA 和域名。

应结合客户端输出和 Nginx 日志判断。单凭 HTTP 状态码不能完全证明 mTLS 的每个环节都正确。

6. 在 macOS Chrome 和 Safari 中测试

Chrome

  1. 确认客户端身份已安装,且钥匙串中存在匹配私钥。
  2. 完全退出并重新打开 Chrome。
  3. 访问 mTLS 网站。
  4. 如果浏览器请求客户端证书,选择正确身份。
  5. 检查页面结果和服务器日志。

如果证书未出现,检查证书用途、私钥可用性,以及服务器是否在当前 TLS 握手中请求客户端证书。

Safari

用相同 URL 单独测试。Safari 和 Chrome 的证书选择行为可能不同,不应假定一个浏览器成功就意味着另一个也会使用同一身份。

如果出现类似“没有发送所需客户端证书”的错误,应检查浏览器是否选择并发送了正确身份、系统是否识别该身份,以及服务器在该次握手中是否收到客户端证书。

7. iOS:安装客户端身份

iPhone 可以使用独立的客户端证书。常见做法是在受控环境中生成专用私钥和 CSR,由现有 CA 签发客户端证书,再导出 PKCS#12 文件供 iOS 安装。

7.1 生成私钥和 CSR

mkdir -p ~/ios-mtls
cd ~/ios-mtls
umask 077

生成 P-256 私钥:

openssl ecparam \
  -genkey \
  -name prime256v1 \
  -noout \
  -out ios-client.key

生成 CSR:

openssl req \
  -new \
  -key ios-client.key \
  -subj "/CN=Mobile Client" \
  -out ios-client.csr

请根据实际设备和证书管理规范设置合适的身份名称。

7.2 使用 step-ca 签发

step ca sign \
  ios-client.csr \
  ios-client.crt \
  --not-after=8760h

实际签发有效期受 CA 策略约束。签发后检查证书:

openssl x509 \
  -in ios-client.crt \
  -noout \
  -subject \
  -issuer \
  -dates \
  -ext extendedKeyUsage

确认签发者、有效期和客户端认证用途符合预期。如果没有 clientAuth,应先修正 CA 的签发策略。

7.3 导出 PKCS#12

如果有中间 CA 证书:

openssl pkcs12 -export \
  -inkey ios-client.key \
  -in ios-client.crt \
  -certfile intermediate-ca.crt \
  -name "Mobile Client" \
  -keypbe PBE-SHA1-3DES \
  -certpbe PBE-SHA1-3DES \
  -macalg sha1 \
  -out ios-client.p12

如果没有中间 CA,删除 -certfile intermediate-ca.crt 参数。

为文件设置非空密码,并在本机验证:

openssl pkcs12 -info -in ios-client.p12 -noout

不要将根 CA 或中间 CA 的私钥打包进客户端文件。PKCS#12 中只应包含该客户端身份所需的私钥、客户端证书和必要的证书链。

7.4 安装到 iPhone

  1. 通过受控方式将 .p12 文件传到 iPhone。
  2. 打开文件并按照系统提示安装证书身份或配置描述文件。
  3. 输入 PKCS#12 导出密码。
  4. 在系统设置的相关描述文件或设备管理页面确认安装结果。
  5. 确认安装的是包含客户端证书和私钥的身份,而不是只有公开证书。

不同 iOS 版本的界面文字可能略有差异。如果网站使用私有 CA 签发的服务器证书,还需要单独安装并按系统要求信任根 CA。

7.5 通过 Safari 访问

  1. 确认 iPhone 已连接家庭网络或可以访问内网的 VPN。
  2. 在 Safari 中打开 mTLS 网站。
  3. 当系统或浏览器请求客户端身份时,选择对应证书。
  4. 检查页面结果和服务器日志。

如果访问失败,分别排查 DNS、网络可达性、服务器证书信任、客户端身份安装情况,以及服务器端验证日志。

8. iOS 浏览器和应用兼容性

iOS 上的浏览器和其他应用是否能使用系统安装的客户端证书,取决于应用实现、操作系统接口和具体版本。不能仅凭证书已安装,就断定所有应用都能使用它。

建议逐个验证目标应用:

  • Safari:先验证系统客户端身份是否可用。
  • Chrome:单独测试当前版本对客户端证书的支持情况。
  • 其他应用:确认应用是否支持 mTLS、是否能访问系统身份,以及是否支持目标服务器地址。

如果某个应用不能调用系统身份,重新签发证书或改变密钥算法通常不能单独解决应用层的访问限制。

9. 证书与私钥安全

客户端证书通常可以公开,但对应私钥必须保护好。

  • .key 文件和 .p12 文件都应视为敏感材料。
  • 不要将私钥或 PKCS#12 文件提交到公开仓库、博客附件或不受信任的文件分享服务。
  • 传输完成后,检查临时目录、下载目录和文件传输记录中是否还保留不必要的副本。
  • 如需备份,应采用加密方式妥善保存。
  • 私钥泄露后,应撤销或停止信任对应客户端证书,并重新生成密钥和证书。
  • 通过硬件密钥保存私钥时,应尽量避免导出私钥。

将私钥从 Mac 导入手机的方案,不等同于在手机的安全硬件中生成且不可导出的硬件绑定私钥。

10. 故障排查顺序

第一步:检查网络和 DNS

确认客户端能解析目标域名,并能连接到 Nginx 的 HTTPS 端口。

第二步:检查服务器证书信任

确认客户端信任网站服务器证书的签发 CA,且服务器证书的域名、有效期和证书链正确。

第三步:检查客户端身份

确认客户端证书有效、包含 clientAuth 用途,并且对应私钥可用。macOS 钥匙串方案可执行:

security find-identity -v -p ssl-client \
  "$HOME/Library/Keychains/login.keychain-db"

第四步:检查 Nginx

检查 ssl_client_certificate 指向的 CA 文件是否包含正确的客户端证书签发链,并确认 ssl_verify_client 和 ssl_verify_depth 与实际 CA 层级相符。

第五步:区分未发送证书与验证失败

  • No required SSL certificate was sent:通常表示该次 TLS 握手没有收到服务器要求的客户端证书。
  • 客户端证书验证失败:可能与证书链、有效期、用途或服务端信任配置有关。

结合 Nginx error log 和客户端实际行为判断,不要只依据浏览器错误页面下结论。

11. 方案总结

平台客户端身份方案注意事项
macOS ChromePIV 硬件密钥或钥匙串身份单独验证证书选择和硬件支持
macOS SafariPIV 硬件密钥或钥匙串身份不假设与 Chrome 行为完全一致
iOS Safari系统安装的 PKCS#12 身份另行确认服务器 CA 信任
iOS Chrome取决于应用和系统版本的支持情况必须在目标版本上实际测试

推荐做法是:Mac 优先考虑将私钥保存在 PIV 硬件密钥中;需要软件身份时,使用兼容的 PKCS#12 文件导入钥匙串;iPhone 使用独立客户端身份,并通过目标应用逐一验证。

如果后续需要为密码管理器等原生应用启用 mTLS,应将该应用自身的网络请求认证与浏览器访问网站的 mTLS 分开设计。