Skip to content
Closed
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
1 change: 1 addition & 0 deletions caddyconfig/httpcaddyfile/shorthands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
}
Expand Down
1 change: 1 addition & 0 deletions modules/caddyhttp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion modules/caddyhttp/replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
43 changes: 41 additions & 2 deletions modules/caddyhttp/replacer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand All @@ -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,
}
Expand Down Expand Up @@ -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)
Expand Down