Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: Add replacer vars for mTLS connection details. #67

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions modules/l4tls/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,21 @@
package l4tls

import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"net"
"net/url"
"strconv"
"strings"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
Expand Down Expand Up @@ -96,6 +109,9 @@ func (t *Handler) Handle(cx *layer4.Connection, next layer4.Handler) error {
connectionState := tlsConn.ConnectionState()
appendConnectionState(cx, &connectionState)

repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer)
addTLSVarsToReplacer(repl, connectionState)

// all future reads/writes will now be decrypted/encrypted
// (tlsConn, which wraps cx, is wrapped into a new cx so
// that future I/O succeeds... if we use the same cx, it'd
Expand All @@ -104,6 +120,209 @@ func (t *Handler) Handle(cx *layer4.Connection, next layer4.Handler) error {
return next.Handle(cx.Wrap(tlsConn))
}

// FIXME: Should this perhaps be moved instead to caddytls?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's start by trying it here first, then it'll be easier to consider moving it upstream. 👍

func addTLSVarsToReplacer(repl *caddy.Replacer, cs tls.ConnectionState) {
cert := getTLSPeerCert(cs)
if cert == nil {
return
}
repl.Map(func(key string) (interface{}, bool) {
if !strings.HasPrefix(key, "l4.tls.") {
return "", false
}
field := strings.ToLower(key[len("l4.tls."):])
// 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
}
}
// Break-out the client's Subject
if strings.HasPrefix(field, "client.subject.") {
field = field[len("client.subject."):]
var fieldName string
var fieldValue []string
switch {
case field == "common_name":
// There can only be one.
return cert.Subject.CommonName, true
case strings.HasPrefix(field, "organizational_unit"):
fieldName = "organizational_unit"
fieldValue = cert.Subject.OrganizationalUnit
case strings.HasPrefix(field, "organization"):
fieldName = "organization"
fieldValue = cert.Subject.Organization
case strings.HasPrefix(field, "country"):
fieldName = "country"
fieldValue = cert.Subject.Country
case strings.HasPrefix(field, "locality"):
fieldName = "locality"
fieldValue = cert.Subject.Locality
case strings.HasPrefix(field, "province"):
fieldName = "province"
fieldValue = cert.Subject.Province
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
if idx >= len(fieldValue) {
return nil, true
}
return fieldValue[idx], true
}
// Break-out the issuer's Subject
if strings.HasPrefix(field, "client.issuer.") {
field = field[len("client.issuer."):]
var fieldName string
var fieldValue []string
switch {
case field == "common_name":
// There can only be one.
return cert.Issuer.CommonName, true
case strings.HasPrefix(field, "organizational_unit"):
fieldName = "organizational_unit"
fieldValue = cert.Issuer.OrganizationalUnit
case strings.HasPrefix(field, "organization"):
fieldName = "organization"
fieldValue = cert.Issuer.Organization
case strings.HasPrefix(field, "country"):
fieldName = "country"
fieldValue = cert.Issuer.Country
case strings.HasPrefix(field, "locality"):
fieldName = "locality"
fieldValue = cert.Issuer.Locality
case strings.HasPrefix(field, "province"):
fieldName = "province"
fieldValue = cert.Issuer.Province
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
if idx >= len(fieldValue) {
return nil, true
}
return fieldValue[idx], true
}
// Remaining client mTLS fields
switch field {
case "client.fingerprint":
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true
case "client.public_key", "client.public_key_sha256":
if cert.PublicKey == nil {
return nil, true
}
pubKeyBytes, err := marshalPublicKey(cert.PublicKey)
if err != nil {
return nil, true
}
if strings.HasSuffix(field, "_sha256") {
return fmt.Sprintf("%x", sha256.Sum256(pubKeyBytes)), true
}
return fmt.Sprintf("%x", pubKeyBytes), true
case "client.issuer":
return cert.Issuer, true
case "client.serial":
return cert.SerialNumber, true
case "client.subject":
return cert.Subject, true
case "client.common_name":
return cert.Subject.CommonName, true
case "client.certificate_pem":
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
return pem.EncodeToMemory(&block), true
case "client.certificate_der_base64":
return base64.StdEncoding.EncodeToString(cert.Raw), true
default:
return nil, false
}
})
}

func appendClientHello(cx *layer4.Connection, chi ClientHelloInfo) {
var clientHellos []ClientHelloInfo
if val := cx.GetVar("tls_client_hellos"); val != nil {
Expand Down Expand Up @@ -140,6 +359,28 @@ func GetConnectionStates(cx *layer4.Connection) []*tls.ConnectionState {
return connectionStates
}

// marshalPublicKey returns the byte encoding of pubKey.
func marshalPublicKey(pubKey interface{}) ([]byte, error) {
switch key := pubKey.(type) {
case *rsa.PublicKey:
return asn1.Marshal(key)
case *ecdsa.PublicKey:
return elliptic.Marshal(key.Curve, key.X, key.Y), nil
case ed25519.PublicKey:
return key, nil
}
return nil, fmt.Errorf("unrecognized public key type: %T", pubKey)
}

// 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]
}

// Interface guards
var (
_ caddy.Provisioner = (*Handler)(nil)
Expand Down
Loading