From 8b047478557755757fe7ced0d36e78b25c295dea Mon Sep 17 00:00:00 2001 From: David Stotijn Date: Mon, 28 Sep 2020 20:37:25 +0200 Subject: [PATCH] Add support for CA key and certificate generation --- README.md | 28 +++++++++--------- cmd/hetty/main.go | 23 +++++++++------ go.mod | 1 + go.sum | 1 + modd.conf | 4 --- pkg/proxy/cert.go | 72 +++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 100 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 674a361..4f26c08 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ - [ ] Full text search (with regex) in proxy log viewer. - [ ] Project management. - [ ] Sender module for sending manual HTTP requests, either from scratch or based - off requests from the proxy log. + off requests from the proxy log. - [ ] Attacker module for automated sending of HTTP requests. Leverage the concurrency - features of Go and its `net/http` package to make it blazingly fast. + features of Go and its `net/http` package to make it blazingly fast. ## Installation @@ -57,11 +57,11 @@ on Docker Hub. ``` $ docker run \ --v $HOME/.ssh/hetty_key.pem:/.ssh/hetty_key.pem \ --v $HOME/.ssh/hetty_cert.pem:/.ssh/hetty_cert.pem \ +-v $HOME/.hetty/hetty_key.pem:/root/.hetty/hetty_key.pem \ +-v $HOME/.hetty/hetty_cert.pem:/root/.hetty/hetty_cert.pem \ -v $HOME/.hetty/hetty.db:/app/hetty.db \ -p 127.0.0.1:8080:80 \ -dstotijn/hetty -key /.ssh/hetty_key.pem -cert /.ssh/hetty_cert.pem -db hetty.db +dstotijn/hetty ``` ## Usage @@ -72,23 +72,21 @@ http://localhost:8080. Depending on incoming HTTP requests, it either acts as a MITM proxy, or it serves the GraphQL API and web interface (Next.js). ``` -$ hetty -h -Usage of hetty: +$ Usage of ./hetty: -addr string - TCP address to listen on, in the form "host:port" (default ":80") + TCP address to listen on, in the form "host:port" (default ":80") -adminPath string - File path to admin build + File path to admin build -cert string - CA certificate file path + CA certificate filepath. Creates a new CA certificate is file doesn't exist (default "~/.hetty/hetty_cert.pem") -db string - Database file path (default "hetty.db") + Database file path (default "hetty.db") -key string - CA private key file path + CA private key filepath. Creates a new CA private key if file doesn't exist (default "~/.hetty/hetty_key.pem") ``` -**Note:** There is no built-in in support yet for generating a CA certificate. -This will be added really soon in an upcoming release. In the meantime, please -use `openssl` (_TODO: add instructions_). +⚠️ _Todo: Write instructions for installing CA certificate in local CA store, and_ +_configuring Hetty to be used as a proxy server._ ## Vision and roadmap diff --git a/cmd/hetty/main.go b/cmd/hetty/main.go index 7224cd8..3c4d7cd 100644 --- a/cmd/hetty/main.go +++ b/cmd/hetty/main.go @@ -2,7 +2,6 @@ package main import ( "crypto/tls" - "crypto/x509" "flag" "log" "net" @@ -19,6 +18,7 @@ import ( "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/gorilla/mux" + "github.com/mitchellh/go-homedir" ) var ( @@ -30,21 +30,28 @@ var ( ) func main() { - flag.StringVar(&caCertFile, "cert", "", "CA certificate file path") - flag.StringVar(&caKeyFile, "key", "", "CA private key file path") + flag.StringVar(&caCertFile, "cert", "~/.hetty/hetty_cert.pem", "CA certificate filepath. Creates a new CA certificate is file doesn't exist") + flag.StringVar(&caKeyFile, "key", "~/.hetty/hetty_key.pem", "CA private key filepath. Creates a new CA private key if file doesn't exist") flag.StringVar(&dbFile, "db", "hetty.db", "Database file path") flag.StringVar(&addr, "addr", ":80", "TCP address to listen on, in the form \"host:port\"") flag.StringVar(&adminPath, "adminPath", "", "File path to admin build") flag.Parse() - tlsCA, err := tls.LoadX509KeyPair(caCertFile, caKeyFile) + // Expand `~` in filepaths. + caCertFile, err := homedir.Expand(caCertFile) if err != nil { - log.Fatalf("[FATAL] Could not load CA key pair: %v", err) + log.Fatalf("[FATAL] Could not parse CA certificate filepath: %v", err) + } + caKeyFile, err := homedir.Expand(caKeyFile) + if err != nil { + log.Fatalf("[FATAL] Could not parse CA private key filepath: %v", err) } - caCert, err := x509.ParseCertificate(tlsCA.Certificate[0]) + // Load existing CA certificate and key from disk, or generate and write + // to disk if no files exist yet. + caCert, caKey, err := proxy.LoadOrCreateCA(caKeyFile, caCertFile) if err != nil { - log.Fatalf("[FATAL] Could not parse CA: %v", err) + log.Fatalf("[FATAL] Could not create/load CA key pair: %v", err) } db, err := cayley.NewDatabase(dbFile) @@ -55,7 +62,7 @@ func main() { reqLogService := reqlog.NewService(db) - p, err := proxy.NewProxy(caCert, tlsCA.PrivateKey) + p, err := proxy.NewProxy(caCert, caKey) if err != nil { log.Fatalf("[FATAL] Could not create Proxy: %v", err) } diff --git a/go.mod b/go.mod index 3ae6ca2..313597c 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/uuid v1.1.2 github.com/gorilla/mux v1.7.4 github.com/hidal-go/hidalgo v0.0.0-20190814174001-42e03f3b5eaa + github.com/mitchellh/go-homedir v1.1.0 github.com/vektah/gqlparser/v2 v2.0.1 golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346 // indirect ) diff --git a/go.sum b/go.sum index 69b5fb5..ef997c2 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= diff --git a/modd.conf b/modd.conf index 8cac65d..0747742 100644 --- a/modd.conf +++ b/modd.conf @@ -1,12 +1,8 @@ -@cert = $HOME/.ssh/hetty_cert.pem -@key = $HOME/.ssh/hetty_key.pem @db = hetty.bolt @addr = :8080 **/*.go { daemon +sigterm: go run ./cmd/hetty \ - -cert=@cert \ - -key=@key \ -db=@db \ -addr=@addr } \ No newline at end of file diff --git a/pkg/proxy/cert.go b/pkg/proxy/cert.go index d8b5f66..9798410 100644 --- a/pkg/proxy/cert.go +++ b/pkg/proxy/cert.go @@ -9,9 +9,13 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/pem" "errors" + "fmt" "math/big" "net" + "os" + "path/filepath" "time" ) @@ -56,6 +60,70 @@ func NewCertConfig(ca *x509.Certificate, caPrivKey crypto.PrivateKey) (*CertConf }, nil } +// LoadOrCreateCA loads an existing CA key pair from disk, or creates +// a new keypair and saves to disk if certificate or key files don't exist. +func LoadOrCreateCA(caKeyFile, caCertFile string) (*x509.Certificate, *rsa.PrivateKey, error) { + tlsCA, err := tls.LoadX509KeyPair(caCertFile, caKeyFile) + if err == nil { + caCert, err := x509.ParseCertificate(tlsCA.Certificate[0]) + if err != nil { + return nil, nil, fmt.Errorf("proxy: could not parse CA: %v", err) + } + caKey, ok := tlsCA.PrivateKey.(*rsa.PrivateKey) + if !ok { + return nil, nil, errors.New("proxy: private key is not RSA") + } + return caCert, caKey, nil + } + if !os.IsNotExist(err) { + return nil, nil, fmt.Errorf("proxy: could not load CA key pair: %v", err) + } + + // Create directories for files if they don't exist yet. + keyDir, _ := filepath.Split(caKeyFile) + if keyDir != "" { + if _, err := os.Stat(keyDir); os.IsNotExist(err) { + os.Mkdir(keyDir, 0755) + } + } + keyDir, _ = filepath.Split(caCertFile) + if keyDir != "" { + if _, err := os.Stat("keyDir"); os.IsNotExist(err) { + os.Mkdir(keyDir, 0755) + } + } + + // Create new CA keypair. + caCert, caKey, err := NewCA("Hetty", "Hetty CA", time.Duration(365*24*time.Hour)) + if err != nil { + return nil, nil, fmt.Errorf("proxy: could not generate new CA keypair: %v", err) + } + + // Open CA certificate and key files for writing. + certOut, err := os.Create(caCertFile) + if err != nil { + return nil, nil, fmt.Errorf("proxy: could not open cert file for writing: %v", err) + } + keyOut, err := os.OpenFile(caKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return nil, nil, fmt.Errorf("proxy: could not open key file for writing: %v", err) + } + + // Write PEM blocks to CA certificate and key files. + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}); err != nil { + return nil, nil, fmt.Errorf("proxy: could not write CA certificate to disk: %v", err) + } + privBytes, err := x509.MarshalPKCS8PrivateKey(caKey) + if err != nil { + return nil, nil, fmt.Errorf("proxy: could not convert private key to DER format: %v", err) + } + if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { + return nil, nil, fmt.Errorf("proxy: could not write CA key to disk: %v", err) + } + + return caCert, caKey, nil +} + // NewCA creates a new CA certificate and associated private key. func NewCA(name, organization string, validity time.Duration) (*x509.Certificate, *rsa.PrivateKey, error) { priv, err := rsa.GenerateKey(rand.Reader, 2048) @@ -91,8 +159,8 @@ func NewCA(name, organization string, validity time.Duration) (*x509.Certificate KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, - NotBefore: time.Now().Add(-24 * time.Hour), - NotAfter: time.Now().Add(24 * time.Hour), + NotBefore: time.Now(), + NotAfter: time.Now().Add(validity), DNSNames: []string{name}, IsCA: true, }