//go:build windows package cursor import ( "crypto/sha1" "crypto/x509" "cursor/internal/certs" "encoding/base64" "encoding/binary" "encoding/pem" "errors" "fmt" "os/exec" "strings" "syscall" "unicode/utf16" "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 { 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) if err != nil { return false, err } 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 } return strings.EqualFold(strings.TrimSpace(string(out)), "True"), nil } 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)) }