* feat: add support for base64 encoded client certificate chain

This commit is contained in:
S K 2025-03-21 19:30:44 -07:00
parent 173573035c
commit 2f0a14ee77
4 changed files with 51 additions and 3 deletions

View File

@ -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}",
}

View File

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

View File

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

View File

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