diff options
-rw-r--r-- | caddyconfig/httpcaddyfile/httptype.go | 7 | ||||
-rw-r--r-- | modules/caddyhttp/caddyhttp.go | 10 | ||||
-rw-r--r-- | modules/caddyhttp/replacer.go | 104 | ||||
-rw-r--r-- | modules/caddyhttp/replacer_test.go | 80 | ||||
-rw-r--r-- | modules/caddyhttp/server.go | 2 | ||||
-rw-r--r-- | modules/caddytls/values.go | 28 |
6 files changed, 214 insertions, 17 deletions
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 03234b3..d4c2bb6 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -88,6 +88,13 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, "{remote}", "{http.request.remote}", "{scheme}", "{http.request.scheme}", "{uri}", "{http.request.uri}", + + "{tls_cipher}", "{http.request.tls.cipher_suite}", + "{tls_version}", "{http.request.tls.version}", + "{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}", + "{tls_client_issuer}", "{http.request.tls.client.issuer}", + "{tls_client_serial}", "{http.request.tls.client.serial}", + "{tls_client_subject}", "{http.request.tls.client.subject}", ) for _, segment := range sb.block.Segments { for i := 0; i < len(segment); i++ { diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 30c2f79..135afef 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -71,6 +71,16 @@ func init() { // `{http.request.remote.port}` | The port part of the remote client's address // `{http.request.remote}` | The address of the remote client // `{http.request.scheme}` | The request scheme +// `{http.request.tls.version}` | The TLS version name +// `{http.request.tls.cipher_suite}` | The TLS cipher suite +// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection +// `{http.request.tls.proto}` | The negotiated next protocol +// `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server +// `{http.request.tls.server_name}` | The server name requested by the client, if any +// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate +// `{http.request.tls.client.issuer}` | The issuer DN of the client certificate +// `{http.request.tls.client.serial}` | The serial number of the client certificate +// `{http.request.tls.client.subject}` | The subject DN of the client certificate // `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left) // `{http.request.uri.path.dir}` | The directory, excluding leaf filename // `{http.request.uri.path.file}` | The filename of the path, excluding directory diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index cea820d..c9c7522 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -15,6 +15,9 @@ package caddyhttp import ( + "crypto/sha256" + "crypto/tls" + "crypto/x509" "fmt" "net" "net/http" @@ -24,14 +27,15 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddytls" ) func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) { httpVars := func(key string) (string, bool) { if req != nil { // query string parameters - if strings.HasPrefix(key, queryReplPrefix) { - vals := req.URL.Query()[key[len(queryReplPrefix):]] + if strings.HasPrefix(key, reqURIQueryReplPrefix) { + vals := req.URL.Query()[key[len(reqURIQueryReplPrefix):]] // always return true, since the query param might // be present only in some requests return strings.Join(vals, ","), true @@ -47,8 +51,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } // cookies - if strings.HasPrefix(key, cookieReplPrefix) { - name := key[len(cookieReplPrefix):] + if strings.HasPrefix(key, reqCookieReplPrefix) { + name := key[len(reqCookieReplPrefix):] for _, cookie := range req.Cookies() { if strings.EqualFold(name, cookie.Name) { // always return true, since the cookie might @@ -58,6 +62,11 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } } + // http.request.tls. + if strings.HasPrefix(key, reqTLSReplPrefix) { + return getReqTLSReplacement(req, key) + } + switch key { case "http.request.method": return req.Method, true @@ -129,8 +138,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } // hostname labels - if strings.HasPrefix(key, hostLabelReplPrefix) { - idxStr := key[len(hostLabelReplPrefix):] + if strings.HasPrefix(key, reqHostLabelsReplPrefix) { + idxStr := key[len(reqHostLabelsReplPrefix):] idx, err := strconv.Atoi(idxStr) if err != nil { return "", false @@ -150,8 +159,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } // path parts - if strings.HasPrefix(key, pathPartsReplPrefix) { - idxStr := key[len(pathPartsReplPrefix):] + if strings.HasPrefix(key, reqURIPathReplPrefix) { + idxStr := key[len(reqURIPathReplPrefix):] idx, err := strconv.Atoi(idxStr) if err != nil { return "", false @@ -208,12 +217,77 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo repl.Map(httpVars) } +func getReqTLSReplacement(req *http.Request, key string) (string, bool) { + if req == nil || req.TLS == nil { + return "", false + } + + if len(key) < len(reqTLSReplPrefix) { + return "", false + } + + field := strings.ToLower(key[len(reqTLSReplPrefix):]) + + if strings.HasPrefix(field, "client.") { + cert := getTLSPeerCert(req.TLS) + if cert == nil { + return "", false + } + + switch field { + case "client.fingerprint": + return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true + case "client.issuer": + return cert.Issuer.String(), true + case "client.serial": + return fmt.Sprintf("%x", cert.SerialNumber), true + case "client.subject": + return cert.Subject.String(), true + default: + return "", false + } + } + + switch field { + case "version": + return caddytls.ProtocolName(req.TLS.Version), true + case "cipher_suite": + return tls.CipherSuiteName(req.TLS.CipherSuite), true + case "resumed": + if req.TLS.DidResume { + return "true", true + } + return "false", true + case "proto": + return req.TLS.NegotiatedProtocol, true + case "proto_mutual": + if req.TLS.NegotiatedProtocolIsMutual { + return "true", true + } + return "false", true + case "server_name": + return req.TLS.ServerName, true + default: + return "", false + } +} + +// getTLSPeerCert retrieves the first peer certificate from a TLS session. +// Returns nil if no peer cert is in use. +func getTLSPeerCert(cs *tls.ConnectionState) *x509.Certificate { + if len(cs.PeerCertificates) == 0 { + return nil + } + return cs.PeerCertificates[0] +} + const ( - queryReplPrefix = "http.request.uri.query." - reqHeaderReplPrefix = "http.request.header." - cookieReplPrefix = "http.request.cookie." - hostLabelReplPrefix = "http.request.host.labels." - pathPartsReplPrefix = "http.request.uri.path." - varsReplPrefix = "http.vars." - respHeaderReplPrefix = "http.response.header." + reqCookieReplPrefix = "http.request.cookie." + reqHeaderReplPrefix = "http.request.header." + reqHostLabelsReplPrefix = "http.request.host.labels." + reqTLSReplPrefix = "http.request.tls." + reqURIPathReplPrefix = "http.request.uri.path." + reqURIQueryReplPrefix = "http.request.uri.query." + respHeaderReplPrefix = "http.response.header." + varsReplPrefix = "http.vars." ) diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index b355c7f..ea9fa65 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -16,6 +16,9 @@ package caddyhttp import ( "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" "net/http" "net/http/httptest" "testing" @@ -30,6 +33,41 @@ func TestHTTPVarReplacement(t *testing.T) { req = req.WithContext(ctx) req.Host = "example.com:80" req.RemoteAddr = "localhost:1234" + + clientCert := []byte(`-----BEGIN CERTIFICATE----- +MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk +eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG +A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB +iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF +z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+ +fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ +BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A +AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+ +eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV +3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH +9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g= +-----END CERTIFICATE-----`) + + block, _ := pem.Decode(clientCert) + if block == nil { + t.Fatalf("failed to decode PEM certificate") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to decode PEM certificate: %v", err) + } + + req.TLS = &tls.ConnectionState{ + Version: tls.VersionTLS13, + HandshakeComplete: true, + ServerName: "foo.com", + CipherSuite: tls.TLS_AES_256_GCM_SHA384, + PeerCertificates: []*x509.Certificate{cert}, + NegotiatedProtocol: "h2", + NegotiatedProtocolIsMutual: true, + } + res := httptest.NewRecorder() addHTTPVarsToReplacer(repl, req, res) @@ -39,7 +77,7 @@ func TestHTTPVarReplacement(t *testing.T) { }{ { input: "{http.request.scheme}", - expect: "http", + expect: "https", }, { input: "{http.request.host}", @@ -69,6 +107,46 @@ func TestHTTPVarReplacement(t *testing.T) { input: "{http.request.host.labels.1}", expect: "example", }, + { + input: "{http.request.tls.cipher_suite}", + expect: "TLS_AES_256_GCM_SHA384", + }, + { + input: "{http.request.tls.proto}", + expect: "h2", + }, + { + input: "{http.request.tls.proto_mutual}", + expect: "true", + }, + { + input: "{http.request.tls.resumed}", + expect: "false", + }, + { + input: "{http.request.tls.server_name}", + expect: "foo.com", + }, + { + input: "{http.request.tls.version}", + expect: "tls1.3", + }, + { + input: "{http.request.tls.client.fingerprint}", + expect: "9f57b7b497cceacc5459b76ac1c3afedbc12b300e728071f55f84168ff0f7702", + }, + { + input: "{http.request.tls.client.issuer}", + expect: "CN=Caddy Test CA", + }, + { + input: "{http.request.tls.client.serial}", + expect: "2", + }, + { + input: "{http.request.tls.client.subject}", + expect: "CN=client.localdomain", + }, } { actual := repl.ReplaceAll(tc.input, "<empty>") if actual != tc.expect { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 124331d..580449b 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -173,7 +173,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } log("handled request", - zap.String("common_log", repl.ReplaceAll(commonLogFormat, "-")), + zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)), zap.Duration("latency", latency), zap.Int("size", wrec.Size()), zap.Int("status", wrec.Status()), diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 21a6b33..d231650 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -17,6 +17,7 @@ package caddytls import ( "crypto/tls" "crypto/x509" + "fmt" "github.com/go-acme/lego/v3/certcrypto" "github.com/klauspost/cpuid" @@ -127,9 +128,36 @@ var SupportedProtocols = map[string]uint16{ "tls1.3": tls.VersionTLS13, } +// unsupportedProtocols is a map of unsupported protocols. +// Used for logging only, not enforcement. +var unsupportedProtocols = map[string]uint16{ + "ssl3.0": tls.VersionSSL30, + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, +} + // publicKeyAlgorithms is the map of supported public key algorithms. var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{ "rsa": x509.RSA, "dsa": x509.DSA, "ecdsa": x509.ECDSA, } + +// ProtocolName returns the standard name for the passed protocol version ID +// (e.g. "TLS1.3") or a fallback representation of the ID value if the version +// is not supported. +func ProtocolName(id uint16) string { + for k, v := range SupportedProtocols { + if v == id { + return k + } + } + + for k, v := range unsupportedProtocols { + if v == id { + return k + } + } + + return fmt.Sprintf("0x%04x", id) +} |