дȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9
¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档 д Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£ Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£ Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢ Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£ Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
This commit is contained in:
@@ -25,11 +25,6 @@ import (
|
||||
//go:embed ca.crt
|
||||
var embeddedCACertPEM []byte
|
||||
|
||||
// embeddedCAKeyPEM 表示当前模块中的 embeddedCAKeyPEM 状态值。
|
||||
//
|
||||
//go:embed ca.key
|
||||
var embeddedCAKeyPEM []byte
|
||||
|
||||
// Manager 定义了当前模块中的 Manager 类型。
|
||||
type Manager struct {
|
||||
// caCert 表示当前声明中的 caCert。
|
||||
@@ -52,27 +47,23 @@ func NewManager(caCertPath, caKeyPath string) (*Manager, error) {
|
||||
return NewManagerFromPEM(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
// NewEmbeddedManager 用于处理与 NewEmbeddedManager 相关的逻辑。
|
||||
func NewEmbeddedManager() (*Manager, error) {
|
||||
return NewManagerFromPEM(embeddedCACertPEM, embeddedCAKeyPEM)
|
||||
}
|
||||
|
||||
// EmbeddedCACertPEM 用于处理与 EmbeddedCACertPEM 相关的逻辑。
|
||||
func EmbeddedCACertPEM() []byte {
|
||||
return cloneBytes(embeddedCACertPEM)
|
||||
}
|
||||
|
||||
// EmbeddedCAKeyPEM 用于处理与 EmbeddedCAKeyPEM 相关的逻辑。
|
||||
func EmbeddedCAKeyPEM() []byte {
|
||||
return cloneBytes(embeddedCAKeyPEM)
|
||||
}
|
||||
|
||||
// NewManagerFromPEM 用于处理与 NewManagerFromPEM 相关的逻辑。
|
||||
func NewManagerFromPEM(caCertPEM, caKeyPEM []byte) (*Manager, error) {
|
||||
if _, err := tls.X509KeyPair(caCertPEM, caKeyPEM); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caCert, caKey, err := loadCAFromPEM(caCertPEM, caKeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !caCert.IsCA || caCert.KeyUsage&x509.KeyUsageCertSign == 0 || time.Now().Before(caCert.NotBefore) || !time.Now().Before(caCert.NotAfter) {
|
||||
return nil, errors.New("invalid or expired CA certificate")
|
||||
}
|
||||
return &Manager{caCert: caCert, caKey: caKey, cache: make(map[string]*tls.Certificate)}, nil
|
||||
}
|
||||
|
||||
@@ -98,7 +89,7 @@ func (m *Manager) CertificateForServerName(serverName string) (*tls.Certificate,
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if cert, ok := m.cache[host]; ok {
|
||||
if cert, ok := m.cache[host]; ok && cert.Leaf != nil && time.Now().Add(time.Minute).Before(cert.Leaf.NotAfter) {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
@@ -119,6 +110,12 @@ func (m *Manager) CertificateForServerName(serverName string) (*tls.Certificate,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
if !time.Now().Before(m.caCert.NotAfter) {
|
||||
return nil, errors.New("CA certificate expired")
|
||||
}
|
||||
if leaf.NotAfter.After(m.caCert.NotAfter) {
|
||||
leaf.NotAfter = m.caCert.NotAfter
|
||||
}
|
||||
if len(m.caCert.SubjectKeyId) > 0 {
|
||||
leaf.AuthorityKeyId = append([]byte(nil), m.caCert.SubjectKeyId...)
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAyh3jND/aFusuRjGTmhQtX2hkF1qroNjEEKCxWPlfprvdl8Tx
|
||||
upxNv1TQcm+K9KsS7OxnKYP4Qtv068XLbaCCGuoA/xpor6enrT85KulBq0j8z/g1
|
||||
y0VWxjz3xN9F9ND13h9yxDZCn76egbRkhFxpXow67jLsIlrqWDSltERlTKh2cJ1g
|
||||
hTRuQNSr7jtKQgFAR3aQ6dTzOP4fCOtLYn63jL4+YcxdGoK66tx8eFHq8oBLYvsc
|
||||
LNjjkaAHilZFwA4Jr7zHBofTRBg04eZum2UTaRqmSyT65ifXm4vRdQa4k1FS1gnq
|
||||
hPMhIjST8b6RkxvbLNmFpkOfI9eCMRpFG7y+NQIDAQABAoIBAATU9ZVOcHmLSkop
|
||||
zcBJerM09O2dAIziGb/XA55fqdJ728aQ0gGW0oIANlKCCaWjQFrTJP04VzNL/F01
|
||||
l5EpnOqlTPxMRpPqc2cAI677sBL29fpH0gtnvzUSiI7Xkp3RcAtNH6qCrJGSlkn+
|
||||
BMgoSGmW+yKuK3h/yWnt6kc2umA8fN+bHKhS3pI56PMW8qVnny9n92RaCA7Uf/4j
|
||||
XDewIreiH5jRqRwrPbOpjDFmv+W18LWZQiTwwxfY6sRZiZpsfsHidzfFGUFZXMlq
|
||||
2P3FCqoF4oMM1rRgBlHhDR7JHmSkFpZG639HJTXLllpyDbj3H86nsjIj9WZYKn+h
|
||||
B9k9bcECgYEA1HDzRCqoRZh6cL46KiYjv+LwmEKMa3nPb4ljywgLbRkzTMyVs0MK
|
||||
fDsDoTLFY9PBhveypU5gjTbQqtyBsDtbU+dH9Eks1FdL9P8bJf0ZOJFRiEB7uB4a
|
||||
z3V9tcXHwH4l2bCWbWThQGFwRudDAoY0EH89oSA/WHayjOKe1Wi4ovUCgYEA848B
|
||||
cYi+Qbkk+fOv9gSJn8KS1LH/jE28S/e7E4YkTfYUuu+7wr8bdRKUNpPIsLX0Fo9R
|
||||
KpJX0Oyjjady9n/8ARZRmD8Upl+F7Sl7Ro6F8+nfqQUrxbVDiIL3b+aZF+cDfFrB
|
||||
/xL5kyZZqTtFfP360tfYnlS6Sssd4E2Jsj0fJkECgYEA0gG+WbqZkgMDtwQ114jQ
|
||||
elZLZRkUWwKVjzsQDZssQHNTBS6RJh619M0Z73aTLvYcL+IZFdT/GVoAuYc2JRLo
|
||||
W28c8F6OFHMfwVeWbN1g20y8fqbQJtiLxF3vIYwcxStvG123tvisu8oXBeCDm7Ez
|
||||
MsO2FtwcAsWECEXWojzdmSkCgYEA18GvPawtHnuszd+Z2Q5b/DKZb+Hex6N1Ura5
|
||||
+qmyL333D0Kfyf0RjbxPn6l690+4UuPSuyu4r1Nx72KO7N6jlzL2RTBcUqX8NgOx
|
||||
OOe4skJT5561EAdrM9sQ5wgYRpxW8ipUAGoGvNwUQV5ISFmVgIHFWz0jam5UoQcP
|
||||
G94ZYgECgYBN4PB8BuAwMRqhTCLL5RJKtcdC/Ls9xdWrmswtQQ4OSBUTLGdeMhEN
|
||||
E23F0d+NdtiTWDYRDJ7z6KAF8CcOwWMt2sNbrxgNRuDzyfLqbuVLOM8iY3xA50Nl
|
||||
clZWujKILWp47+gA6/AqJLv2lA2LK8gM8zYzzm/nCuLCBLYH/hOiCA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
78
internal/certs/local.go
Normal file
78
internal/certs/local.go
Normal file
@@ -0,0 +1,78 @@
|
||||
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})
|
||||
}
|
||||
70
internal/certs/local_test.go
Normal file
70
internal/certs/local_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLocalCAStablePrivateAndIndependent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
a, err := LoadOrCreateLocalCA(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := LoadOrCreateLocalCA(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(a.CACertPEM(), b.CACertPEM()) {
|
||||
t.Fatal("CA rotated on restart")
|
||||
}
|
||||
c, err := LoadOrCreateLocalCA(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(a.CACertPEM(), c.CACertPEM()) {
|
||||
t.Fatal("installations share a CA")
|
||||
}
|
||||
if bytes.Equal(a.CACertPEM(), EmbeddedCACertPEM()) {
|
||||
t.Fatal("legacy CA reused")
|
||||
}
|
||||
leaf, err := a.CertificateForServerName("api2.cursor.sh")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := leaf.Leaf.CheckSignatureFrom(a.caCert); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := leaf.Leaf.VerifyHostname("api2.cursor.sh"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := leaf.Leaf.VerifyHostname("unrelated.example"); err == nil {
|
||||
t.Fatal("hostname mismatch accepted")
|
||||
}
|
||||
bundle, err := os.ReadFile(filepath.Join(dir, "local-ca.pem"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, key := pem.Decode(bundle)
|
||||
if _, err = NewManagerFromPEM(c.CACertPEM(), key); err == nil {
|
||||
t.Fatal("mismatched key accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorruptCADoesNotRotateOrOverwrite(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bad := []byte("broken private authority")
|
||||
if err := os.WriteFile(filepath.Join(dir, "local-ca.pem"), bad, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadOrCreateLocalCA(dir); err == nil {
|
||||
t.Fatal("corrupt CA accepted")
|
||||
}
|
||||
after, _ := os.ReadFile(filepath.Join(dir, "local-ca.pem"))
|
||||
if !bytes.Equal(bad, after) {
|
||||
t.Fatal("corrupt CA silently replaced")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user