Add support for CA key and certificate generation

This commit is contained in:
David Stotijn 2020-09-28 20:37:25 +02:00
parent 81ae8f55da
commit 8b04747855
6 changed files with 100 additions and 29 deletions

View file

@ -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

View file

@ -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)
}

1
go.mod
View file

@ -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
)

1
go.sum
View file

@ -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=

View file

@ -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
}

View file

@ -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,
}