fix: generate a per-installation root CA

This commit is contained in:
leokun
2026-08-12 20:04:29 +08:00
parent 7a67e76617
commit 2b8d1c9e3d
14 changed files with 454 additions and 89 deletions
+42 -44
View File
@@ -1,6 +1,7 @@
package certs
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
@@ -9,33 +10,24 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
_ "embed"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"os"
"strings"
"sync"
"time"
)
// embeddedCACertPEM 表示当前模块中的 embeddedCACertPEM 状态值。
//
//go:embed ca.crt
var embeddedCACertPEM []byte
// embeddedCAKeyPEM 表示当前模块中的 embeddedCAKeyPEM 状态值。
//
//go:embed ca.key
var embeddedCAKeyPEM []byte
// Manager 定义了当前模块中的 Manager 类型。
type Manager struct {
// caCert 表示当前声明中的 caCert。
caCert *x509.Certificate
// caKey 表示当前声明中的 caKey。
caKey crypto.PrivateKey
// caCertPEM 保存可注入宿主信任存储的 CA 证书,不包含私钥。
caCertPEM []byte
// mu 表示当前声明中的 mu。
mu sync.Mutex
@@ -52,28 +44,26 @@ 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) {
caCert, caKey, err := loadCAFromPEM(caCertPEM, caKeyPEM)
if err != nil {
return nil, err
}
return &Manager{caCert: caCert, caKey: caKey, cache: make(map[string]*tls.Certificate)}, nil
return &Manager{
caCert: caCert,
caKey: caKey,
caCertPEM: cloneBytes(caCertPEM),
cache: make(map[string]*tls.Certificate),
}, nil
}
// CACertPEM 返回当前 Manager 使用的 CA 证书。返回值不包含私钥。
func (m *Manager) CACertPEM() []byte {
if m == nil {
return nil
}
return cloneBytes(m.caCertPEM)
}
// CATLSCertificate 用于处理与 CATLSCertificate 相关的逻辑。
@@ -185,19 +175,6 @@ func marshalPrivateKeyPEM(key any) ([]byte, error) {
}
}
// loadCAPEMFromFiles 用于处理与 loadCAPEMFromFiles 相关的逻辑。
func loadCAPEMFromFiles(certPath, keyPath string) ([]byte, []byte, error) {
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, nil, err
}
keyPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, nil, err
}
return certPEM, keyPEM, nil
}
// loadCAFromPEM 用于处理与 loadCAFromPEM 相关的逻辑。
func loadCAFromPEM(certPEM, keyPEM []byte) (*x509.Certificate, crypto.PrivateKey, error) {
certBlock, _ := pem.Decode(certPEM)
@@ -214,28 +191,49 @@ func loadCAFromPEM(certPEM, keyPEM []byte) (*x509.Certificate, crypto.PrivateKey
return nil, nil, errors.New("invalid CA key PEM")
}
var caKey crypto.PrivateKey
switch keyBlock.Type {
case "RSA PRIVATE KEY":
key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
if err != nil {
return nil, nil, err
}
return caCert, key, nil
caKey = key
case "EC PRIVATE KEY":
key, err := x509.ParseECPrivateKey(keyBlock.Bytes)
if err != nil {
return nil, nil, err
}
return caCert, key, nil
caKey = key
case "PRIVATE KEY":
key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
if err != nil {
return nil, nil, err
}
return caCert, key, nil
caKey = key
default:
return nil, nil, errors.New("unsupported CA key format")
}
if !caCert.IsCA || !caCert.BasicConstraintsValid || caCert.KeyUsage&x509.KeyUsageCertSign == 0 {
return nil, nil, errors.New("certificate is not a valid signing CA")
}
signer, ok := caKey.(crypto.Signer)
if !ok {
return nil, nil, errors.New("CA private key cannot sign certificates")
}
certPublicKey, err := x509.MarshalPKIXPublicKey(caCert.PublicKey)
if err != nil {
return nil, nil, fmt.Errorf("marshal CA certificate public key: %w", err)
}
privatePublicKey, err := x509.MarshalPKIXPublicKey(signer.Public())
if err != nil {
return nil, nil, fmt.Errorf("marshal CA private key public key: %w", err)
}
if !bytes.Equal(certPublicKey, privatePublicKey) {
return nil, nil, errors.New("CA certificate and private key do not match")
}
return caCert, caKey, nil
}
// normalizeHost 用于处理与 normalizeHost 相关的逻辑。
-27
View File
@@ -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-----
+191
View File
@@ -0,0 +1,191 @@
package certs
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"time"
)
const legacySharedCASHA256 = "836E6BB84F6C3E63316DBB4EC257223AF09F7490E7AAE09030B8515ED61EE9FF"
// LoadOrCreateManager loads the installation-specific CA, generating it on
// first run. The private key is persisted only in the supplied local path.
func LoadOrCreateManager(certPath, keyPath string) (*Manager, []byte, error) {
certPEM, certErr := os.ReadFile(certPath)
keyPEM, keyErr := os.ReadFile(keyPath)
if certErr == nil && isLegacySharedCA(certPEM) {
return generateAndPersistManager(certPath, keyPath)
}
if certErr == nil && keyErr == nil {
manager, err := NewManagerFromPEM(certPEM, keyPEM)
if err != nil {
return nil, nil, fmt.Errorf("load installation CA: %w", err)
}
if err := os.Chmod(keyPath, 0o600); err != nil {
return nil, nil, fmt.Errorf("restrict installation CA private key permissions: %w", err)
}
return manager, manager.CACertPEM(), nil
}
if errors.Is(certErr, os.ErrNotExist) && errors.Is(keyErr, os.ErrNotExist) {
return generateAndPersistManager(certPath, keyPath)
}
// A key without a certificate cannot have been installed as a trusted root.
// This is safe to recover if the first-run write was interrupted.
if errors.Is(certErr, os.ErrNotExist) && keyErr == nil {
return generateAndPersistManager(certPath, keyPath)
}
if certErr != nil && !errors.Is(certErr, os.ErrNotExist) {
return nil, nil, fmt.Errorf("read installation CA certificate: %w", certErr)
}
if keyErr != nil && !errors.Is(keyErr, os.ErrNotExist) {
return nil, nil, fmt.Errorf("read installation CA private key: %w", keyErr)
}
return nil, nil, errors.New("installation CA is incomplete; both certificate and private key are required")
}
// NewGeneratedManager creates an in-memory CA suitable for short-lived tools.
func NewGeneratedManager() (*Manager, []byte, error) {
certPEM, keyPEM, err := generateCA()
if err != nil {
return nil, nil, err
}
manager, err := NewManagerFromPEM(certPEM, keyPEM)
if err != nil {
return nil, nil, err
}
return manager, manager.CACertPEM(), nil
}
func generateAndPersistManager(certPath, keyPath string) (*Manager, []byte, error) {
if filepath.Dir(certPath) != filepath.Dir(keyPath) {
return nil, nil, errors.New("installation CA certificate and key must share a directory")
}
certPEM, keyPEM, err := generateCA()
if err != nil {
return nil, nil, err
}
if err := os.MkdirAll(filepath.Dir(certPath), 0o700); err != nil {
return nil, nil, fmt.Errorf("create installation CA directory: %w", err)
}
// Write the private key first so a crash cannot leave a new certificate
// without the signing key needed by the proxy.
if err := writeLocalCAFile(keyPath, keyPEM, 0o600); err != nil {
return nil, nil, fmt.Errorf("persist installation CA private key: %w", err)
}
if err := writeLocalCAFile(certPath, certPEM, 0o644); err != nil {
return nil, nil, fmt.Errorf("persist installation CA certificate: %w", err)
}
manager, err := NewManagerFromPEM(certPEM, keyPEM)
if err != nil {
return nil, nil, err
}
return manager, manager.CACertPEM(), nil
}
func generateCA() ([]byte, []byte, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 3072)
if err != nil {
return nil, nil, fmt.Errorf("generate installation CA private key: %w", err)
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, nil, fmt.Errorf("generate installation CA serial: %w", err)
}
publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, nil, fmt.Errorf("marshal installation CA public key: %w", err)
}
subjectKeyID := sha256.Sum256(publicKeyDER)
now := time.Now()
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "Cursor BYOK Local CA",
Organization: []string{"Cursor BYOK"},
},
NotBefore: now.Add(-5 * time.Minute),
NotAfter: now.AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
MaxPathLenZero: true,
SubjectKeyId: append([]byte(nil), subjectKeyID[:20]...),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("create installation CA certificate: %w", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
return certPEM, keyPEM, nil
}
func writeLocalCAFile(path string, data []byte, mode os.FileMode) error {
temp, err := os.CreateTemp(filepath.Dir(path), ".ca-*")
if err != nil {
return err
}
tempPath := temp.Name()
defer os.Remove(tempPath)
if err := temp.Chmod(mode); err != nil {
_ = temp.Close()
return err
}
if _, err := temp.Write(data); err != nil {
_ = temp.Close()
return err
}
if err := temp.Sync(); err != nil {
_ = temp.Close()
return err
}
if err := temp.Close(); err != nil {
return err
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if err := os.Rename(tempPath, path); err != nil {
return err
}
return os.Chmod(path, mode)
}
func isLegacySharedCA(certPEM []byte) bool {
block, _ := pem.Decode(certPEM)
if block == nil {
return false
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return false
}
sum := sha256.Sum256(cert.Raw)
return strings.EqualFold(hex.EncodeToString(sum[:]), legacySharedCASHA256)
}
// loadCAPEMFromFiles reads an explicitly supplied CA pair.
func loadCAPEMFromFiles(certPath, keyPath string) ([]byte, []byte, error) {
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, nil, err
}
keyPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, nil, err
}
return certPEM, keyPEM, nil
}
+125
View File
@@ -0,0 +1,125 @@
package certs
import (
"bytes"
"crypto/x509"
"encoding/pem"
"os"
"path/filepath"
"runtime"
"testing"
)
func TestLoadOrCreateManagerPersistsAndReusesInstallationCA(t *testing.T) {
dir := t.TempDir()
certPath := filepath.Join(dir, "ca.crt")
keyPath := filepath.Join(dir, "ca.key")
manager, certPEM, err := LoadOrCreateManager(certPath, keyPath)
if err != nil {
t.Fatalf("LoadOrCreateManager() error = %v", err)
}
if isLegacySharedCA(certPEM) {
t.Fatal("generated CA reused the legacy shared certificate")
}
keyInfo, err := os.Stat(keyPath)
if err != nil {
t.Fatalf("stat private key: %v", err)
}
if runtime.GOOS != "windows" && keyInfo.Mode().Perm() != 0o600 {
t.Fatalf("private key mode = %o, want 600", keyInfo.Mode().Perm())
}
leaf, err := manager.CertificateForServerName("api2.cursor.sh")
if err != nil {
t.Fatalf("CertificateForServerName() error = %v", err)
}
ca := parseCertificatePEM(t, certPEM)
roots := x509.NewCertPool()
roots.AddCert(ca)
if _, err := leaf.Leaf.Verify(x509.VerifyOptions{DNSName: "api2.cursor.sh", Roots: roots}); err != nil {
t.Fatalf("verify generated leaf: %v", err)
}
_, reusedCertPEM, err := LoadOrCreateManager(certPath, keyPath)
if err != nil {
t.Fatalf("second LoadOrCreateManager() error = %v", err)
}
if !bytes.Equal(certPEM, reusedCertPEM) {
t.Fatal("installation CA changed between loads")
}
}
func TestLoadOrCreateManagerCreatesUniqueCAsPerInstallation(t *testing.T) {
firstDir := t.TempDir()
secondDir := t.TempDir()
_, firstCert, err := LoadOrCreateManager(filepath.Join(firstDir, "ca.crt"), filepath.Join(firstDir, "ca.key"))
if err != nil {
t.Fatalf("create first CA: %v", err)
}
_, secondCert, err := LoadOrCreateManager(filepath.Join(secondDir, "ca.crt"), filepath.Join(secondDir, "ca.key"))
if err != nil {
t.Fatalf("create second CA: %v", err)
}
if bytes.Equal(firstCert, secondCert) {
t.Fatal("separate installations received the same CA certificate")
}
}
func TestLoadOrCreateManagerReplacesLegacySharedCertificate(t *testing.T) {
dir := t.TempDir()
certPath := filepath.Join(dir, "ca.crt")
keyPath := filepath.Join(dir, "ca.key")
legacyCert, err := os.ReadFile(filepath.Join("testdata", "legacy_shared_ca.crt"))
if err != nil {
t.Fatalf("read legacy certificate fixture: %v", err)
}
if !isLegacySharedCA(legacyCert) {
t.Fatal("legacy certificate fixture fingerprint changed")
}
if err := os.WriteFile(certPath, legacyCert, 0o644); err != nil {
t.Fatalf("write legacy certificate: %v", err)
}
_, generatedCert, err := LoadOrCreateManager(certPath, keyPath)
if err != nil {
t.Fatalf("migrate legacy certificate: %v", err)
}
if bytes.Equal(legacyCert, generatedCert) || isLegacySharedCA(generatedCert) {
t.Fatal("legacy shared certificate was not replaced")
}
if _, err := os.Stat(keyPath); err != nil {
t.Fatalf("generated private key missing: %v", err)
}
}
func TestNewManagerFromPEMRejectsMismatchedKey(t *testing.T) {
firstDir := t.TempDir()
secondDir := t.TempDir()
_, _, err := LoadOrCreateManager(filepath.Join(firstDir, "ca.crt"), filepath.Join(firstDir, "ca.key"))
if err != nil {
t.Fatalf("create first CA: %v", err)
}
_, _, err = LoadOrCreateManager(filepath.Join(secondDir, "ca.crt"), filepath.Join(secondDir, "ca.key"))
if err != nil {
t.Fatalf("create second CA: %v", err)
}
certPEM, _ := os.ReadFile(filepath.Join(firstDir, "ca.crt"))
keyPEM, _ := os.ReadFile(filepath.Join(secondDir, "ca.key"))
if _, err := NewManagerFromPEM(certPEM, keyPEM); err == nil {
t.Fatal("NewManagerFromPEM() accepted a mismatched private key")
}
}
func parseCertificatePEM(t *testing.T, certPEM []byte) *x509.Certificate {
t.Helper()
block, _ := pem.Decode(certPEM)
if block == nil {
t.Fatal("certificate PEM is invalid")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse certificate: %v", err)
}
return cert
}