caddyhttp: Add client cert SAN placeholders

This commit is contained in:
Matthew Holt 2020-06-11 16:19:07 -06:00
parent b3bff13f7d
commit d55c3b31eb
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
4 changed files with 87 additions and 5 deletions

View file

@ -72,9 +72,15 @@ func init() {
// `{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.public_key}` | The public key of the client certificate.
// `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key.
// `{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.tls.client.san.dns_names.*}` | SAN DNS names(index optional)
// `{http.request.tls.client.san.emails.*}` | SAN email addresses (index optional)
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
// `{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

@ -27,7 +27,7 @@ func init() {
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// basicauth [<matcher>] [<hash_algorithm>] {
// basicauth [<matcher>] [<hash_algorithm> [<realm>]] {
// <username> <hashed_password_base64> [<salt_base64>]
// ...
// }

View file

@ -28,6 +28,7 @@ import (
"net"
"net/http"
"net/textproto"
"net/url"
"path"
"strconv"
"strings"
@ -163,7 +164,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
idxStr := key[len(reqHostLabelsReplPrefix):]
idx, err := strconv.Atoi(idxStr)
if err != nil {
if err != nil || idx < 0 {
return "", false
}
reqHost, _, err := net.SplitHostPort(req.Host)
@ -171,9 +172,6 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
reqHost = req.Host // OK; assume there was no port
}
hostLabels := strings.Split(reqHost, ".")
if idx < 0 {
return "", false
}
if idx > len(hostLabels) {
return "", true
}
@ -245,6 +243,64 @@ func getReqTLSReplacement(req *http.Request, key string) (interface{}, bool) {
return nil, false
}
// subject alternate names (SANs)
if strings.HasPrefix(field, "client.san.") {
field = field[len("client.san."):]
var fieldName string
var fieldValue interface{}
switch {
case strings.HasPrefix(field, "dns_names"):
fieldName = "dns_names"
fieldValue = cert.DNSNames
case strings.HasPrefix(field, "emails"):
fieldName = "emails"
fieldValue = cert.EmailAddresses
case strings.HasPrefix(field, "ips"):
fieldName = "ips"
fieldValue = cert.IPAddresses
case strings.HasPrefix(field, "uris"):
fieldName = "uris"
fieldValue = cert.URIs
default:
return nil, false
}
field = field[len(fieldName):]
// if no index was specified, return the whole list
if field == "" {
return fieldValue, true
}
if len(field) < 2 || field[0] != '.' {
return nil, false
}
field = field[1:] // trim '.' between field name and index
// get the numeric index
idx, err := strconv.Atoi(field)
if err != nil || idx < 0 {
return nil, false
}
// access the indexed element and return it
switch v := fieldValue.(type) {
case []string:
if idx >= len(v) {
return nil, true
}
return v[idx], true
case []net.IP:
if idx >= len(v) {
return nil, true
}
return v[idx], true
case []*url.URL:
if idx >= len(v) {
return nil, true
}
return v[idx], true
}
}
switch field {
case "client.fingerprint":
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true

View file

@ -147,6 +147,26 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
input: "{http.request.tls.client.subject}",
expect: "CN=client.localdomain",
},
{
input: "{http.request.tls.client.san.dns_names}",
expect: "[localhost]",
},
{
input: "{http.request.tls.client.san.dns_names.0}",
expect: "localhost",
},
{
input: "{http.request.tls.client.san.dns_names.1}",
expect: "<empty>",
},
{
input: "{http.request.tls.client.san.ips}",
expect: "[127.0.0.1]",
},
{
input: "{http.request.tls.client.san.ips.0}",
expect: "127.0.0.1",
},
} {
actual := repl.ReplaceAll(tc.input, "<empty>")
if actual != tc.expect {