diff --git a/.travis.yml b/.travis.yml index 4df32e3..41bd02d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: go go: - - 1.11.x - 1.12.x - 1.13.x - tip diff --git a/README.md b/README.md index 1338a32..23f00f4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ If you don't have time to waste configuring, hosting, debugging and maintaining # Getting started ## Installation ### Building from source -To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the +To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.12 or newer environment and then run ```bash $ go get github.com/adnanh/webhook ``` @@ -83,6 +83,8 @@ However, hook defined like that could pose a security threat to your system, bec ## Using HTTPS [webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate. +TLS version and cipher suite selection flags are available from the command line. To list available cipher suites, use the `-list-cipher-suites` flag. The `-tls-min-version` flag can be used with `-list-cipher-suites`. + ## CORS Headers If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response. diff --git a/cipher_suites.go b/cipher_suites.go new file mode 100644 index 0000000..81db51f --- /dev/null +++ b/cipher_suites.go @@ -0,0 +1,102 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Copied from Go 1.14 tip src/crypto/tls/cipher_suites.go + +package main + +import ( + "crypto/tls" + "fmt" +) + +// CipherSuite is a TLS cipher suite. Note that most functions in this package +// accept and expose cipher suite IDs instead of this type. +type CipherSuite struct { + ID uint16 + Name string + + // Supported versions is the list of TLS protocol versions that can + // negotiate this cipher suite. + SupportedVersions []uint16 + + // Insecure is true if the cipher suite has known security issues + // due to its primitives, design, or implementation. + Insecure bool +} + +var ( + supportedUpToTLS12 = []uint16{tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12} + supportedOnlyTLS12 = []uint16{tls.VersionTLS12} + supportedOnlyTLS13 = []uint16{tls.VersionTLS13} +) + +// CipherSuites returns a list of cipher suites currently implemented by this +// package, excluding those with security issues, which are returned by +// InsecureCipherSuites. +// +// The list is sorted by ID. Note that the default cipher suites selected by +// this package might depend on logic that can't be captured by a static list. +func CipherSuites() []*CipherSuite { + return []*CipherSuite{ + {tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false}, + {tls.TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false}, + + {tls.TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false}, + {tls.TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false}, + {tls.TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false}, + + {tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false}, + {tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false}, + {tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false}, + {tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false}, + {tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false}, + + // go1.14 + // {tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false}, + // {tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false}, + } +} + +// InsecureCipherSuites returns a list of cipher suites currently implemented by +// this package and which have security issues. +// +// Most applications should not use the cipher suites in this list, and should +// only use those returned by CipherSuites. +func InsecureCipherSuites() []*CipherSuite { + // RC4 suites are broken because RC4 is. + // CBC-SHA256 suites have no Lucky13 countermeasures. + return []*CipherSuite{ + {tls.TLS_RSA_WITH_RC4_128_SHA, "TLS_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true}, + {tls.TLS_RSA_WITH_AES_128_CBC_SHA256, "TLS_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true}, + {tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", supportedUpToTLS12, true}, + {tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true}, + {tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true}, + {tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true}, + } +} + +// CipherSuiteName returns the standard name for the passed cipher suite ID +// (e.g. "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"), or a fallback representation +// of the ID value if the cipher suite is not implemented by this package. +func CipherSuiteName(id uint16) string { + for _, c := range CipherSuites() { + if c.ID == id { + return c.Name + } + } + for _, c := range InsecureCipherSuites() { + if c.ID == id { + return c.Name + } + } + return fmt.Sprintf("0x%04X", id) +} diff --git a/docs/Webhook-Parameters.md b/docs/Webhook-Parameters.md index ab51bea..4e68ebb 100644 --- a/docs/Webhook-Parameters.md +++ b/docs/Webhook-Parameters.md @@ -3,6 +3,8 @@ Usage of webhook: -cert string path to the HTTPS certificate pem file (default "cert.pem") + -cipher-suites string + comma-separated list of supported TLS cipher suites -header value response header to return, specified in format name=value, use multiple times to set multiple headers -hooks value @@ -13,6 +15,8 @@ Usage of webhook: ip the webhook should serve hooks on (default "0.0.0.0") -key string path to the HTTPS certificate private key pem file (default "key.pem") + -list-cipher-suites + list available TLS cipher suites -nopanic do not panic if hooks cannot be loaded when webhook is not running in verbose mode -port int @@ -21,6 +25,8 @@ Usage of webhook: use HTTPS instead of HTTP -template parse hooks file as a Go template + -tls-min-version string + minimum TLS version (1.0, 1.1, 1.2, 1.3) (default "1.2") -urlprefix string url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks") -verbose @@ -35,4 +41,4 @@ Use any of the above specified flags to override their default behavior. If you are running an OS that supports USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance. ```bash kill -USR1 webhookpid -``` \ No newline at end of file +``` diff --git a/tls.go b/tls.go new file mode 100644 index 0000000..8e6cb73 --- /dev/null +++ b/tls.go @@ -0,0 +1,85 @@ +package main + +import ( + "crypto/tls" + "io" + "log" + "strings" +) + +func writeTLSSupportedCipherStrings(w io.Writer, min uint16) error { + for _, c := range CipherSuites() { + var found bool + + for _, v := range c.SupportedVersions { + if v >= min { + found = true + } + } + + if !found { + continue + } + + _, err := w.Write([]byte(c.Name + "\n")) + if err != nil { + return err + } + } + + return nil +} + +// getTLSMinVersion converts a version string into a TLS version ID. +func getTLSMinVersion(v string) uint16 { + switch v { + case "1.0": + return tls.VersionTLS10 + case "1.1": + return tls.VersionTLS11 + case "1.2", "": + return tls.VersionTLS12 + case "1.3": + return tls.VersionTLS13 + default: + log.Fatalln("error: unknown minimum TLS version:", v) + return 0 + } +} + +// getTLSCipherSuites converts a comma separated list of cipher suites into a +// slice of TLS cipher suite IDs. +func getTLSCipherSuites(v string) []uint16 { + supported := CipherSuites() + + if v == "" { + suites := make([]uint16, len(supported)) + + for _, cs := range supported { + suites = append(suites, cs.ID) + } + + return suites + } + + var found bool + txts := strings.Split(v, ",") + suites := make([]uint16, len(txts)) + + for _, want := range txts { + found = false + + for _, cs := range supported { + if want == cs.Name { + suites = append(suites, cs.ID) + found = true + } + } + + if !found { + log.Fatalln("error: unknown TLS cipher suite:", want) + } + } + + return suites +} diff --git a/webhook.go b/webhook.go index 7e37aef..aedf7c5 100644 --- a/webhook.go +++ b/webhook.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "encoding/json" "flag" "fmt" @@ -18,7 +19,7 @@ import ( "github.com/codegangsta/negroni" "github.com/gorilla/mux" - "github.com/satori/go.uuid" + uuid "github.com/satori/go.uuid" fsnotify "gopkg.in/fsnotify.v1" ) @@ -39,6 +40,9 @@ var ( cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file") key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file") justDisplayVersion = flag.Bool("version", false, "display webhook version and quit") + justListCiphers = flag.Bool("list-cipher-suites", false, "list available TLS cipher suites") + tlsMinVersion = flag.String("tls-min-version", "1.2", "minimum TLS version (1.0, 1.1, 1.2, 1.3)") + tlsCipherSuites = flag.String("cipher-suites", "", "comma-separated list of supported TLS cipher suites") responseHeaders hook.ResponseHeaders hooksFiles hook.HooksFiles @@ -79,6 +83,14 @@ func main() { os.Exit(0) } + if *justListCiphers { + err := writeTLSSupportedCipherStrings(os.Stdout, getTLSMinVersion(*tlsMinVersion)) + if err != nil { + log.Fatal(err) + } + os.Exit(0) + } + if len(hooksFiles) == 0 { hooksFiles = append(hooksFiles, "hooks.json") } @@ -194,18 +206,28 @@ func main() { n.UseHandler(router) - if *secure { - log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL) - log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n)) - } else { + if !*secure { log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL) log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n)) } + svr := &http.Server{ + Addr: fmt.Sprintf("%s:%d", *ip, *port), + Handler: n, + TLSConfig: &tls.Config{ + CipherSuites: getTLSCipherSuites(*tlsCipherSuites), + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + MinVersion: getTLSMinVersion(*tlsMinVersion), + PreferServerCipherSuites: true, + }, + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0), // disable http/2 + } + + log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL) + log.Fatal(svr.ListenAndServeTLS(*cert, *key)) } func hookHandler(w http.ResponseWriter, r *http.Request) { - // generate a request id for logging rid := uuid.NewV4().String()[:6] @@ -246,7 +268,6 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { decoder.UseNumber() err := decoder.Decode(&payload) - if err != nil { log.Printf("[%s] error parsing JSON payload %+v\n", rid, err) }