mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-15 20:46:32 +01:00
Implement placeholder support for server and client certificates, including mod_ssl-like behavior
This commit is contained in:
parent
0e570e0cc7
commit
11fd2dcb2b
2 changed files with 147 additions and 99 deletions
59
modules/caddyhttp/package main.go
Normal file
59
modules/caddyhttp/package main.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func populateEnvFromClientCert(req *http.Request) {
|
||||||
|
if req.TLS == nil || len(req.TLS.VerifiedChains) == 0 {
|
||||||
|
fmt.Println("No client certificate provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the client certificate
|
||||||
|
clientCert := req.TLS.VerifiedChains[0][0]
|
||||||
|
|
||||||
|
// Populate environment variables
|
||||||
|
os.Setenv("SSL_CLIENT_CERT", string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: clientCert.Raw,
|
||||||
|
})))
|
||||||
|
os.Setenv("SSL_CLIENT_SUBJECT", clientCert.Subject.String())
|
||||||
|
os.Setenv("SSL_CLIENT_ISSUER", clientCert.Issuer.String())
|
||||||
|
os.Setenv("SSL_CLIENT_SERIAL", clientCert.SerialNumber.String())
|
||||||
|
|
||||||
|
// SANs (Subject Alternative Names)
|
||||||
|
for i, dns := range clientCert.DNSNames {
|
||||||
|
os.Setenv("SSL_CLIENT_SAN_DNS_"+strconv.Itoa(i), dns)
|
||||||
|
}
|
||||||
|
for i, email := range clientCert.EmailAddresses {
|
||||||
|
os.Setenv("SSL_CLIENT_SAN_EMAIL_"+strconv.Itoa(i), email)
|
||||||
|
}
|
||||||
|
for i, ip := range clientCert.IPAddresses {
|
||||||
|
os.Setenv("SSL_CLIENT_SAN_IP_"+strconv.Itoa(i), ip.String())
|
||||||
|
}
|
||||||
|
for i, uri := range clientCert.URIs {
|
||||||
|
os.Setenv("SSL_CLIENT_SAN_URI_"+strconv.Itoa(i), uri.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Fingerprint
|
||||||
|
fingerprint := sha256.Sum256(clientCert.Raw)
|
||||||
|
os.Setenv("SSL_CLIENT_FINGERPRINT", fmt.Sprintf("%x", fingerprint))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Populate environment variables
|
||||||
|
populateEnvFromClientCert(r)
|
||||||
|
|
||||||
|
// Respond with the environment variables for testing
|
||||||
|
fmt.Fprintf(w, "SSL_CLIENT_CERT: %s\n", os.Getenv("SSL_CLIENT_CERT"))
|
||||||
|
fmt.Fprintf(w, "SSL_CLIENT_SUBJECT: %s\n", os.Getenv("SSL_CLIENT_SUBJECT"))
|
||||||
|
fmt.Fprintf(w, "SSL_CLIENT_ISSUER: %s\n", os.Getenv("SSL_CLIENT_ISSUER"))
|
||||||
|
fmt.Fprintf(w, "SSL_CLIENT_SERIAL: %s\n", os.Getenv("SSL_CLIENT_SERIAL"))
|
||||||
|
fmt.Fprintf(w, "SSL_CLIENT_FINGERPRINT: %s\n", os.Getenv("SSL_CLIENT_FINGERPRINT"))
|
||||||
|
}
|
|
@ -384,6 +384,64 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
|
|
||||||
repl.Map(httpVars)
|
repl.Map(httpVars)
|
||||||
}
|
}
|
||||||
|
func handleMTLSEnabledWithExport(
|
||||||
|
req *http.Request,
|
||||||
|
connState *tls.ConnectionState,
|
||||||
|
exportCertData bool,
|
||||||
|
) map[string]string {
|
||||||
|
certDetails := make(map[string]string)
|
||||||
|
|
||||||
|
// Get the Caddy replacer from the request context
|
||||||
|
repl, _ := req.Context().Value(ReplacerCtxKey).(*caddy.Replacer)
|
||||||
|
|
||||||
|
// Helper function to set placeholders or map values
|
||||||
|
setData := func(key, value string) {
|
||||||
|
if exportCertData && repl != nil {
|
||||||
|
repl.Set(key, value)
|
||||||
|
} else {
|
||||||
|
certDetails[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server certificate details
|
||||||
|
if connState != nil && len(connState.PeerCertificates) > 0 {
|
||||||
|
serverCert := connState.PeerCertificates[0]
|
||||||
|
setData("SSL_SERVER_CERT", string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: serverCert.Raw,
|
||||||
|
})))
|
||||||
|
setData("SSL_SERVER_SUBJECT", serverCert.Subject.String())
|
||||||
|
setData("SSL_SERVER_ISSUER", serverCert.Issuer.String())
|
||||||
|
} else {
|
||||||
|
setData("SSL_SERVER_CERT", "No Server Certificate Found")
|
||||||
|
setData("SSL_SERVER_SUBJECT", "No Server Certificate Subject")
|
||||||
|
setData("SSL_SERVER_ISSUER", "No Server Certificate Issuer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client certificate details
|
||||||
|
if connState != nil && len(connState.VerifiedChains) > 0 {
|
||||||
|
clientCert := connState.VerifiedChains[0][0]
|
||||||
|
setData("SSL_CLIENT_CERT", string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: clientCert.Raw,
|
||||||
|
})))
|
||||||
|
setData("SSL_CLIENT_SUBJECT", clientCert.Subject.String())
|
||||||
|
setData("SSL_CLIENT_ISSUER", clientCert.Issuer.String())
|
||||||
|
} else {
|
||||||
|
// Fallback: Set SSL_CLIENT_CERT to "null" or empty when no client certificate is provided
|
||||||
|
if exportCertData && repl != nil {
|
||||||
|
repl.Set("SSL_CLIENT_CERT", "") // Empty string represents "null"
|
||||||
|
repl.Set("SSL_CLIENT_SUBJECT", "No Client Certificate Subject")
|
||||||
|
repl.Set("SSL_CLIENT_ISSUER", "No Client Certificate Issuer")
|
||||||
|
} else {
|
||||||
|
certDetails["SSL_CLIENT_CERT"] = "" // Empty string represents "null"
|
||||||
|
certDetails["SSL_CLIENT_SUBJECT"] = "No Client Certificate Subject"
|
||||||
|
certDetails["SSL_CLIENT_ISSUER"] = "No Client Certificate Issuer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certDetails
|
||||||
|
}
|
||||||
|
|
||||||
func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
|
func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
|
||||||
if req == nil || req.TLS == nil {
|
if req == nil || req.TLS == nil {
|
||||||
|
@ -396,98 +454,29 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
|
||||||
|
|
||||||
field := strings.ToLower(key[len(reqTLSReplPrefix):])
|
field := strings.ToLower(key[len(reqTLSReplPrefix):])
|
||||||
|
|
||||||
|
// Process client and server placeholders
|
||||||
|
if strings.HasPrefix(field, "client.") || strings.HasPrefix(field, "server.") {
|
||||||
|
handleMTLSEnabledWithExport(req, req.TLS, true)
|
||||||
|
|
||||||
|
// Continue processing for specific fields
|
||||||
if strings.HasPrefix(field, "client.") {
|
if strings.HasPrefix(field, "client.") {
|
||||||
cert := getTLSPeerCert(req.TLS)
|
cert := getTLSPeerCert(req.TLS)
|
||||||
if cert == nil {
|
if cert == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// subject alternate names (SANs)
|
|
||||||
if strings.HasPrefix(field, "client.san.") {
|
|
||||||
field = field[len("client.san."):]
|
|
||||||
var fieldName string
|
|
||||||
var fieldValue any
|
|
||||||
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 {
|
switch field {
|
||||||
case "client.fingerprint":
|
case "client.fingerprint":
|
||||||
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true
|
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":
|
case "client.subject":
|
||||||
return cert.Subject, true
|
return cert.Subject, true
|
||||||
|
case "client.issuer":
|
||||||
|
return cert.Issuer, true
|
||||||
case "client.certificate_pem":
|
case "client.certificate_pem":
|
||||||
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
||||||
return pem.EncodeToMemory(&block), true
|
return pem.EncodeToMemory(&block), true
|
||||||
case "client.certificate_der_base64":
|
case "client.certificate_der_base64":
|
||||||
return base64.StdEncoding.EncodeToString(cert.Raw), true
|
return base64.StdEncoding.EncodeToString(cert.Raw), true
|
||||||
default:
|
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue