srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/ca
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-01-16 16:26:00 +0200
committersrdusr <[email protected]>2024-01-16 16:26:00 +0200
commit1125afc47b9d6e68d95d0ffdbb74514f4618e624 (patch)
tree63d5150b7dbc74685ebaec35ee0e74ec30c721d5 /internal/ca
downloadmitmux-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.go153
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
+}