178 lines
6.2 KiB
Go
178 lines
6.2 KiB
Go
package proxy
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"crypto/x509/pkix"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"math/big"
|
||
"os"
|
||
"path/filepath"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// CA — локальный корневой сертификат для перехвата HTTPS-трафика (MITM),
|
||
// по аналогии с Burp CA / mitmproxy CA. Генерируется один раз и
|
||
// сохраняется на диск; чтобы браузер перестал ругаться на HTTPS-сайты,
|
||
// ca.pem нужно один раз импортировать в доверенные корневые сертификаты
|
||
// браузера/ОС.
|
||
type CA struct {
|
||
cert *x509.Certificate
|
||
key *rsa.PrivateKey
|
||
|
||
leafCache sync.Map // host string -> *tls.Certificate
|
||
}
|
||
|
||
// CADir возвращает директорию для хранения CA (~/.burterm).
|
||
func CADir() (string, error) {
|
||
home, err := os.UserHomeDir()
|
||
if err != nil {
|
||
return "", fmt.Errorf("не удалось определить домашнюю директорию: %w", err)
|
||
}
|
||
return filepath.Join(home, ".burterm"), nil
|
||
}
|
||
|
||
// LoadOrCreateCA загружает CA из dir/ca.pem + dir/ca-key.pem, либо создаёт
|
||
// новый и сохраняет туда же, если файлов ещё нет.
|
||
func LoadOrCreateCA(dir string) (*CA, error) {
|
||
certPath := filepath.Join(dir, "ca.pem")
|
||
keyPath := filepath.Join(dir, "ca-key.pem")
|
||
|
||
certBytes, err := os.ReadFile(certPath)
|
||
if err == nil {
|
||
keyBytes, err := os.ReadFile(keyPath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("найден ca.pem, но не найден ca-key.pem рядом: %w", err)
|
||
}
|
||
return parseCA(certBytes, keyBytes)
|
||
}
|
||
|
||
return createCA(dir, certPath, keyPath)
|
||
}
|
||
|
||
func createCA(dir, certPath, keyPath string) (*CA, error) {
|
||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||
return nil, fmt.Errorf("не удалось создать %s: %w", dir, err)
|
||
}
|
||
|
||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("генерация ключа CA: %w", err)
|
||
}
|
||
|
||
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("генерация серийного номера CA: %w", err)
|
||
}
|
||
|
||
tmpl := &x509.Certificate{
|
||
SerialNumber: serial,
|
||
Subject: pkix.Name{
|
||
CommonName: "burterm MITM CA",
|
||
Organization: []string{"burterm"},
|
||
},
|
||
NotBefore: time.Now().Add(-24 * time.Hour),
|
||
NotAfter: time.Now().AddDate(10, 0, 0),
|
||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign,
|
||
BasicConstraintsValid: true,
|
||
IsCA: true,
|
||
}
|
||
|
||
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("создание сертификата CA: %w", err)
|
||
}
|
||
|
||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||
|
||
if err := os.WriteFile(certPath, certPEM, 0o644); err != nil {
|
||
return nil, fmt.Errorf("сохранение ca.pem: %w", err)
|
||
}
|
||
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
||
return nil, fmt.Errorf("сохранение ca-key.pem: %w", err)
|
||
}
|
||
|
||
cert, err := x509.ParseCertificate(certDER)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("разбор свежесозданного сертификата CA: %w", err)
|
||
}
|
||
|
||
return &CA{cert: cert, key: key}, nil
|
||
}
|
||
|
||
func parseCA(certPEMBytes, keyPEMBytes []byte) (*CA, error) {
|
||
certBlock, _ := pem.Decode(certPEMBytes)
|
||
if certBlock == nil {
|
||
return nil, fmt.Errorf("ca.pem повреждён или пуст")
|
||
}
|
||
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("разбор ca.pem: %w", err)
|
||
}
|
||
|
||
keyBlock, _ := pem.Decode(keyPEMBytes)
|
||
if keyBlock == nil {
|
||
return nil, fmt.Errorf("ca-key.pem повреждён или пуст")
|
||
}
|
||
key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("разбор ca-key.pem: %w", err)
|
||
}
|
||
|
||
return &CA{cert: cert, key: key}, nil
|
||
}
|
||
|
||
// LeafFor возвращает (создавая и кэшируя при первом обращении) листовой
|
||
// TLS-сертификат для host, подписанный этим CA. Используется в
|
||
// tls.Config.GetCertificate при перехвате CONNECT-туннеля — по сертификату
|
||
// на каждый домен, с которым говорит браузер через прокси.
|
||
func (ca *CA) LeafFor(host string) (*tls.Certificate, error) {
|
||
if cached, ok := ca.leafCache.Load(host); ok {
|
||
return cached.(*tls.Certificate), nil
|
||
}
|
||
|
||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("генерация ключа для %s: %w", host, err)
|
||
}
|
||
|
||
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("генерация серийного номера для %s: %w", host, err)
|
||
}
|
||
|
||
tmpl := &x509.Certificate{
|
||
SerialNumber: serial,
|
||
Subject: pkix.Name{CommonName: host},
|
||
DNSNames: []string{host},
|
||
NotBefore: time.Now().Add(-24 * time.Hour),
|
||
NotAfter: time.Now().AddDate(1, 0, 0),
|
||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||
}
|
||
|
||
der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, &key.PublicKey, ca.key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("подпись сертификата для %s: %w", host, err)
|
||
}
|
||
|
||
leaf := &tls.Certificate{
|
||
Certificate: [][]byte{der, ca.cert.Raw},
|
||
PrivateKey: key,
|
||
}
|
||
|
||
actual, _ := ca.leafCache.LoadOrStore(host, leaf)
|
||
return actual.(*tls.Certificate), nil
|
||
}
|
||
|
||
// CertPath — путь к ca.pem, который нужно один раз импортировать
|
||
// в доверенные корневые сертификаты браузера/ОС, иначе HTTPS-сайты
|
||
// будут показывать предупреждение о недоверенном сертификате.
|
||
func (ca *CA) CertPath(dir string) string {
|
||
return filepath.Join(dir, "ca.pem")
|
||
}
|