From 2f0a14ee77baf627037d66184705a9cf5b4beeec Mon Sep 17 00:00:00 2001 From: S K Date: Fri, 21 Mar 2025 19:30:44 -0700 Subject: [PATCH] * feat: add support for base64 encoded client certificate chain --- caddyconfig/httpcaddyfile/shorthands.go | 1 + modules/caddyhttp/app.go | 1 + modules/caddyhttp/replacer.go | 9 +++++- modules/caddyhttp/replacer_test.go | 43 +++++++++++++++++++++++-- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/caddyconfig/httpcaddyfile/shorthands.go b/caddyconfig/httpcaddyfile/shorthands.go index ca6e4f92c..8b0acfae9 100644 --- a/caddyconfig/httpcaddyfile/shorthands.go +++ b/caddyconfig/httpcaddyfile/shorthands.go @@ -82,6 +82,7 @@ func placeholderShorthands() []string { "{tls_client_subject}", "{http.request.tls.client.subject}", "{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}", "{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}", + "{tls_client_certificate_chain_der_base64}", "{http.request.tls.client.certificate_chain_der_base64}", "{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}", "{client_ip}", "{http.vars.client_ip}", } diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index cbd168d31..883e5660f 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -88,6 +88,7 @@ func init() { // `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key. // `{http.request.tls.client.certificate_pem}` | The PEM-encoded value of the certificate. // `{http.request.tls.client.certificate_der_base64}` | The base64-encoded value of the certificate. +// `{http.request.tls.client.certificate_chain_der_base64}` | The base64-encoded value of certificate_der_base64 value of all certificates, joined by newline characters. // `{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 diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 776aa6294..4203761e9 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -397,7 +397,8 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) { field := strings.ToLower(key[len(reqTLSReplPrefix):]) if strings.HasPrefix(field, "client.") { - cert := getTLSPeerCert(req.TLS) + tlsConnectionState := req.TLS + cert := getTLSPeerCert(tlsConnectionState) if cert == nil { return nil, false } @@ -486,6 +487,12 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) { return pem.EncodeToMemory(&block), true case "client.certificate_der_base64": return base64.StdEncoding.EncodeToString(cert.Raw), true + case "client.certificate_chain_der_base64": + var chain []string + for _, cert := range tlsConnectionState.PeerCertificates { + chain = append(chain, base64.StdEncoding.EncodeToString(cert.Raw)) + } + return base64.StdEncoding.EncodeToString([]byte(strings.Join(chain, "\n"))), true default: return nil, false } diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index 50a2e8c62..4df2610ff 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -18,10 +18,12 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/pem" "net" "net/http" "net/http/httptest" + "strings" "testing" "github.com/caddyserver/caddy/v2" @@ -51,6 +53,25 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV 9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g= -----END CERTIFICATE-----`) + clientCert2 := []byte(`-----BEGIN CERTIFICATE----- +MIIChTCCAe4CCQCyNNPmOyATOjANBgkqhkiG9w0BAQsFADCBhjELMAkGA1UEBhMC +WFgxEjAQBgNVBAgMCVN0YXRlTmFtZTERMA8GA1UEBwwIQ2l0eU5hbWUxFDASBgNV +BAoMC0NvbXBhbnlOYW1lMRswGQYDVQQLDBJDb21wYW55U2VjdGlvbk5hbWUxHTAb +BgNVBAMMFENvbW1vbk5hbWVPckhvc3RuYW1lMB4XDTI1MDMyMTAxMDEzM1oXDTM1 +MDMxOTAxMDEzM1owgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUx +ETAPBgNVBAcMCENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UE +CwwSQ29tcGFueVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0 +bmFtZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxPqTvtkDvNoqtWnRYbq5 +Itpa7/XK5oRfjva4beCYh1DRiprCOsdUgso9mug6Uq9Dt+kDxIA88B5my2gMfiLc +BLIC0SaG/wVayGN9uCL+kr751BfQEioBjmtn/d+VoSTjygm54CV948Lu6MeJ0cLc +r1PTvwpPt7zqYkD5nZ+hzzcCAwEAATANBgkqhkiG9w0BAQsFAAOBgQAmuFJhJgiI +PPNJ3ryb15Hnlz1TtLYcgoxnGI8u7lNX/P5HMjiVhv53ccYIvI9OUDLkQchuGCpy +MxV7+5zO8oWJzerFqu2pXjXeJf+28NpfVVd7l8R8Y2LzQYnDcqm1wNsj4CloEW01 +OoL+ttSPjADNgrxLWOAvjD4UZQ6zKgkpQw== +-----END CERTIFICATE-----`) + + pemToBase64DerReplacer := strings.NewReplacer("-----BEGIN CERTIFICATE-----", "", "-----END CERTIFICATE-----", "", "\n", "") + block, _ := pem.Decode(clientCert) if block == nil { t.Fatalf("failed to decode PEM certificate") @@ -61,12 +82,22 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV t.Fatalf("failed to decode PEM certificate: %v", err) } + block, _ = pem.Decode(clientCert2) + if block == nil { + t.Fatalf("failed to decode PEM certificate") + } + + cert2, 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: "example.com", CipherSuite: tls.TLS_AES_256_GCM_SHA384, - PeerCertificates: []*x509.Certificate{cert}, + PeerCertificates: []*x509.Certificate{cert, cert2}, NegotiatedProtocol: "h2", NegotiatedProtocolIsMutual: true, } @@ -217,7 +248,15 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV }, { get: "http.request.tls.client.certificate_pem", - expect: string(clientCert) + "\n", // returned value comes with a newline appended to it + expect: string(clientCert) + "\n", + }, + { + get: "http.request.tls.client.certificate_der_base64", + expect: pemToBase64DerReplacer.Replace(string(clientCert)), + }, + { + get: "http.request.tls.client.certificate_chain_der_base64", + expect: base64.StdEncoding.EncodeToString([]byte(pemToBase64DerReplacer.Replace(string(clientCert)) + "\n" + pemToBase64DerReplacer.Replace(string(clientCert2)))), }, } { actual, got := repl.GetString(tc.get)