SagerNet / SagerNet/sing-box

Feature Request:支持从操作系统密钥库加载客户端证书(不可导出私钥)用于 TLS 客户端认证

Open
#4,420 0 comments 0 reactions 0 assignees View on GitHub

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)。
  • 操作系统密钥库可以就地完成签名:
    • macOSSecItemCopyMatching 定位 identity + SecKeyCreateSignature 签名(ECDSA/RSA)——私钥始终留在钥匙串 / SEP 内。
    • Windows:CNG / NCryptSignHash
    • PKCS#11C_Sign,适用于智能卡 / HSM / 令牌(跨平台)。

因此所需的改动是:构造一个 tls.Certificate,其 PrivateKey 是一个由密钥库支撑的 crypto.Signer,而不是去解析 PEM 字节。

可参考的现有实现(先例)

以下库已经把操作系统密钥库中的私钥暴露为 crypto.Signer,可作为设计参考或直接依赖:

  • github.com/github/certstore —— 用统一的 crypto.Signer 风格 API 抽象了 macOS 钥匙串 Windows CNG(被 gitsmimesign 使用)。最贴合跨平台目标。
  • github.com/keybase/go-keychain —— macOS 钥匙串访问(含 SecKeyCreateSignature)。
  • github.com/ThalesGroup/crypto11 / github.com/miekg/pkcs11 —— 面向令牌 / HSM 的 PKCS#11 crypto.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_keychainwindows_cngpkcs11(分阶段实现,优先 macos_keychain)。
  • identity 选择:按证书 SHA-1/SHA-256 指纹,或按 subject CN / issuer+serial 等便捷选择器。
  • 对于 pkcs11:提供 module_pathtoken_labelpin(或 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 做路由 / 分流的意义。

范围 / 最小可行版本

一个已经能解决大多数真实场景的最小起点:

  1. provider: "macos_keychain",按指纹选择 identity。
  2. 把密钥库支撑的 crypto.Signer 接入 openconnect endpoint 的 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.