2026-11-10 01:24:28 +08:00
|
|
|
|
//go:build windows
|
|
|
|
|
|
|
|
|
|
|
|
package cursor
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"crypto/sha1"
|
|
|
|
|
|
"crypto/x509"
|
2026-11-10 09:59:34 +08:00
|
|
|
|
"cursor/internal/certs"
|
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
|
"encoding/binary"
|
2026-11-10 01:24:28 +08:00
|
|
|
|
"encoding/pem"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"os/exec"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"syscall"
|
2026-11-10 09:59:34 +08:00
|
|
|
|
"unicode/utf16"
|
2026-11-10 01:24:28 +08:00
|
|
|
|
|
|
|
|
|
|
"cursor/internal/logger"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
windowsRootStoreName = "Root"
|
|
|
|
|
|
windowsCertutilExe = "certutil.exe"
|
|
|
|
|
|
windowsPowerShellExe = "powershell.exe"
|
|
|
|
|
|
windowsUserCancelCode = 1223
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// getCertThumbprint 获取证书的SHA1指纹,用于唯一标识证书
|
|
|
|
|
|
func getCertThumbprint(certPEM []byte) (string, error) {
|
|
|
|
|
|
block, _ := pem.Decode(certPEM)
|
|
|
|
|
|
if block == nil {
|
|
|
|
|
|
return "", fmt.Errorf("无法解析证书 PEM")
|
|
|
|
|
|
}
|
|
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("解析证书失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
// SHA1 指纹,certutil 使用此格式
|
|
|
|
|
|
thumbprint := fmt.Sprintf("%X", sha1.Sum(cert.Raw))
|
|
|
|
|
|
return thumbprint, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// hideWindow 返回隐藏命令行窗口的 SysProcAttr
|
|
|
|
|
|
func hideWindow() *syscall.SysProcAttr {
|
|
|
|
|
|
return &syscall.SysProcAttr{
|
|
|
|
|
|
HideWindow: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// isCACertInstalled 检查 CA 证书是否已安装到 Windows 系统根证书存储。
|
|
|
|
|
|
// 默认不带 -user,表示 LocalMachine\Root。
|
|
|
|
|
|
func isCACertInstalled(certPEM []byte) (bool, error) {
|
|
|
|
|
|
thumbprint, err := getCertThumbprint(certPEM)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false, fmt.Errorf("获取证书指纹失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command(windowsCertutilExe, "-verifystore", windowsRootStoreName, thumbprint)
|
|
|
|
|
|
cmd.SysProcAttr = hideWindow()
|
|
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
var exitErr *exec.ExitError
|
|
|
|
|
|
if errors.As(err, &exitErr) {
|
|
|
|
|
|
// certutil 在找不到证书时返回非零退出码。
|
|
|
|
|
|
logger.Infof("isCACertInstalled: cert not found in system store, thumbprint=%s exitCode=%d", thumbprint, exitErr.ExitCode())
|
|
|
|
|
|
return false, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return false, fmt.Errorf("执行 certutil 检查系统证书存储失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
outStr := strings.ToUpper(string(output))
|
|
|
|
|
|
if strings.Contains(outStr, thumbprint) {
|
|
|
|
|
|
logger.Infof("isCACertInstalled: cert found in system store, thumbprint=%s", thumbprint)
|
|
|
|
|
|
return true, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 某些 Windows 语言环境下 certutil 的文本不同,这里仍然按未找到处理。
|
|
|
|
|
|
logger.Infof("isCACertInstalled: cert not found in certutil output, thumbprint=%s", thumbprint)
|
|
|
|
|
|
return false, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func quotePowerShellLiteral(value string) string {
|
|
|
|
|
|
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func runElevatedCertutil(args ...string) error {
|
|
|
|
|
|
quotedArgs := make([]string, 0, len(args))
|
|
|
|
|
|
for _, arg := range args {
|
|
|
|
|
|
quotedArgs = append(quotedArgs, quotePowerShellLiteral(arg))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
script := fmt.Sprintf(
|
|
|
|
|
|
"$process = Start-Process -FilePath %s -ArgumentList @(%s) -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $process.ExitCode",
|
|
|
|
|
|
quotePowerShellLiteral(windowsCertutilExe),
|
|
|
|
|
|
strings.Join(quotedArgs, ","),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command(
|
|
|
|
|
|
windowsPowerShellExe,
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-NonInteractive",
|
|
|
|
|
|
"-ExecutionPolicy",
|
|
|
|
|
|
"Bypass",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
script,
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd.SysProcAttr = hideWindow()
|
|
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var exitErr *exec.ExitError
|
|
|
|
|
|
if errors.As(err, &exitErr) && exitErr.ExitCode() == windowsUserCancelCode {
|
|
|
|
|
|
return fmt.Errorf("用户取消了管理员权限授予")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
trimmedOutput := strings.TrimSpace(string(output))
|
|
|
|
|
|
if trimmedOutput == "" {
|
|
|
|
|
|
return fmt.Errorf("通过管理员权限执行 certutil 失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("通过管理员权限执行 certutil 失败: %w, output: %s", err, trimmedOutput)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// installCACertToWindowsStore 将 CA 证书安装到 Windows 系统根证书存储。
|
|
|
|
|
|
// LocalMachine\Root 需要管理员权限,因此这里会触发 UAC 提权。
|
|
|
|
|
|
func installCACertToWindowsStore(certPEM []byte, certPath string) error {
|
|
|
|
|
|
thumbprint, err := getCertThumbprint(certPEM)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("获取证书指纹失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.Infof("installCACertToWindowsStore: installing cert into system store, path=%s thumbprint=%s", certPath, thumbprint)
|
|
|
|
|
|
|
|
|
|
|
|
if err := runElevatedCertutil("-addstore", windowsRootStoreName, certPath); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
installed, err := isCACertInstalled(certPEM)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("验证系统证书安装状态失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !installed {
|
|
|
|
|
|
return fmt.Errorf("证书导入命令已执行,但系统信任存储中未找到证书")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.Infof("installCACertToWindowsStore: cert installed successfully into system store, thumbprint=%s", thumbprint)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// EnsureCACertInstalled 确保证书已安装到 Windows 系统信任存储。
|
|
|
|
|
|
func EnsureCACertInstalled(certPEM []byte, certPath string) error {
|
2026-11-10 09:59:34 +08:00
|
|
|
|
return migrateCATrust(certPEM, isCAInstalledAnyStore, func() error {
|
|
|
|
|
|
thumb, err := getCertThumbprint(certPEM)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
legacy, err := getCertThumbprint(certs.EmbeddedCACertPEM())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
body := windowsCAMigrationBody(certPath, thumb, legacy)
|
|
|
|
|
|
units := utf16.Encode([]rune(body))
|
|
|
|
|
|
bytes := make([]byte, len(units)*2)
|
|
|
|
|
|
for i, u := range units {
|
|
|
|
|
|
binary.LittleEndian.PutUint16(bytes[i*2:], u)
|
|
|
|
|
|
}
|
|
|
|
|
|
encoded := base64.StdEncoding.EncodeToString(bytes)
|
|
|
|
|
|
outer := "$p=Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','" + encoded + "') -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode"
|
|
|
|
|
|
cmd := exec.Command(windowsPowerShellExe, "-NoProfile", "-NonInteractive", "-Command", outer)
|
|
|
|
|
|
cmd.SysProcAttr = hideWindow()
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
|
return fmt.Errorf("安装证书授权失败或已取消,请重试: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
cleanup := fmt.Sprintf("$ErrorActionPreference='Stop'; $s=New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','CurrentUser'); try { $s.Open('ReadWrite'); foreach($c in @($s.Certificates.Find('FindByThumbprint',%s,$false))) { $s.Remove($c) } } finally { $s.Close() }", quotePowerShellLiteral(legacy))
|
|
|
|
|
|
originalUser := exec.Command(windowsPowerShellExe, "-NoProfile", "-NonInteractive", "-Command", cleanup)
|
|
|
|
|
|
originalUser.SysProcAttr = hideWindow()
|
|
|
|
|
|
if err := originalUser.Run(); err != nil {
|
|
|
|
|
|
return fmt.Errorf("移除当前用户的旧共享证书失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func isCAInstalledAnyStore(certPEM []byte) (bool, error) {
|
|
|
|
|
|
thumb, err := getCertThumbprint(certPEM)
|
2026-11-10 01:24:28 +08:00
|
|
|
|
if err != nil {
|
2026-11-10 09:59:34 +08:00
|
|
|
|
return false, err
|
2026-11-10 01:24:28 +08:00
|
|
|
|
}
|
2026-11-10 09:59:34 +08:00
|
|
|
|
script := "$ErrorActionPreference='Stop'; (Test-Path " + quotePowerShellLiteral(`Cert:\LocalMachine\Root\`+thumb) + ") -or (Test-Path " + quotePowerShellLiteral(`Cert:\CurrentUser\Root\`+thumb) + ")"
|
|
|
|
|
|
cmd := exec.Command(windowsPowerShellExe, "-NoProfile", "-NonInteractive", "-Command", script)
|
|
|
|
|
|
cmd.SysProcAttr = hideWindow()
|
|
|
|
|
|
out, err := cmd.Output()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false, err
|
2026-11-10 01:24:28 +08:00
|
|
|
|
}
|
2026-11-10 09:59:34 +08:00
|
|
|
|
return strings.EqualFold(strings.TrimSpace(string(out)), "True"), nil
|
|
|
|
|
|
}
|
2026-11-10 01:24:28 +08:00
|
|
|
|
|
2026-11-10 09:59:34 +08:00
|
|
|
|
func windowsCAMigrationBody(certPath, thumb, legacy string) string {
|
|
|
|
|
|
return fmt.Sprintf(`$ErrorActionPreference='Stop'
|
|
|
|
|
|
$c = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(%s)
|
|
|
|
|
|
if ($c.Thumbprint -ne %s) { throw 'CA fingerprint mismatch' }
|
|
|
|
|
|
$s = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine')
|
|
|
|
|
|
try {
|
|
|
|
|
|
$s.Open('ReadWrite'); $s.Add($c)
|
|
|
|
|
|
if ($s.Certificates.Find('FindByThumbprint',%s,$false).Count -eq 0) { throw 'CA installation failed' }
|
|
|
|
|
|
foreach ($old in @($s.Certificates.Find('FindByThumbprint',%s,$false))) { $s.Remove($old) }
|
|
|
|
|
|
} finally { $s.Close() }
|
|
|
|
|
|
`, quotePowerShellLiteral(certPath), quotePowerShellLiteral(thumb), quotePowerShellLiteral(thumb), quotePowerShellLiteral(legacy))
|
2026-11-10 01:24:28 +08:00
|
|
|
|
}
|