package certs import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "math/big" "os" "path/filepath" "time" "cursor/internal/securefile" ) // LoadOrCreateLocalCA persists one private CA per application data directory. // A single private bundle is the authority; ca.crt is a public export for clients. func LoadOrCreateLocalCA(dir string) (*Manager, error) { if err := securefile.PrivateDir(dir); err != nil { return nil, err } unlock, err := securefile.Lock(filepath.Join(dir, "ca.lock")) if err != nil { return nil, err } defer unlock() bundlePath := filepath.Join(dir, "local-ca.pem") bundle, err := os.ReadFile(bundlePath) if errors.Is(err, os.ErrNotExist) { key, keyErr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if keyErr != nil { return nil, keyErr } serial, keyErr := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if keyErr != nil { return nil, keyErr } now := time.Now() template := &x509.Certificate{SerialNumber: serial, Subject: pkix.Name{CommonName: "Local-Cursor Private CA", Organization: []string{"Local-Cursor"}}, NotBefore: now.Add(-time.Hour), NotAfter: now.AddDate(10, 0, 0), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign} der, keyErr := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) if keyErr != nil { return nil, keyErr } keyPEM, keyErr := marshalPrivateKeyPEM(key) if keyErr != nil { return nil, keyErr } bundle = append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), keyPEM...) if err = securefile.Write(bundlePath, bundle); err != nil { return nil, err } } else if err != nil { return nil, err } if err = securefile.Protect(bundlePath); err != nil { return nil, err } certBlock, rest := pem.Decode(bundle) if certBlock == nil { return nil, fmt.Errorf("本机 CA 文件损坏,请先备份并修复") } manager, err := NewManagerFromPEM(pem.EncodeToMemory(certBlock), rest) if err != nil { return nil, fmt.Errorf("本机 CA 校验失败(不会自动轮换): %w", err) } if err = securefile.Write(filepath.Join(dir, "ca.crt"), manager.CACertPEM()); err != nil { return nil, err } return manager, nil } func (m *Manager) CACertPEM() []byte { return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: m.caCert.Raw}) }