Expose TLS placeholders (#2982)

* caddytls: Add CipherSuiteName and ProtocolName functions

The cipher_suites.go file is derived from a commit to the Go master
branch that's slated for Go 1.14.  Once Go 1.14 is released, this file
can be removed.

* caddyhttp: Use commonLogEmptyValue in common_log replacer

* caddyhttp: Add TLS placeholders

* caddytls: update unsupportedProtocols

Don't export unsupportedProtocols and update its godoc to mention that
it's used for logging only.

* caddyhttp: simplify getRegTLSReplacement signature

getRegTLSReplacement should receive a string instead of a pointer.

* caddyhttp: Remove http.request.tls.client.cert replacer

The previous behavior of printing the raw certificate bytes was ported
from Caddy 1, but the usefulness of that approach is suspect.  Remove
the client cert replacer from v2 until a use case is presented.

* caddyhttp: Use tls.CipherSuiteName from Go 1.14

Remove ported version of CipherSuiteName in the process.
This commit is contained in:
Cameron Moore 2020-02-26 02:22:50 +00:00 committed by GitHub
parent 45b171ff3a
commit b0a491aec8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 214 additions and 17 deletions

View file

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

View file

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

View file

@ -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."
reqCookieReplPrefix = "http.request.cookie."
reqHeaderReplPrefix = "http.request.header."
cookieReplPrefix = "http.request.cookie."
hostLabelReplPrefix = "http.request.host.labels."
pathPartsReplPrefix = "http.request.uri.path."
varsReplPrefix = "http.vars."
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."
)

View file

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

View file

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

View file

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