@@ -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
2122const rootCACommonName = "trae-proxy Root CA"
2223
24+ // certProfileVersion is stamped as Subject.OrganizationalUnit to enable migration detection.
25+ const certProfileVersion = "v2"
26+
2327var (
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.
134151func 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).
147178func 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+
174245func InstallCA (caCertPath string ) error {
175246 switch currentGOOS {
176247 case "darwin" :
0 commit comments