diff options
| author | srdusr <[email protected]> | 2024-01-16 16:26:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-01-16 16:26:00 +0200 |
| commit | 1125afc47b9d6e68d95d0ffdbb74514f4618e624 (patch) | |
| tree | 63d5150b7dbc74685ebaec35ee0e74ec30c721d5 /internal/ca | |
| download | mitmux-1125afc47b9d6e68d95d0ffdbb74514f4618e624.tar.gz mitmux-1125afc47b9d6e68d95d0ffdbb74514f4618e624.zip | |
Scaffold mitmux: proxy daemon, CA generation, HTTP/CONNECT passthrough
Implements build-order step 1: headless proxy daemon (mitmuxd) with
plaintext HTTP passthrough and raw CONNECT tunneling, plus root CA
generation/persistence for later TLS interception. Verified live
against real HTTP and HTTPS requests through the proxy.
Diffstat (limited to 'internal/ca')
| -rw-r--r-- | internal/ca/ca.go | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/ca/ca.go b/internal/ca/ca.go new file mode 100644 index 0000000..949c2fd --- /dev/null +++ b/internal/ca/ca.go @@ -0,0 +1,153 @@ +// Package ca manages mitmux's root CA: generating it on first run and +// loading it on subsequent runs. Leaf certificate signing for TLS +// interception is added in a later build step. +package ca + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "os" + "path/filepath" + "time" +) + +const ( + certFileName = "ca.pem" + keyFileName = "ca-key.pem" +) + +// CA holds the root certificate and key used to sign per-host leaf +// certificates during TLS interception. +type CA struct { + Cert *x509.Certificate + Key *ecdsa.PrivateKey + CertPEM []byte + KeyPEM []byte +} + +// Dir returns the directory mitmux stores its CA material in +// (XDG config dir, e.g. ~/.config/mitmux). +func Dir() (string, error) { + cfg, err := os.UserConfigDir() + if err != nil { + return "", fmt.Errorf("resolve config dir: %w", err) + } + return filepath.Join(cfg, "mitmux"), nil +} + +// EnsureCA loads the CA from dir, generating and persisting a new one if +// none exists yet. +func EnsureCA(dir string) (*CA, error) { + certPath := filepath.Join(dir, certFileName) + keyPath := filepath.Join(dir, keyFileName) + + certPEM, certErr := os.ReadFile(certPath) + keyPEM, keyErr := os.ReadFile(keyPath) + if certErr == nil && keyErr == nil { + return load(certPEM, keyPEM) + } + if !errors.Is(certErr, os.ErrNotExist) && certErr != nil { + return nil, fmt.Errorf("read %s: %w", certPath, certErr) + } + if !errors.Is(keyErr, os.ErrNotExist) && keyErr != nil { + return nil, fmt.Errorf("read %s: %w", keyPath, keyErr) + } + + ca, err := generate() + if err != nil { + return nil, fmt.Errorf("generate CA: %w", err) + } + if err := persist(dir, ca); err != nil { + return nil, fmt.Errorf("persist CA: %w", err) + } + return ca, nil +} + +func generate() (*CA, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, err + } + + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: "mitmux local CA", + Organization: []string{"mitmux"}, + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + return nil, err + } + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + + return &CA{ + Cert: cert, + Key: key, + CertPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), + KeyPEM: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), + }, nil +} + +func load(certPEM, keyPEM []byte) (*CA, error) { + certBlock, _ := pem.Decode(certPEM) + if certBlock == nil { + return nil, errors.New("no PEM block found in CA certificate") + } + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("parse CA certificate: %w", err) + } + + keyBlock, _ := pem.Decode(keyPEM) + if keyBlock == nil { + return nil, errors.New("no PEM block found in CA key") + } + key, err := x509.ParseECPrivateKey(keyBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("parse CA key: %w", err) + } + + return &CA{Cert: cert, Key: key, CertPEM: certPEM, KeyPEM: keyPEM}, nil +} + +func persist(dir string, ca *CA) error { + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, certFileName), ca.CertPEM, 0o644); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, keyFileName), ca.KeyPEM, 0o600); err != nil { + return err + } + return nil +} |