mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
Merge pull request #299 from leookun/fix/issue-298-per-user-ca
Fix shared root CA by generating one per installation
This commit is contained in:
@@ -28,6 +28,7 @@ type exchangeContext struct {
|
||||
type Server struct {
|
||||
config Config
|
||||
certManager *certs.Manager
|
||||
caCertPEM []byte
|
||||
store *exchangeStore
|
||||
counter atomic.Uint64
|
||||
proxyServer *http.Server
|
||||
@@ -43,13 +44,14 @@ func New(config Config) (*Server, error) {
|
||||
if err := validateLoopbackAddress(config.UIAddr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager, err := certs.NewEmbeddedManager()
|
||||
manager, caCertPEM, err := certs.NewGeneratedManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 MITM CA 失败:%w", err)
|
||||
}
|
||||
server := &Server{
|
||||
config: config,
|
||||
certManager: manager,
|
||||
caCertPEM: caCertPEM,
|
||||
store: newExchangeStore(config.MaxExchanges),
|
||||
}
|
||||
proxyHandler, err := server.newProxyHandler()
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cursor/internal/certs"
|
||||
)
|
||||
|
||||
//go:embed web/*
|
||||
@@ -93,7 +91,7 @@ func (server *Server) handleEvents(writer http.ResponseWriter, request *http.Req
|
||||
func (server *Server) handleCACertificate(writer http.ResponseWriter, _ *http.Request) {
|
||||
writer.Header().Set("Content-Type", "application/x-x509-ca-cert")
|
||||
writer.Header().Set("Content-Disposition", `attachment; filename="cursor-local-proxy-ca.crt"`)
|
||||
_, _ = writer.Write(certs.EmbeddedCACertPEM())
|
||||
_, _ = writer.Write(server.caCertPEM)
|
||||
}
|
||||
|
||||
func writeJSON(writer http.ResponseWriter, status int, payload any) {
|
||||
|
||||
+14
-13
@@ -70,20 +70,21 @@ func Run(resources EmbeddedResources) error {
|
||||
logger.Init()
|
||||
netproxy.InstallDefaultTransport()
|
||||
|
||||
embeddedCACertPEM := certs.EmbeddedCACertPEM()
|
||||
logEmbeddedCAInfo(embeddedCACertPEM)
|
||||
|
||||
certManager, err := certs.NewEmbeddedManager()
|
||||
if err := appdata.EnsureAssistantHome(); err != nil {
|
||||
return err
|
||||
}
|
||||
certManager, caCertPEM, err := certs.LoadOrCreateManager(appdata.CACertFilePath(), appdata.CAKeyFilePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logCAInfo(caCertPEM)
|
||||
|
||||
defaultBackendBaseURL := "http://" + serverconfig.DefaultBackendListenAddr
|
||||
proxyServer, err := mitm.NewProxyServer(serverconfig.DefaultProxyListenAddr, defaultBackendBaseURL, "", "", certManager)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proxyService := bridge.NewProxyService(proxyServer, certManager, embeddedCACertPEM)
|
||||
proxyService := bridge.NewProxyService(proxyServer, certManager, caCertPEM)
|
||||
adAssetBaseURL := defaultBackendBaseURL
|
||||
if cfg, err := proxyService.LoadUserConfig(); err == nil {
|
||||
adAssetBaseURL = browserReachableLoopbackBaseURL(cfg.BackendListenAddr)
|
||||
@@ -442,20 +443,20 @@ func browserReachableLoopbackBaseURL(listenAddr string) string {
|
||||
return "http://" + net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
// logEmbeddedCAInfo 用于处理与 logEmbeddedCAInfo 相关的逻辑。
|
||||
func logEmbeddedCAInfo(certPEM []byte) {
|
||||
// logCAInfo 记录当前安装专属 CA 的公开信息。
|
||||
func logCAInfo(certPEM []byte) {
|
||||
if len(certPEM) == 0 {
|
||||
logger.Errorf("embedded CA is empty")
|
||||
logger.Errorf("installation CA is empty")
|
||||
return
|
||||
}
|
||||
cert, err := parseEmbeddedCert(certPEM)
|
||||
cert, err := parseCert(certPEM)
|
||||
if err != nil {
|
||||
logger.Errorf("parse embedded CA failed: %v", err)
|
||||
logger.Errorf("parse installation CA failed: %v", err)
|
||||
return
|
||||
}
|
||||
sum := sha256.Sum256(cert.Raw)
|
||||
logger.Infof(
|
||||
"embedded CA loaded: sha256=%s subject=%s valid=%s~%s",
|
||||
"installation CA loaded: sha256=%s subject=%s valid=%s~%s",
|
||||
strings.ToUpper(hex.EncodeToString(sum[:])),
|
||||
cert.Subject.String(),
|
||||
cert.NotBefore.Format(time.RFC3339),
|
||||
@@ -463,8 +464,8 @@ func logEmbeddedCAInfo(certPEM []byte) {
|
||||
)
|
||||
}
|
||||
|
||||
// parseEmbeddedCert 用于处理与 parseEmbeddedCert 相关的逻辑。
|
||||
func parseEmbeddedCert(data []byte) (*x509.Certificate, error) {
|
||||
// parseCert 解析 DER 或 PEM 编码的证书。
|
||||
func parseCert(data []byte) (*x509.Certificate, error) {
|
||||
if block, _ := pem.Decode(data); block != nil {
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
@@ -70,3 +70,8 @@ func LogsRootPath() string {
|
||||
func CACertFilePath() string {
|
||||
return filepath.Join(DataRootPath(), "ca.crt")
|
||||
}
|
||||
|
||||
// CAKeyFilePath 返回仅供本安装使用的 CA 私钥路径。
|
||||
func CAKeyFilePath() string {
|
||||
return filepath.Join(DataRootPath(), "ca.key")
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ internal/backend/
|
||||
|
||||
- `~/.cursor-local-assistant-v2/config.yaml`
|
||||
- `~/.cursor-local-assistant-v2/data/ca.crt`
|
||||
- `~/.cursor-local-assistant-v2/data/ca.key`
|
||||
- `~/.cursor-local-assistant-v2/data/ads/`
|
||||
- `~/.cursor-local-assistant-v2/history/`
|
||||
- `~/.cursor-local-assistant-v2/logs/`
|
||||
@@ -104,7 +105,8 @@ internal/backend/
|
||||
约定:
|
||||
|
||||
- `config.yaml` 是用户配置
|
||||
- `data/ca.crt` 是注入给宿主的 CA 证书
|
||||
- `data/ca.crt` 是首次运行时为当前用户生成、注入给宿主的 CA 证书
|
||||
- `data/ca.key` 是与该证书配套的本地私钥,权限固定为 `0600`,不得打包或提交到仓库
|
||||
- `data/ads/` 是广告包与资源缓存目录
|
||||
- `history/` 是会话事实与全局 usage JSON 目录,不属于日志
|
||||
- `logs/` 只保留必要文本运行日志
|
||||
|
||||
+42
-44
@@ -1,6 +1,7 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
@@ -9,33 +10,24 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
_ "embed"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// embeddedCACertPEM 表示当前模块中的 embeddedCACertPEM 状态值。
|
||||
//
|
||||
//go:embed ca.crt
|
||||
var embeddedCACertPEM []byte
|
||||
|
||||
// embeddedCAKeyPEM 表示当前模块中的 embeddedCAKeyPEM 状态值。
|
||||
//
|
||||
//go:embed ca.key
|
||||
var embeddedCAKeyPEM []byte
|
||||
|
||||
// Manager 定义了当前模块中的 Manager 类型。
|
||||
type Manager struct {
|
||||
// caCert 表示当前声明中的 caCert。
|
||||
caCert *x509.Certificate
|
||||
// caKey 表示当前声明中的 caKey。
|
||||
caKey crypto.PrivateKey
|
||||
// caCertPEM 保存可注入宿主信任存储的 CA 证书,不包含私钥。
|
||||
caCertPEM []byte
|
||||
|
||||
// mu 表示当前声明中的 mu。
|
||||
mu sync.Mutex
|
||||
@@ -52,28 +44,26 @@ func NewManager(caCertPath, caKeyPath string) (*Manager, error) {
|
||||
return NewManagerFromPEM(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
// NewEmbeddedManager 用于处理与 NewEmbeddedManager 相关的逻辑。
|
||||
func NewEmbeddedManager() (*Manager, error) {
|
||||
return NewManagerFromPEM(embeddedCACertPEM, embeddedCAKeyPEM)
|
||||
}
|
||||
|
||||
// EmbeddedCACertPEM 用于处理与 EmbeddedCACertPEM 相关的逻辑。
|
||||
func EmbeddedCACertPEM() []byte {
|
||||
return cloneBytes(embeddedCACertPEM)
|
||||
}
|
||||
|
||||
// EmbeddedCAKeyPEM 用于处理与 EmbeddedCAKeyPEM 相关的逻辑。
|
||||
func EmbeddedCAKeyPEM() []byte {
|
||||
return cloneBytes(embeddedCAKeyPEM)
|
||||
}
|
||||
|
||||
// NewManagerFromPEM 用于处理与 NewManagerFromPEM 相关的逻辑。
|
||||
func NewManagerFromPEM(caCertPEM, caKeyPEM []byte) (*Manager, error) {
|
||||
caCert, caKey, err := loadCAFromPEM(caCertPEM, caKeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Manager{caCert: caCert, caKey: caKey, cache: make(map[string]*tls.Certificate)}, nil
|
||||
return &Manager{
|
||||
caCert: caCert,
|
||||
caKey: caKey,
|
||||
caCertPEM: cloneBytes(caCertPEM),
|
||||
cache: make(map[string]*tls.Certificate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CACertPEM 返回当前 Manager 使用的 CA 证书。返回值不包含私钥。
|
||||
func (m *Manager) CACertPEM() []byte {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneBytes(m.caCertPEM)
|
||||
}
|
||||
|
||||
// CATLSCertificate 用于处理与 CATLSCertificate 相关的逻辑。
|
||||
@@ -185,19 +175,6 @@ func marshalPrivateKeyPEM(key any) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// loadCAPEMFromFiles 用于处理与 loadCAPEMFromFiles 相关的逻辑。
|
||||
func loadCAPEMFromFiles(certPath, keyPath string) ([]byte, []byte, error) {
|
||||
certPEM, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
keyPEM, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// loadCAFromPEM 用于处理与 loadCAFromPEM 相关的逻辑。
|
||||
func loadCAFromPEM(certPEM, keyPEM []byte) (*x509.Certificate, crypto.PrivateKey, error) {
|
||||
certBlock, _ := pem.Decode(certPEM)
|
||||
@@ -214,28 +191,49 @@ func loadCAFromPEM(certPEM, keyPEM []byte) (*x509.Certificate, crypto.PrivateKey
|
||||
return nil, nil, errors.New("invalid CA key PEM")
|
||||
}
|
||||
|
||||
var caKey crypto.PrivateKey
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return caCert, key, nil
|
||||
caKey = key
|
||||
case "EC PRIVATE KEY":
|
||||
key, err := x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return caCert, key, nil
|
||||
caKey = key
|
||||
case "PRIVATE KEY":
|
||||
key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return caCert, key, nil
|
||||
caKey = key
|
||||
default:
|
||||
return nil, nil, errors.New("unsupported CA key format")
|
||||
}
|
||||
|
||||
if !caCert.IsCA || !caCert.BasicConstraintsValid || caCert.KeyUsage&x509.KeyUsageCertSign == 0 {
|
||||
return nil, nil, errors.New("certificate is not a valid signing CA")
|
||||
}
|
||||
signer, ok := caKey.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, nil, errors.New("CA private key cannot sign certificates")
|
||||
}
|
||||
certPublicKey, err := x509.MarshalPKIXPublicKey(caCert.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("marshal CA certificate public key: %w", err)
|
||||
}
|
||||
privatePublicKey, err := x509.MarshalPKIXPublicKey(signer.Public())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("marshal CA private key public key: %w", err)
|
||||
}
|
||||
if !bytes.Equal(certPublicKey, privatePublicKey) {
|
||||
return nil, nil, errors.New("CA certificate and private key do not match")
|
||||
}
|
||||
return caCert, caKey, nil
|
||||
}
|
||||
|
||||
// normalizeHost 用于处理与 normalizeHost 相关的逻辑。
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAyh3jND/aFusuRjGTmhQtX2hkF1qroNjEEKCxWPlfprvdl8Tx
|
||||
upxNv1TQcm+K9KsS7OxnKYP4Qtv068XLbaCCGuoA/xpor6enrT85KulBq0j8z/g1
|
||||
y0VWxjz3xN9F9ND13h9yxDZCn76egbRkhFxpXow67jLsIlrqWDSltERlTKh2cJ1g
|
||||
hTRuQNSr7jtKQgFAR3aQ6dTzOP4fCOtLYn63jL4+YcxdGoK66tx8eFHq8oBLYvsc
|
||||
LNjjkaAHilZFwA4Jr7zHBofTRBg04eZum2UTaRqmSyT65ifXm4vRdQa4k1FS1gnq
|
||||
hPMhIjST8b6RkxvbLNmFpkOfI9eCMRpFG7y+NQIDAQABAoIBAATU9ZVOcHmLSkop
|
||||
zcBJerM09O2dAIziGb/XA55fqdJ728aQ0gGW0oIANlKCCaWjQFrTJP04VzNL/F01
|
||||
l5EpnOqlTPxMRpPqc2cAI677sBL29fpH0gtnvzUSiI7Xkp3RcAtNH6qCrJGSlkn+
|
||||
BMgoSGmW+yKuK3h/yWnt6kc2umA8fN+bHKhS3pI56PMW8qVnny9n92RaCA7Uf/4j
|
||||
XDewIreiH5jRqRwrPbOpjDFmv+W18LWZQiTwwxfY6sRZiZpsfsHidzfFGUFZXMlq
|
||||
2P3FCqoF4oMM1rRgBlHhDR7JHmSkFpZG639HJTXLllpyDbj3H86nsjIj9WZYKn+h
|
||||
B9k9bcECgYEA1HDzRCqoRZh6cL46KiYjv+LwmEKMa3nPb4ljywgLbRkzTMyVs0MK
|
||||
fDsDoTLFY9PBhveypU5gjTbQqtyBsDtbU+dH9Eks1FdL9P8bJf0ZOJFRiEB7uB4a
|
||||
z3V9tcXHwH4l2bCWbWThQGFwRudDAoY0EH89oSA/WHayjOKe1Wi4ovUCgYEA848B
|
||||
cYi+Qbkk+fOv9gSJn8KS1LH/jE28S/e7E4YkTfYUuu+7wr8bdRKUNpPIsLX0Fo9R
|
||||
KpJX0Oyjjady9n/8ARZRmD8Upl+F7Sl7Ro6F8+nfqQUrxbVDiIL3b+aZF+cDfFrB
|
||||
/xL5kyZZqTtFfP360tfYnlS6Sssd4E2Jsj0fJkECgYEA0gG+WbqZkgMDtwQ114jQ
|
||||
elZLZRkUWwKVjzsQDZssQHNTBS6RJh619M0Z73aTLvYcL+IZFdT/GVoAuYc2JRLo
|
||||
W28c8F6OFHMfwVeWbN1g20y8fqbQJtiLxF3vIYwcxStvG123tvisu8oXBeCDm7Ez
|
||||
MsO2FtwcAsWECEXWojzdmSkCgYEA18GvPawtHnuszd+Z2Q5b/DKZb+Hex6N1Ura5
|
||||
+qmyL333D0Kfyf0RjbxPn6l690+4UuPSuyu4r1Nx72KO7N6jlzL2RTBcUqX8NgOx
|
||||
OOe4skJT5561EAdrM9sQ5wgYRpxW8ipUAGoGvNwUQV5ISFmVgIHFWz0jam5UoQcP
|
||||
G94ZYgECgYBN4PB8BuAwMRqhTCLL5RJKtcdC/Ls9xdWrmswtQQ4OSBUTLGdeMhEN
|
||||
E23F0d+NdtiTWDYRDJ7z6KAF8CcOwWMt2sNbrxgNRuDzyfLqbuVLOM8iY3xA50Nl
|
||||
clZWujKILWp47+gA6/AqJLv2lA2LK8gM8zYzzm/nCuLCBLYH/hOiCA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,191 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const legacySharedCASHA256 = "836E6BB84F6C3E63316DBB4EC257223AF09F7490E7AAE09030B8515ED61EE9FF"
|
||||
|
||||
// LoadOrCreateManager loads the installation-specific CA, generating it on
|
||||
// first run. The private key is persisted only in the supplied local path.
|
||||
func LoadOrCreateManager(certPath, keyPath string) (*Manager, []byte, error) {
|
||||
certPEM, certErr := os.ReadFile(certPath)
|
||||
keyPEM, keyErr := os.ReadFile(keyPath)
|
||||
|
||||
if certErr == nil && isLegacySharedCA(certPEM) {
|
||||
return generateAndPersistManager(certPath, keyPath)
|
||||
}
|
||||
if certErr == nil && keyErr == nil {
|
||||
manager, err := NewManagerFromPEM(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load installation CA: %w", err)
|
||||
}
|
||||
if err := os.Chmod(keyPath, 0o600); err != nil {
|
||||
return nil, nil, fmt.Errorf("restrict installation CA private key permissions: %w", err)
|
||||
}
|
||||
return manager, manager.CACertPEM(), nil
|
||||
}
|
||||
if errors.Is(certErr, os.ErrNotExist) && errors.Is(keyErr, os.ErrNotExist) {
|
||||
return generateAndPersistManager(certPath, keyPath)
|
||||
}
|
||||
// A key without a certificate cannot have been installed as a trusted root.
|
||||
// This is safe to recover if the first-run write was interrupted.
|
||||
if errors.Is(certErr, os.ErrNotExist) && keyErr == nil {
|
||||
return generateAndPersistManager(certPath, keyPath)
|
||||
}
|
||||
if certErr != nil && !errors.Is(certErr, os.ErrNotExist) {
|
||||
return nil, nil, fmt.Errorf("read installation CA certificate: %w", certErr)
|
||||
}
|
||||
if keyErr != nil && !errors.Is(keyErr, os.ErrNotExist) {
|
||||
return nil, nil, fmt.Errorf("read installation CA private key: %w", keyErr)
|
||||
}
|
||||
return nil, nil, errors.New("installation CA is incomplete; both certificate and private key are required")
|
||||
}
|
||||
|
||||
// NewGeneratedManager creates an in-memory CA suitable for short-lived tools.
|
||||
func NewGeneratedManager() (*Manager, []byte, error) {
|
||||
certPEM, keyPEM, err := generateCA()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
manager, err := NewManagerFromPEM(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return manager, manager.CACertPEM(), nil
|
||||
}
|
||||
|
||||
func generateAndPersistManager(certPath, keyPath string) (*Manager, []byte, error) {
|
||||
if filepath.Dir(certPath) != filepath.Dir(keyPath) {
|
||||
return nil, nil, errors.New("installation CA certificate and key must share a directory")
|
||||
}
|
||||
certPEM, keyPEM, err := generateCA()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), 0o700); err != nil {
|
||||
return nil, nil, fmt.Errorf("create installation CA directory: %w", err)
|
||||
}
|
||||
// Write the private key first so a crash cannot leave a new certificate
|
||||
// without the signing key needed by the proxy.
|
||||
if err := writeLocalCAFile(keyPath, keyPEM, 0o600); err != nil {
|
||||
return nil, nil, fmt.Errorf("persist installation CA private key: %w", err)
|
||||
}
|
||||
if err := writeLocalCAFile(certPath, certPEM, 0o644); err != nil {
|
||||
return nil, nil, fmt.Errorf("persist installation CA certificate: %w", err)
|
||||
}
|
||||
manager, err := NewManagerFromPEM(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return manager, manager.CACertPEM(), nil
|
||||
}
|
||||
|
||||
func generateCA() ([]byte, []byte, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 3072)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generate installation CA private key: %w", err)
|
||||
}
|
||||
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generate installation CA serial: %w", err)
|
||||
}
|
||||
publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("marshal installation CA public key: %w", err)
|
||||
}
|
||||
subjectKeyID := sha256.Sum256(publicKeyDER)
|
||||
now := time.Now()
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Cursor BYOK Local CA",
|
||||
Organization: []string{"Cursor BYOK"},
|
||||
},
|
||||
NotBefore: now.Add(-5 * time.Minute),
|
||||
NotAfter: now.AddDate(10, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
SubjectKeyId: append([]byte(nil), subjectKeyID[:20]...),
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create installation CA certificate: %w", err)
|
||||
}
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
func writeLocalCAFile(path string, data []byte, mode os.FileMode) error {
|
||||
temp, err := os.CreateTemp(filepath.Dir(path), ".ca-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempPath := temp.Name()
|
||||
defer os.Remove(tempPath)
|
||||
if err := temp.Chmod(mode); err != nil {
|
||||
_ = temp.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := temp.Write(data); err != nil {
|
||||
_ = temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Sync(); err != nil {
|
||||
_ = temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tempPath, path); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(path, mode)
|
||||
}
|
||||
|
||||
func isLegacySharedCA(certPEM []byte) bool {
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return false
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
sum := sha256.Sum256(cert.Raw)
|
||||
return strings.EqualFold(hex.EncodeToString(sum[:]), legacySharedCASHA256)
|
||||
}
|
||||
|
||||
// loadCAPEMFromFiles reads an explicitly supplied CA pair.
|
||||
func loadCAPEMFromFiles(certPath, keyPath string) ([]byte, []byte, error) {
|
||||
certPEM, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
keyPEM, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadOrCreateManagerPersistsAndReusesInstallationCA(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath := filepath.Join(dir, "ca.crt")
|
||||
keyPath := filepath.Join(dir, "ca.key")
|
||||
|
||||
manager, certPEM, err := LoadOrCreateManager(certPath, keyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadOrCreateManager() error = %v", err)
|
||||
}
|
||||
if isLegacySharedCA(certPEM) {
|
||||
t.Fatal("generated CA reused the legacy shared certificate")
|
||||
}
|
||||
keyInfo, err := os.Stat(keyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("stat private key: %v", err)
|
||||
}
|
||||
if runtime.GOOS != "windows" && keyInfo.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("private key mode = %o, want 600", keyInfo.Mode().Perm())
|
||||
}
|
||||
|
||||
leaf, err := manager.CertificateForServerName("api2.cursor.sh")
|
||||
if err != nil {
|
||||
t.Fatalf("CertificateForServerName() error = %v", err)
|
||||
}
|
||||
ca := parseCertificatePEM(t, certPEM)
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(ca)
|
||||
if _, err := leaf.Leaf.Verify(x509.VerifyOptions{DNSName: "api2.cursor.sh", Roots: roots}); err != nil {
|
||||
t.Fatalf("verify generated leaf: %v", err)
|
||||
}
|
||||
|
||||
_, reusedCertPEM, err := LoadOrCreateManager(certPath, keyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("second LoadOrCreateManager() error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(certPEM, reusedCertPEM) {
|
||||
t.Fatal("installation CA changed between loads")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOrCreateManagerCreatesUniqueCAsPerInstallation(t *testing.T) {
|
||||
firstDir := t.TempDir()
|
||||
secondDir := t.TempDir()
|
||||
_, firstCert, err := LoadOrCreateManager(filepath.Join(firstDir, "ca.crt"), filepath.Join(firstDir, "ca.key"))
|
||||
if err != nil {
|
||||
t.Fatalf("create first CA: %v", err)
|
||||
}
|
||||
_, secondCert, err := LoadOrCreateManager(filepath.Join(secondDir, "ca.crt"), filepath.Join(secondDir, "ca.key"))
|
||||
if err != nil {
|
||||
t.Fatalf("create second CA: %v", err)
|
||||
}
|
||||
if bytes.Equal(firstCert, secondCert) {
|
||||
t.Fatal("separate installations received the same CA certificate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOrCreateManagerReplacesLegacySharedCertificate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath := filepath.Join(dir, "ca.crt")
|
||||
keyPath := filepath.Join(dir, "ca.key")
|
||||
legacyCert, err := os.ReadFile(filepath.Join("testdata", "legacy_shared_ca.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("read legacy certificate fixture: %v", err)
|
||||
}
|
||||
if !isLegacySharedCA(legacyCert) {
|
||||
t.Fatal("legacy certificate fixture fingerprint changed")
|
||||
}
|
||||
if err := os.WriteFile(certPath, legacyCert, 0o644); err != nil {
|
||||
t.Fatalf("write legacy certificate: %v", err)
|
||||
}
|
||||
|
||||
_, generatedCert, err := LoadOrCreateManager(certPath, keyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("migrate legacy certificate: %v", err)
|
||||
}
|
||||
if bytes.Equal(legacyCert, generatedCert) || isLegacySharedCA(generatedCert) {
|
||||
t.Fatal("legacy shared certificate was not replaced")
|
||||
}
|
||||
if _, err := os.Stat(keyPath); err != nil {
|
||||
t.Fatalf("generated private key missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewManagerFromPEMRejectsMismatchedKey(t *testing.T) {
|
||||
firstDir := t.TempDir()
|
||||
secondDir := t.TempDir()
|
||||
_, _, err := LoadOrCreateManager(filepath.Join(firstDir, "ca.crt"), filepath.Join(firstDir, "ca.key"))
|
||||
if err != nil {
|
||||
t.Fatalf("create first CA: %v", err)
|
||||
}
|
||||
_, _, err = LoadOrCreateManager(filepath.Join(secondDir, "ca.crt"), filepath.Join(secondDir, "ca.key"))
|
||||
if err != nil {
|
||||
t.Fatalf("create second CA: %v", err)
|
||||
}
|
||||
certPEM, _ := os.ReadFile(filepath.Join(firstDir, "ca.crt"))
|
||||
keyPEM, _ := os.ReadFile(filepath.Join(secondDir, "ca.key"))
|
||||
if _, err := NewManagerFromPEM(certPEM, keyPEM); err == nil {
|
||||
t.Fatal("NewManagerFromPEM() accepted a mismatched private key")
|
||||
}
|
||||
}
|
||||
|
||||
func parseCertificatePEM(t *testing.T, certPEM []byte) *x509.Certificate {
|
||||
t.Helper()
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
t.Fatal("certificate PEM is invalid")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("parse certificate: %v", err)
|
||||
}
|
||||
return cert
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
goruntime "runtime"
|
||||
|
||||
"cursor/internal/cursor"
|
||||
"cursor/internal/logger"
|
||||
)
|
||||
|
||||
// ApplyCursorSettings 用于处理与 ApplyCursorSettings 相关的逻辑。
|
||||
@@ -21,6 +22,9 @@ func (s *ProxyService) ApplyCursorSettings() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("ensure ca cert file: %w", err)
|
||||
}
|
||||
if err := cursor.EnsureLegacySharedCACertRemoved(); err != nil {
|
||||
logger.Errorf("remove legacy shared ca cert failed, continuing with installation CA: %v", err)
|
||||
}
|
||||
|
||||
switch goruntime.GOOS {
|
||||
case "windows":
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
const (
|
||||
darwinSecurityExe = "security"
|
||||
darwinLoginKeychainName = "login.keychain-db"
|
||||
legacySharedCASHA1 = "C14B7488C5AB83F098BEB2603F1135595A381FC0"
|
||||
)
|
||||
|
||||
func getCertSHA1Fingerprint(certPEM []byte) (string, error) {
|
||||
@@ -36,7 +37,10 @@ func isCACertInstalled(certPEM []byte) (bool, error) {
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取证书指纹失败: %w", err)
|
||||
}
|
||||
return isCACertFingerprintInstalled(fingerprint)
|
||||
}
|
||||
|
||||
func isCACertFingerprintInstalled(fingerprint string) (bool, error) {
|
||||
out, err := exec.Command(darwinSecurityExe, "find-certificate", "-a", "-Z", darwinLoginKeychainName).CombinedOutput()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("检查 macOS 登录钥匙串失败: %w: %s", err, strings.TrimSpace(string(out)))
|
||||
@@ -50,6 +54,36 @@ func isCACertInstalled(certPEM []byte) (bool, error) {
|
||||
return installed, nil
|
||||
}
|
||||
|
||||
// EnsureLegacySharedCACertRemoved removes the compromised CA shipped by older versions.
|
||||
func EnsureLegacySharedCACertRemoved() error {
|
||||
installed, err := isCACertFingerprintInstalled(legacySharedCASHA1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查旧版共享 CA 失败: %w", err)
|
||||
}
|
||||
if !installed {
|
||||
return nil
|
||||
}
|
||||
out, err := exec.Command(
|
||||
darwinSecurityExe,
|
||||
"delete-certificate",
|
||||
"-Z", legacySharedCASHA1,
|
||||
"-t",
|
||||
darwinLoginKeychainName,
|
||||
).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("从 macOS 登录钥匙串删除旧版共享 CA 失败: %w: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
installed, err = isCACertFingerprintInstalled(legacySharedCASHA1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证旧版共享 CA 删除状态失败: %w", err)
|
||||
}
|
||||
if installed {
|
||||
return fmt.Errorf("删除命令已执行,但 macOS 登录钥匙串中仍存在旧版共享 CA")
|
||||
}
|
||||
logger.Infof("ensureLegacySharedCACertRemoved: legacy shared CA removed from macOS login keychain")
|
||||
return nil
|
||||
}
|
||||
|
||||
func installCACertToDarwinKeychain(certPEM []byte, certPath string) error {
|
||||
fingerprint, err := getCertSHA1Fingerprint(certPEM)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
windowsCertutilExe = "certutil.exe"
|
||||
windowsPowerShellExe = "powershell.exe"
|
||||
windowsUserCancelCode = 1223
|
||||
legacySharedCASHA1 = "C14B7488C5AB83F098BEB2603F1135595A381FC0"
|
||||
)
|
||||
|
||||
// getCertThumbprint 获取证书的SHA1指纹,用于唯一标识证书
|
||||
@@ -51,7 +52,10 @@ func isCACertInstalled(certPEM []byte) (bool, error) {
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取证书指纹失败: %w", err)
|
||||
}
|
||||
return isCACertThumbprintInstalled(thumbprint)
|
||||
}
|
||||
|
||||
func isCACertThumbprintInstalled(thumbprint string) (bool, error) {
|
||||
cmd := exec.Command(windowsCertutilExe, "-verifystore", windowsRootStoreName, thumbprint)
|
||||
cmd.SysProcAttr = hideWindow()
|
||||
output, err := cmd.CombinedOutput()
|
||||
@@ -76,6 +80,29 @@ func isCACertInstalled(certPEM []byte) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// EnsureLegacySharedCACertRemoved removes the compromised CA shipped by older versions.
|
||||
func EnsureLegacySharedCACertRemoved() error {
|
||||
installed, err := isCACertThumbprintInstalled(legacySharedCASHA1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查旧版共享 CA 失败: %w", err)
|
||||
}
|
||||
if !installed {
|
||||
return nil
|
||||
}
|
||||
if err := runElevatedCertutil("-delstore", windowsRootStoreName, legacySharedCASHA1); err != nil {
|
||||
return fmt.Errorf("从 Windows 系统信任存储删除旧版共享 CA 失败: %w", err)
|
||||
}
|
||||
installed, err = isCACertThumbprintInstalled(legacySharedCASHA1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证旧版共享 CA 删除状态失败: %w", err)
|
||||
}
|
||||
if installed {
|
||||
return fmt.Errorf("删除命令已执行,但 Windows 系统信任存储中仍存在旧版共享 CA")
|
||||
}
|
||||
logger.Infof("ensureLegacySharedCACertRemoved: legacy shared CA removed from Windows system store")
|
||||
return nil
|
||||
}
|
||||
|
||||
func quotePowerShellLiteral(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
||||
}
|
||||
|
||||
@@ -8,3 +8,8 @@ import "fmt"
|
||||
func EnsureCACertInstalled(_ []byte, certPath string) error {
|
||||
return fmt.Errorf("ensureCACertInstalled: 当前平台暂不支持,certPath=%s", certPath)
|
||||
}
|
||||
|
||||
// EnsureLegacySharedCACertRemoved is a no-op on unsupported platforms.
|
||||
func EnsureLegacySharedCACertRemoved() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user