From 8844427927f5a709024f26ed52ba5dd363213b2d Mon Sep 17 00:00:00 2001 From: Florian Bauer Date: Tue, 21 Jan 2025 08:33:49 +0100 Subject: [PATCH] feat: crl support, metrics --- .gitignore | 1 + go.mod | 10 ++++ go.sum | 28 ++++++++- internal/metrics/metrics.go | 34 +++++++++++ internal/metrics/middleware.go | 35 +++++++++++ internal/metrics/response_writer.go | 19 ++++++ internal/ocsp_source/source.go | 18 +----- main.go | 92 +++++++++++++++++++++-------- test.sh | 16 ++++- 9 files changed, 207 insertions(+), 46 deletions(-) create mode 100644 .gitignore create mode 100644 internal/metrics/metrics.go create mode 100644 internal/metrics/middleware.go create mode 100644 internal/metrics/response_writer.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/go.mod b/go.mod index ac569a2..c3480a2 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,24 @@ go 1.23.4 require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/cloudflare/cfssl v1.6.5 + github.com/prometheus/client_golang v1.20.5 golang.org/x/crypto v0.32.0 ) require ( github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/google/certificate-transparency-go v1.1.7 // indirect github.com/jmhodges/clock v1.2.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + golang.org/x/sys v0.29.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 9af9c82..79639c2 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI= github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -12,28 +16,48 @@ github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrt github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..a29dbd0 --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,34 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const ( + labelPath = "path" + labelStatus = "status" +) + +var ( + totalRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Number of get requests.", + }, []string{labelPath}) + + responseStatus = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "response_status", + Help: "Status of HTTP response", + }, []string{labelPath, labelStatus}) + + httpDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "http_response_time_seconds", + Help: "Duration of HTTP requests.", + Buckets: prometheus.DefBuckets, + }, []string{labelPath}) +) + +func init() { + prometheus.MustRegister(totalRequests) + prometheus.MustRegister(responseStatus) + prometheus.MustRegister(httpDuration) +} diff --git a/internal/metrics/middleware.go b/internal/metrics/middleware.go new file mode 100644 index 0000000..7e2a9d7 --- /dev/null +++ b/internal/metrics/middleware.go @@ -0,0 +1,35 @@ +package metrics + +import ( + "log" + "net/http" + "strconv" + + "github.com/prometheus/client_golang/prometheus" +) + +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + timer := prometheus.NewTimer(httpDuration.With(prometheus.Labels{ + labelPath: path, + })) + rw := newResponseWriter(w) + next.ServeHTTP(rw, r) + if rw.statusCode == 0 { + rw.WriteHeader(http.StatusOK) + } + statusCode := rw.statusCode + + responseStatus.With(prometheus.Labels{ + labelPath: path, + labelStatus: strconv.Itoa(statusCode), + }).Inc() + totalRequests.With(prometheus.Labels{ + labelPath: path, + }).Inc() + + log.Printf("%s %s %s %d %s", r.RemoteAddr, r.Method, r.URL.Path, statusCode, timer.ObserveDuration()) + }) +} diff --git a/internal/metrics/response_writer.go b/internal/metrics/response_writer.go new file mode 100644 index 0000000..0db68b8 --- /dev/null +++ b/internal/metrics/response_writer.go @@ -0,0 +1,19 @@ +package metrics + +import "net/http" + +func newResponseWriter(w http.ResponseWriter) *responseWriter { + return &responseWriter{w, http.StatusOK} +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + if code != http.StatusOK { + rw.ResponseWriter.WriteHeader(code) + } +} + +type responseWriter struct { + http.ResponseWriter + statusCode int +} diff --git a/internal/ocsp_source/source.go b/internal/ocsp_source/source.go index 694726f..daac7e6 100644 --- a/internal/ocsp_source/source.go +++ b/internal/ocsp_source/source.go @@ -4,11 +4,8 @@ import ( "crypto" "crypto/tls" "crypto/x509" - "encoding/pem" - "fmt" "math/big" "net/http" - "os" "time" "golang.org/x/crypto/ocsp" @@ -29,21 +26,8 @@ func NewCrlSource(caCertificate *x509.Certificate, responderKeyPair tls.Certific } } -func (source *CrlSource) LoadCrlFromFile(path string) error { - crlContent, openCrlError := os.ReadFile(path) - if openCrlError != nil { - return openCrlError - } - block, rest := pem.Decode(crlContent) - if len(rest) > 0 { - return fmt.Errorf("failed to decode crl") - } - crl, parseCrlError := x509.ParseRevocationList(block.Bytes) - if parseCrlError != nil { - return parseCrlError - } +func (source *CrlSource) UseCrl(crl *x509.RevocationList) { source.crl = crl - return nil } func (source *CrlSource) Response(request *ocsp.Request) ([]byte, http.Header, error) { diff --git a/main.go b/main.go index 69c13ad..22a9645 100644 --- a/main.go +++ b/main.go @@ -4,17 +4,36 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "fmt" + "github.com/prometheus/client_golang/prometheus/promhttp" + "log" "net/http" + "ocspcrl/internal/metrics" "os" "os/signal" "syscall" "github.com/alecthomas/kingpin/v2" cfocsp "github.com/cloudflare/cfssl/ocsp" - "ocspcrl/internal/ocsp_source" ) +func loadCrlFromFile(path string) (*x509.RevocationList, error) { + crlContent, openCrlError := os.ReadFile(path) + if openCrlError != nil { + return nil, openCrlError + } + block, rest := pem.Decode(crlContent) + if len(rest) > 0 { + return nil, fmt.Errorf("failed to decode crl") + } + crl, parseCrlError := x509.ParseRevocationList(block.Bytes) + if parseCrlError != nil { + return nil, parseCrlError + } + return crl, nil +} + type responder struct { certificatePath string keyPath string @@ -24,24 +43,19 @@ type crlSourceFile struct { path string } -type addresses struct { - ocsp string - crl string -} - type configuration struct { - responder *responder - caCrtPath string - crlSourceType string - crlSourceFile *crlSourceFile - addresses *addresses + responder *responder + caCrtPath string + crlSourceType string + crlSourceFile *crlSourceFile + applicationListenAddress string + metricsListenAddress string } func main() { config := &configuration{ responder: &responder{}, crlSourceFile: &crlSourceFile{}, - addresses: &addresses{}, } app := kingpin.New("ocspcrl", "OCSP responder / CRL server") app.HelpFlag.Short('h') @@ -50,42 +64,68 @@ func main() { app.Flag("ca-crt-path", "Path to the CA certificate").Envar("CA_CRL_PATH").Required().ExistingFileVar(&config.caCrtPath) app.Flag("crl-source-type", "Type of CRL source").Envar("CRL_SOURCE").Default("file").EnumVar(&config.crlSourceType, "file") app.Flag("source.file.path", "Path to the CRL file").Envar("SOURCE_FILE_PATH").ExistingFileVar(&config.crlSourceFile.path) - app.Flag("ocsp.listen-address", "Address for ocsp endpoint").Envar("OCSP_LISTEN_ADDRESS").Default(":8080").StringVar(&config.addresses.ocsp) - app.Flag("crl.listen-address", "Address for crl endpoint").Envar("CRL_LISTEN_ADDRESS").Default(":8081").StringVar(&config.addresses.crl) + app.Flag("web.listen-address", "Address for application endpoint").Envar("WEB_LISTEN_ADDRESS").Default(":8080").StringVar(&config.applicationListenAddress) + app.Flag("metrics.listen-address", "Address for metrics endpoint").Envar("METRICS_LISTEN_ADDRESS").Default("[::1]:8081").StringVar(&config.metricsListenAddress) kingpin.MustParse(app.Parse(os.Args[1:])) responderKeyPair, loadResponderKeyPairError := tls.LoadX509KeyPair(config.responder.certificatePath, config.responder.keyPath) if loadResponderKeyPairError != nil { - panic(loadResponderKeyPairError) + log.Fatalf("failed to load responder key pair: %v", loadResponderKeyPairError) } caCrtContent, openCaCrtError := os.ReadFile(config.caCrtPath) if openCaCrtError != nil { - panic(openCaCrtError) + log.Fatalf("failed to open ca certificate: %v", openCaCrtError) } block, rest := pem.Decode(caCrtContent) if len(rest) > 0 { - panic("failed to decode ca certificate") + log.Fatalln("failed to decode ca certificate") } caCertificate, loadCaCertificateError := x509.ParseCertificate(block.Bytes) if loadCaCertificateError != nil { - panic(loadCaCertificateError) + log.Fatalf("failed to parse ca certificate: %v", loadCaCertificateError) } source := ocsp_source.NewCrlSource(caCertificate, responderKeyPair) - loadCrlError := source.LoadCrlFromFile(config.crlSourceFile.path) + crl, loadCrlError := loadCrlFromFile(config.crlSourceFile.path) if loadCrlError != nil { - panic(loadCrlError) + log.Fatalf("failed to load crl: %v", loadCrlError) } + source.UseCrl(crl) signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) - responder := cfocsp.NewResponder(source, nil) - listenError := http.ListenAndServe(config.addresses.ocsp, responder) - if listenError != nil { - panic(listenError) - } + applicationRouter := http.NewServeMux() + applicationRouter.Handle("/ocsp", cfocsp.NewResponder(source, nil)) + applicationRouter.HandleFunc("/crl", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/pkix-crl") + w.Write(crl.Raw) + }) - // TODO: Implement CRL server + applicationServer := &http.Server{Addr: config.applicationListenAddress, Handler: metrics.Middleware(applicationRouter)} + metricsSever := &http.Server{Addr: config.metricsListenAddress, Handler: promhttp.Handler()} + + applicationServerClosed := make(chan any) + metricsServerClosed := make(chan any) + go func() { + log.Printf("starting application server on %s", config.applicationListenAddress) + if listenError := applicationServer.ListenAndServe(); listenError != nil { + log.Printf("application error: %v", listenError) + } + close(applicationServerClosed) + }() + go func() { + log.Printf("starting metrics server on %s", config.metricsListenAddress) + if listenError := metricsSever.ListenAndServe(); listenError != nil { + log.Printf("metrics error: %v", listenError) + } + close(metricsServerClosed) + }() + + <-signalChan + applicationServer.Shutdown(nil) + metricsSever.Shutdown(nil) + <-applicationServerClosed + <-metricsServerClosed } diff --git a/test.sh b/test.sh index bf2a242..9ace0ff 100644 --- a/test.sh +++ b/test.sh @@ -1 +1,15 @@ -openssl ocsp -CAfile ../../ca/ca.crt -url http://127.0.0.1:8080 -issuer ../../ca/ca.crt -resp_text -cert ../../test.crt +#!/usr/bin/env bash +set -xeou pipefail + +# go run main.go --responder.certificate-path ../tinypki/ca/ca.crt --responder.key-path ../tinypki/ca/ca.key --ca-crt-path ../tinypki/ca/ca.crt --source.file.path ../tinypki/root.crl + +ca_dir="$(dirname $(readlink -f $0))/../tinypki" +ocsp_url="$(openssl x509 -noout -ocsp_uri -in $ca_dir/dev-server.crt)" +openssl ocsp \ + -CAfile $ca_dir/ca/ca.crt \ + -url "$ocsp_url" \ + -issuer $ca_dir/ca/ca.crt \ + -resp_text \ + -cert $ca_dir/dev-server.crt + +openssl verify -crl_check -crl_download -CAfile $ca_dir/ca/ca.crt $ca_dir/dev-server.crt \ No newline at end of file