Feature Request:支持从操作系统密钥库加载客户端证书(不可导出私钥)用于 TLS 客户端认证
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 38.1k
- Forks
- 4.6k
- Avg merge
- 19d 15h
- Merged PRs (30d)
- 1
Description
摘要
希望 sing-box 能使用**私钥存储在操作系统密钥库、且不可导出(non-extractable)**的客户端证书来完成 TLS 客户端认证(mTLS)——首先支持 macOS 钥匙串(Keychain),并最好扩展到 Windows(CNG/NCrypt)与 PKCS#11。
目前 openconnect endpoint 的 tls 配置(以及一般的 TLS 客户端证书配置)只接受通过 client_key / client_key_path 提供的 PEM 私钥。这导致由 MDM / 企业 VPN 客户端下发、私钥被刻意标记为不可导出、永远无法导出成 PEM 的证书完全无法使用。
动机 / 使用场景
我需要通过 openconnect endpoint 连接一个 GlobalProtect 网关。该网关要求客户端证书认证。sing-box 握手失败并报错:
OpenConnect Client (GlobalProtect): authentication failed | Valid client certificate is required
该客户端证书由企业 VPN 客户端下发,存放在 macOS 的 login / System 钥匙串 中。具体情况:
- 证书本身可以正常导出为
.cer(仅公钥部分)。 - 将 identity 导出为
.p12/ PKCS#12 时失败:
即私钥不可导出(security export -k ~/Library/Keychains/login.keychain-db -t identities -f pkcs12 -o out.p12 security: SecKeychainItemExport: The contents of this item cannot be retrieved.kSecAttrIsExtractable = false),由钥匙串 / Secure Enclave 存储层强制。任何工具、参数或口令都无法把它导出——这是设计使然。
由于向 sing-box 提供客户端私钥的唯一途径是 PEM 文件 / 字符串,目前根本无法使用这类证书,尽管:
- 系统原生 VPN 客户端用完全相同的这张证书可以成功连接。
- 私钥根本不需要离开密钥库——TLS 客户端认证只需要对握手记录(
CertificateVerify)做一次签名,而这次签名操作系统可以就地完成。
这是非常普遍的企业场景:由 Jamf/Intune/SCEP/ACME、GlobalProtect、Cisco AnyConnect 等签发的证书,其私钥几乎总是不可导出的。
为什么技术上可行(且无需导出私钥)
TLS 客户端认证并不要求 sing-box 持有私钥字节,它只需要在握手过程中产出一个签名。Go 标准库已经很干净地支持这一点:
tls.Certificate.PrivateKey可以是任何实现了crypto.Signer接口的值(不必是真实的*ecdsa.PrivateKey/*rsa.PrivateKey)。- 操作系统密钥库可以就地完成签名:
- macOS:
SecItemCopyMatching定位 identity +SecKeyCreateSignature签名(ECDSA/RSA)——私钥始终留在钥匙串 / SEP 内。 - Windows:CNG /
NCryptSignHash。 - PKCS#11:
C_Sign,适用于智能卡 / HSM / 令牌(跨平台)。
- macOS:
因此所需的改动是:构造一个 tls.Certificate,其 PrivateKey 是一个由密钥库支撑的 crypto.Signer,而不是去解析 PEM 字节。
可参考的现有实现(先例)
以下库已经把操作系统密钥库中的私钥暴露为 crypto.Signer,可作为设计参考或直接依赖:
github.com/github/certstore—— 用统一的crypto.Signer风格 API 抽象了 macOS 钥匙串和 Windows CNG(被git的smimesign使用)。最贴合跨平台目标。github.com/keybase/go-keychain—— macOS 钥匙串访问(含SecKeyCreateSignature)。github.com/ThalesGroup/crypto11/github.com/miekg/pkcs11—— 面向令牌 / HSM 的 PKCS#11crypto.Signer(跨平台,不与具体 OS API 强耦合)。github.com/google/go-pkcs11—— 另一个 PKCS#11 方案。
说明:certstore / macOS / Windows 后端需要 cgo。若对 cgo / 维护成本有顾虑,PKCS#11 是最可移植的选项。
建议的配置
在 TLS 选项中新增一个由密钥库支撑的客户端私钥来源,与 client_key / client_key_path 互斥。草案:
{
"type": "openconnect",
"tag": "gp-vpn",
"server": "https://vpn.example.com",
"flavor": "gp",
"tls": {
"client_certificate_store": {
"provider": "macos_keychain",
"identity_sha1": "<客户端证书的 SHA-1 指纹>"
}
}
}
设计要点:
provider:取值macos_keychain、windows_cng、pkcs11(分阶段实现,优先macos_keychain)。- identity 选择:按证书 SHA-1/SHA-256 指纹,或按 subject CN / issuer+serial 等便捷选择器。
- 对于
pkcs11:提供module_path、token_label、pin(或pin_env)、key_id/key_label等字段。 - 当设置了
client_certificate_store时,sing-box 用密钥库中读取的叶证书 + 密钥库支撑的crypto.Signer构造tls.Certificate;此时client_key*必须为空。 - 平台相关后端应通过 build tag 隔离,使无关平台不受影响,cgo 成本按需启用。
该机制首先应用于 openconnect endpoint 的 tls 块,但同样的能力对任何需要客户端证书的 sing-box TLS 客户端都有用。
已考虑的替代方案
- 导出私钥为 PEM —— 不可行:私钥不可导出(
SecKeychainItemExport返回 "The contents of this item cannot be retrieved")。这正是 MDM / 企业下发的初衷。 - 重新签发一张自持明文私钥的证书 —— 并非总能做到;企业 CA / MDM 控制签发流程并强制私钥不可导出,且这会破坏用户被要求遵守的安全模型。
- 改用系统原生 VPN 客户端 —— 那就失去了用 sing-box 做路由 / 分流的意义。
范围 / 最小可行版本
一个已经能解决大多数真实场景的最小起点:
provider: "macos_keychain",按指纹选择 identity。- 把密钥库支撑的
crypto.Signer接入openconnectendpoint 的 TLS 客户端配置。
Windows CNG 与 PKCS#11 可作为后续增量,共用同一套配置结构。
环境信息
- Endpoint:
openconnect,GlobalProtect 网关 - 客户端证书:EC(P-256)identity,签发者为企业 VPN CA,私钥在 login / System 钥匙串中不可导出
- 观察到的报错:
OpenConnect Client: authentication failed | Valid client certificate is required
补充说明
- 该证书的公钥为 EC(
id-ecPublicKey),因此所需的就地操作是对握手记录做一次 ECDSA 签名。 - 系统原生客户端用同一张证书能成功认证,证明网关配置与证书本身都有效——唯一的缺口是 sing-box 无法使用不可导出的私钥。
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the openconnect endpoint's tls configuration and the Go crypto.Signer and tls.Certificate interfaces described in the issue. Review github.com/github/certstore and the macOS Keychain signing APIs, then define the smallest macOS fingerprint-selected provider. Done means an identity with a non-exportable private key can perform TLS client authentication without client_key or client_key_path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, macos
- Domain
- authentication, networking, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100