Skip to content

Commit d290bc9

Browse files
DASungtaclaude
andcommitted
feat(v0.4.8): 修复 Windows 10 证书信任问题(Issue #9
重写 TLS 证书 profile 为 v2,解决 Win10 Schannel CRYPT_E_NO_REVOCATION_CHECK: - CA 去掉 CRLSign 位 + 设 MaxPathLen=0 - 服务器证书加 KeyEncipherment / ClientAuth / 127.0.0.1 IP SAN - TLS 握手发送叶子+CA 完整链 - init 自动检测并迁移 v0.4.7 及更早旧 profile CA - 补充全套证书 profile 与迁移单元测试 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a96e900 commit d290bc9

4 files changed

Lines changed: 328 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [v0.4.8] - 2026-04-17
6+
7+
### Bug Fixes
8+
9+
- **Windows 10 证书信任修复(Issue #9 后续)**:v0.4.7 修复了安装校验,但证书 profile 与 Windows 10 Schannel / Chromium 130 的严格校验不兼容,导致 `CRYPT_E_NO_REVOCATION_CHECK (0x80092012)` 及 TLS 握手 `EOF`。本次重写证书 profile 至"v2"标准:
10+
- **CA 证书**:去掉 `KeyUsageCRLSign`(该位声明 CRL 能力却无 CDP 扩展,正是 Schannel 触发吊销校验的根因);添加 `MaxPathLen=0, MaxPathLenZero=true`,防止被继续用于签发中间 CA。
11+
- **服务器证书**:补充 `KeyUsageKeyEncipherment`(旧版 Schannel/TLS 栈仍要求此位);`ExtKeyUsage` 加入 `ClientAuth`(最大兼容性);`IPAddresses` 加入 `127.0.0.1`(覆盖 Electron 代理路径用 IP SNI 的场景)。
12+
- **TLS 握手链**:服务端在握手时主动发送"叶子 + CA"完整链,Schannel/Chromium 无需依赖 ROOT 存储 AKI 匹配来构建链。
13+
14+
### Migration
15+
16+
- **自动迁移旧 CA**`trae-proxy init` 在检测到 v0.4.7 及更早版本生成的旧 profile CA(有 `CRLSign` 位或缺少 v2 版本标记)时,自动执行:卸载旧 CA 信任 → 删除旧证书文件 → 重新生成 v2 profile CA 和服务器证书 → 重新安装信任。用户只需执行 `trae-proxy update && trae-proxy init` 即可完成迁移,无需任何手动操作。
17+
18+
---
19+
520
## [v0.4.7] - 2026-04-17
621

722
### Features

cmd/trae-proxy/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ func initCmd() *cobra.Command {
133133
if err := tlsutil.GenerateCA(caDir); err != nil {
134134
return fmt.Errorf("generate CA: %w", err)
135135
}
136+
} else if tlsutil.CANeedsRegeneration(caDir) {
137+
fmt.Println("[init] detected legacy CA profile (v0.4.7 or earlier), regenerating for Windows 10 compatibility...")
138+
caCertPath := filepath.Join(caDir, "root-ca.pem")
139+
_ = tlsutil.UninstallCA(caCertPath) // best-effort: remove old trust entry
140+
for _, f := range []string{"root-ca.pem", "root-ca-key.pem", "server.pem", "server-key.pem"} {
141+
os.Remove(filepath.Join(caDir, f))
142+
}
143+
if err := tlsutil.GenerateCA(caDir); err != nil {
144+
return fmt.Errorf("generate CA: %w", err)
145+
}
136146
} else {
137147
fmt.Println("[init] CA already exists")
138148
}

internal/tls/ca.go

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"encoding/pem"
1111
"fmt"
1212
"math/big"
13+
"net"
1314
"os"
1415
"os/exec"
1516
"path/filepath"
@@ -20,6 +21,9 @@ import (
2021

2122
const rootCACommonName = "trae-proxy Root CA"
2223

24+
// certProfileVersion is stamped as Subject.OrganizationalUnit to enable migration detection.
25+
const certProfileVersion = "v2"
26+
2327
var (
2428
currentGOOS = runtime.GOOS
2529
execCombinedOutput = func(name string, args ...string) ([]byte, error) {
@@ -37,14 +41,19 @@ func GenerateCA(dir string) error {
3741
tmpl := &x509.Certificate{
3842
SerialNumber: serial,
3943
Subject: pkix.Name{
40-
Organization: []string{"trae-proxy"},
41-
CommonName: rootCACommonName,
44+
Organization: []string{"trae-proxy"},
45+
OrganizationalUnit: []string{certProfileVersion},
46+
CommonName: rootCACommonName,
4247
},
43-
NotBefore: time.Now(),
44-
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
45-
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
48+
NotBefore: time.Now(),
49+
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
50+
// KeyUsageCertSign only — omitting CRLSign so Schannel does not expect
51+
// a CRL Distribution Point and fail with CRYPT_E_NO_REVOCATION_CHECK.
52+
KeyUsage: x509.KeyUsageCertSign,
4653
BasicConstraintsValid: true,
4754
IsCA: true,
55+
MaxPathLen: 0,
56+
MaxPathLenZero: true,
4857
}
4958

5059
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
@@ -103,14 +112,19 @@ func GenerateServerCert(dir string, caCert *x509.Certificate, caKey *ecdsa.Priva
103112
tmpl := &x509.Certificate{
104113
SerialNumber: serial,
105114
Subject: pkix.Name{
106-
Organization: []string{"trae-proxy"},
107-
CommonName: domain,
115+
Organization: []string{"trae-proxy"},
116+
OrganizationalUnit: []string{certProfileVersion},
117+
CommonName: domain,
108118
},
109-
DNSNames: []string{domain},
110-
NotBefore: time.Now(),
111-
NotAfter: time.Now().Add(365 * 24 * time.Hour),
112-
KeyUsage: x509.KeyUsageDigitalSignature,
113-
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
119+
DNSNames: []string{domain},
120+
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
121+
NotBefore: time.Now(),
122+
NotAfter: time.Now().Add(365 * 24 * time.Hour),
123+
// KeyEncipherment is required by strict Schannel / older TLS stacks even
124+
// when using ECDSA (ECDHE never uses encryption, but some validators still check).
125+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
126+
// ClientAuth is included for maximum compatibility; it does not affect server role.
127+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
114128
BasicConstraintsValid: true,
115129
IsCA: false,
116130
}
@@ -131,6 +145,9 @@ func GenerateServerCert(dir string, caCert *x509.Certificate, caKey *ecdsa.Priva
131145
return writePEM(filepath.Join(dir, "server-key.pem"), "EC PRIVATE KEY", keyDER)
132146
}
133147

148+
// LoadServerTLSConfig loads the server cert and appends the CA cert to the
149+
// TLS chain so clients (Schannel, Chromium) can build the chain deterministically
150+
// without relying solely on AKI lookup in the system trust store.
134151
func LoadServerTLSConfig(dir string) (*tls.Config, error) {
135152
cert, err := tls.LoadX509KeyPair(
136153
filepath.Join(dir, "server.pem"),
@@ -139,11 +156,25 @@ func LoadServerTLSConfig(dir string) (*tls.Config, error) {
139156
if err != nil {
140157
return nil, err
141158
}
159+
160+
caPEM, err := os.ReadFile(filepath.Join(dir, "root-ca.pem"))
161+
if err != nil {
162+
return nil, fmt.Errorf("read CA cert for chain: %w", err)
163+
}
164+
block, _ := pem.Decode(caPEM)
165+
if block == nil {
166+
return nil, fmt.Errorf("failed to decode CA cert PEM for chain")
167+
}
168+
cert.Certificate = append(cert.Certificate, block.Bytes)
169+
142170
return &tls.Config{
143171
Certificates: []tls.Certificate{cert},
144172
}, nil
145173
}
146174

175+
// NeedsRegeneration reports whether the server cert should be re-issued.
176+
// Returns true for missing/unreadable certs, expiring certs, domain mismatches,
177+
// and legacy v1 profile certs (missing KeyEncipherment or OU version marker).
147178
func NeedsRegeneration(dir string, domain string) bool {
148179
certPEM, err := os.ReadFile(filepath.Join(dir, "server.pem"))
149180
if err != nil {
@@ -163,6 +194,12 @@ func NeedsRegeneration(dir string, domain string) bool {
163194
if cert.NotAfter.Sub(cert.NotBefore) > 398*24*time.Hour {
164195
return true
165196
}
197+
if cert.KeyUsage&x509.KeyUsageKeyEncipherment == 0 {
198+
return true // legacy v1 profile
199+
}
200+
if !containsOU(cert.Subject.OrganizationalUnit, certProfileVersion) {
201+
return true // legacy v1 profile
202+
}
166203
for _, name := range cert.DNSNames {
167204
if name == domain {
168205
return false
@@ -171,6 +208,40 @@ func NeedsRegeneration(dir string, domain string) bool {
171208
return true
172209
}
173210

211+
// CANeedsRegeneration reports whether the root CA should be re-generated.
212+
// Returns true for missing/unreadable CA files and legacy v1 profile CAs
213+
// (those with CRLSign bit or missing OU version marker).
214+
func CANeedsRegeneration(dir string) bool {
215+
caPEM, err := os.ReadFile(filepath.Join(dir, "root-ca.pem"))
216+
if err != nil {
217+
return true
218+
}
219+
block, _ := pem.Decode(caPEM)
220+
if block == nil {
221+
return true
222+
}
223+
cert, err := x509.ParseCertificate(block.Bytes)
224+
if err != nil {
225+
return true
226+
}
227+
if cert.KeyUsage&x509.KeyUsageCRLSign != 0 {
228+
return true // legacy v1 profile — CRLSign triggers Schannel revocation check
229+
}
230+
if !containsOU(cert.Subject.OrganizationalUnit, certProfileVersion) {
231+
return true // legacy v1 profile
232+
}
233+
return false
234+
}
235+
236+
func containsOU(ous []string, target string) bool {
237+
for _, ou := range ous {
238+
if ou == target {
239+
return true
240+
}
241+
}
242+
return false
243+
}
244+
174245
func InstallCA(caCertPath string) error {
175246
switch currentGOOS {
176247
case "darwin":

0 commit comments

Comments
 (0)