summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--admin.go59
-rw-r--r--caddyconfig/httpcaddyfile/httptype.go2
-rw-r--r--caddyconfig/json5/json5.go43
-rw-r--r--caddyconfig/jsonc/jsonc.go49
-rw-r--r--cmd/main.go11
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--modules/caddyhttp/caddyhttp.go30
-rw-r--r--modules/caddyhttp/fileserver/caddyfile.go2
-rw-r--r--modules/caddyhttp/fileserver/staticfiles.go44
-rw-r--r--modules/caddyhttp/replacer.go6
-rw-r--r--modules/caddyhttp/server.go28
-rw-r--r--modules/caddytls/connpolicy.go139
13 files changed, 364 insertions, 55 deletions
diff --git a/admin.go b/admin.go
index c87ef95..b2894be 100644
--- a/admin.go
+++ b/admin.go
@@ -22,6 +22,7 @@ import (
"io"
"io/ioutil"
"log"
+ "mime"
"net"
"net/http"
"net/http/pprof"
@@ -170,38 +171,44 @@ func handleLoadConfig(w http.ResponseWriter, r *http.Request) {
// if the config is formatted other than Caddy's native
// JSON, we need to adapt it before loading it
- ct := r.Header.Get("Content-Type")
- if !strings.Contains(ct, "/json") {
- slashIdx := strings.Index(ct, "/")
- if slashIdx < 0 {
- http.Error(w, "Malformed Content-Type", http.StatusBadRequest)
- return
- }
- adapterName := ct[slashIdx+1:]
- cfgAdapter := caddyconfig.GetAdapter(adapterName)
- if cfgAdapter == nil {
- http.Error(w, "Unrecognized config adapter: "+adapterName, http.StatusBadRequest)
- return
- }
- body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, 1024*1024))
- if err != nil {
- http.Error(w, "Error reading request body: "+err.Error(), http.StatusBadRequest)
- return
- }
- result, warnings, err := cfgAdapter.Adapt(body, nil)
+ if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
+ ct, _, err := mime.ParseMediaType(ctHeader)
if err != nil {
- log.Printf("[ADMIN][ERROR] adapting config from %s: %v", adapterName, err)
- http.Error(w, fmt.Sprintf("Adapting config from %s: %v", adapterName, err), http.StatusBadRequest)
+ http.Error(w, "Invalid Content-Type: "+err.Error(), http.StatusBadRequest)
return
}
- if len(warnings) > 0 {
- respBody, err := json.Marshal(warnings)
+ if !strings.HasSuffix(ct, "/json") {
+ slashIdx := strings.Index(ct, "/")
+ if slashIdx < 0 {
+ http.Error(w, "Malformed Content-Type", http.StatusBadRequest)
+ return
+ }
+ adapterName := ct[slashIdx+1:]
+ cfgAdapter := caddyconfig.GetAdapter(adapterName)
+ if cfgAdapter == nil {
+ http.Error(w, "Unrecognized config adapter: "+adapterName, http.StatusBadRequest)
+ return
+ }
+ body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, 1024*1024))
if err != nil {
- log.Printf("[ADMIN][ERROR] marshaling warnings: %v", err)
+ http.Error(w, "Error reading request body: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ result, warnings, err := cfgAdapter.Adapt(body, nil)
+ if err != nil {
+ log.Printf("[ADMIN][ERROR] adapting config from %s: %v", adapterName, err)
+ http.Error(w, fmt.Sprintf("Adapting config from %s: %v", adapterName, err), http.StatusBadRequest)
+ return
+ }
+ if len(warnings) > 0 {
+ respBody, err := json.Marshal(warnings)
+ if err != nil {
+ log.Printf("[ADMIN][ERROR] marshaling warnings: %v", err)
+ }
+ w.Write(respBody)
}
- w.Write(respBody)
+ payload = bytes.NewReader(result)
}
- payload = bytes.NewReader(result)
}
err := Load(payload)
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index ecf6e94..d183c7c 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -93,7 +93,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
"{scheme}", "{http.request.scheme}",
"{file}", "{http.request.uri.path.file}",
"{dir}", "{http.request.uri.path.dir}",
- "{query}", "{http.request.uri.query}",
+ "{query}", "{http.request.uri.query_string}",
)
for _, segment := range sb.block.Segments {
for i := 0; i < len(segment); i++ {
diff --git a/caddyconfig/json5/json5.go b/caddyconfig/json5/json5.go
new file mode 100644
index 0000000..2c86301
--- /dev/null
+++ b/caddyconfig/json5/json5.go
@@ -0,0 +1,43 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package json5adapter
+
+import (
+ "encoding/json"
+
+ "github.com/caddyserver/caddy/v2/caddyconfig"
+ "github.com/ilibs/json5"
+)
+
+func init() {
+ caddyconfig.RegisterAdapter("json5", Adapter{})
+}
+
+// Adapter adapts JSON5 to Caddy JSON.
+type Adapter struct{}
+
+// Adapt converts the JSON5 config in body to Caddy JSON.
+func (a Adapter) Adapt(body []byte, options map[string]interface{}) (result []byte, warnings []caddyconfig.Warning, err error) {
+ var decoded interface{}
+ err = json5.Unmarshal(body, &decoded)
+ if err != nil {
+ return
+ }
+ result, err = json.Marshal(decoded)
+ return
+}
+
+// Interface guard
+var _ caddyconfig.Adapter = (*Adapter)(nil)
diff --git a/caddyconfig/jsonc/jsonc.go b/caddyconfig/jsonc/jsonc.go
new file mode 100644
index 0000000..4f72c05
--- /dev/null
+++ b/caddyconfig/jsonc/jsonc.go
@@ -0,0 +1,49 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jsoncadapter
+
+import (
+ "encoding/json"
+
+ "github.com/caddyserver/caddy/v2/caddyconfig"
+ "github.com/muhammadmuzzammil1998/jsonc"
+)
+
+func init() {
+ caddyconfig.RegisterAdapter("jsonc", Adapter{})
+}
+
+// Adapter adapts JSON-C to Caddy JSON.
+type Adapter struct{}
+
+// Adapt converts the JSON-C config in body to Caddy JSON.
+func (a Adapter) Adapt(body []byte, options map[string]interface{}) (result []byte, warnings []caddyconfig.Warning, err error) {
+ result = jsonc.ToJSON(body)
+
+ // any errors in the JSON will be
+ // reported during config load, but
+ // we can at least warn here that
+ // it is not valid JSON
+ if !json.Valid(result) {
+ warnings = append(warnings, caddyconfig.Warning{
+ Message: "Resulting JSON is invalid.",
+ })
+ }
+
+ return
+}
+
+// Interface guard
+var _ caddyconfig.Adapter = (*Adapter)(nil)
diff --git a/cmd/main.go b/cmd/main.go
index e2f1233..3b44a85 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -114,10 +114,17 @@ func loadConfig(configFile, adapterName string) ([]byte, error) {
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
if cfgAdapter != nil {
config, err = ioutil.ReadFile("Caddyfile")
- if err != nil && !os.IsNotExist(err) {
+ if os.IsNotExist(err) {
+ // okay, no default Caddyfile; pretend like this never happened
+ cfgAdapter = nil
+ err = nil
+ } else if err != nil {
+ // default Caddyfile exists, but error reading it
return nil, fmt.Errorf("reading default Caddyfile: %v", err)
+ } else {
+ // success reading default Caddyfile
+ configFile = "Caddyfile"
}
- configFile = "Caddyfile"
}
}
diff --git a/go.mod b/go.mod
index 315dcfd..2f0222c 100644
--- a/go.mod
+++ b/go.mod
@@ -13,11 +13,13 @@ require (
github.com/google/go-cmp v0.3.1 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/huandu/xstrings v1.2.0 // indirect
+ github.com/ilibs/json5 v1.0.1
github.com/imdario/mergo v0.3.7 // indirect
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b
github.com/klauspost/cpuid v1.2.1
github.com/mholt/certmagic v0.6.2
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
+ github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190902132743-e4903c4dea48
github.com/rs/cors v1.6.0
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 8771eb1..4c691a6 100644
--- a/go.sum
+++ b/go.sum
@@ -25,6 +25,8 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
+github.com/ilibs/json5 v1.0.1 h1:3e14wUQM8PyK6Hf1bM+zAQFxfG+N5oZj35x5vCNeQ58=
+github.com/ilibs/json5 v1.0.1/go.mod h1:kXsGuzHMPuZZTN15l0IQzy5PR8DrDhPB24tFgwpdKME=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b h1:LHpBANNM/cw1PAXJtKV9dgfp6ztOKfdGXcltGmqU9aE=
@@ -38,6 +40,8 @@ github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM=
github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
+github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190902132743-e4903c4dea48 h1:BM/fjd7MfvZuyoHXLv3YlWNIuNb47PLp6EyFBL1KIMg=
+github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190902132743-e4903c4dea48/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index 174e316..9dfdf36 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -75,6 +75,15 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.AutoHTTPS = new(AutoHTTPSConfig)
}
+ // disallow TLS client auth bypass which could
+ // otherwise be exploited by sending an unprotected
+ // SNI value during TLS handshake, then a protected
+ // Host header during HTTP request later on that
+ // connection
+ if srv.hasTLSClientAuth() {
+ srv.StrictSNIHost = true
+ }
+
// TODO: Test this function to ensure these replacements are performed
for i := range srv.Listen {
srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
@@ -159,8 +168,7 @@ func (app *App) Start() error {
return fmt.Errorf("%s: listening on %s: %v", network, addr, err)
}
- // enable HTTP/2 (and support for solving the
- // TLS-ALPN ACME challenge) by default
+ // enable HTTP/2 by default
for _, pol := range srv.TLSConnPolicies {
if len(pol.ALPN) == 0 {
pol.ALPN = append(pol.ALPN, defaultALPN...)
@@ -226,6 +234,8 @@ func (app *App) automaticHTTPS() error {
// skip if all listeners use the HTTP port
if !srv.listenersUseAnyPortOtherThan(app.HTTPPort) {
+ log.Printf("[INFO] Server %v is only listening on the HTTP port %d, so no automatic HTTPS will be applied to this server",
+ srv.Listen, app.HTTPPort)
continue
}
@@ -294,11 +304,11 @@ func (app *App) automaticHTTPS() error {
return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
}
- // tell the server to use TLS by specifying a TLS
- // connection policy (which supports HTTP/2 and the
- // TLS-ALPN ACME challenge as well)
- srv.TLSConnPolicies = caddytls.ConnectionPolicies{
- {ALPN: defaultALPN},
+ // tell the server to use TLS if it is not already doing so
+ if srv.TLSConnPolicies == nil {
+ srv.TLSConnPolicies = caddytls.ConnectionPolicies{
+ &caddytls.ConnectionPolicy{ALPN: defaultALPN},
+ }
}
if srv.AutoHTTPS.DisableRedir {
@@ -307,6 +317,12 @@ func (app *App) automaticHTTPS() error {
log.Printf("[INFO] Enabling automatic HTTP->HTTPS redirects for %v", domains)
+ // notify user if their config might override the HTTP->HTTPS redirects
+ if srv.listenersIncludePort(app.HTTPPort) {
+ log.Printf("[WARNING] Server %v is listening on HTTP port %d, so automatic HTTP->HTTPS redirects may be overridden by your own configuration",
+ srv.Listen, app.HTTPPort)
+ }
+
// create HTTP->HTTPS redirects
for _, addr := range srv.Listen {
netw, host, port, err := caddy.SplitNetworkAddress(addr)
diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go
index 4622af2..b7cb311 100644
--- a/modules/caddyhttp/fileserver/caddyfile.go
+++ b/modules/caddyhttp/fileserver/caddyfile.go
@@ -94,7 +94,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
}
handler := rewrite.Rewrite{
- URI: "{http.matchers.file.relative}{http.request.uri.query}",
+ URI: "{http.matchers.file.relative}{http.request.uri.query_string}",
}
matcherSet := map[string]json.RawMessage{
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index cfb79f8..3e4cccc 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -41,10 +41,11 @@ func init() {
// FileServer implements a static file server responder for Caddy.
type FileServer struct {
- Root string `json:"root,omitempty"` // default is current directory
- Hide []string `json:"hide,omitempty"`
- IndexNames []string `json:"index_names,omitempty"`
- Browse *Browse `json:"browse,omitempty"`
+ Root string `json:"root,omitempty"` // default is current directory
+ Hide []string `json:"hide,omitempty"`
+ IndexNames []string `json:"index_names,omitempty"`
+ Browse *Browse `json:"browse,omitempty"`
+ CanonicalURIs *bool `json:"canonical_uris,omitempty"`
}
// CaddyModule returns the Caddy module information.
@@ -109,6 +110,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
// if the request mapped to a directory, see if
// there is an index file we can serve
+ var implicitIndexFile bool
if info.IsDir() && len(fsrv.IndexNames) > 0 {
for _, indexPage := range fsrv.IndexNames {
indexPath := sanitizedPathJoin(filename, indexPage)
@@ -122,12 +124,17 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
continue
}
- // we found an index file that might work,
- // so rewrite the request path
- r.URL.Path = path.Join(r.URL.Path, indexPage)
+ // don't rewrite the request path to append
+ // the index file, because we might need to
+ // do a canonical-URL redirect below based
+ // on the URL as-is
+ // we've chosen to use this index file,
+ // so replace the last file info and path
+ // with that of the index file
info = indexInfo
filename = indexPath
+ implicitIndexFile = true
break
}
}
@@ -149,10 +156,22 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
return caddyhttp.Error(http.StatusNotFound, nil)
}
+ // if URL canonicalization is enabled, we need to enforce trailing
+ // slash convention: if a directory, trailing slash; if a file, no
+ // trailing slash - not enforcing this can break relative hrefs
+ // in HTML (see https://github.com/caddyserver/caddy/issues/2741)
+ if fsrv.CanonicalURIs == nil || *fsrv.CanonicalURIs {
+ if implicitIndexFile && !strings.HasSuffix(r.URL.Path, "/") {
+ return redirect(w, r, r.URL.Path+"/")
+ } else if !implicitIndexFile && strings.HasSuffix(r.URL.Path, "/") {
+ return redirect(w, r, r.URL.Path[:len(r.URL.Path)-1])
+ }
+ }
+
// open the file
file, err := fsrv.openFile(filename, w)
if err != nil {
- return err
+ return err // error is already structured
}
defer file.Close()
@@ -309,6 +328,15 @@ func calculateEtag(d os.FileInfo) string {
return `"` + t + s + `"`
}
+func redirect(w http.ResponseWriter, r *http.Request, to string) error {
+ for strings.HasPrefix(to, "//") {
+ // prevent path-based open redirects
+ to = strings.TrimPrefix(to, "/")
+ }
+ http.Redirect(w, r, to, http.StatusPermanentRedirect)
+ return nil
+}
+
var defaultIndexNames = []string{"index.html", "index.txt"}
var bufPool = sync.Pool{
diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go
index f7f69a4..e003259 100644
--- a/modules/caddyhttp/replacer.go
+++ b/modules/caddyhttp/replacer.go
@@ -89,6 +89,12 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
return dir, true
case "http.request.uri.query":
return req.URL.RawQuery, true
+ case "http.request.uri.query_string":
+ qs := req.URL.Query().Encode()
+ if qs != "" {
+ qs = "?" + qs
+ }
+ return qs, true
}
// hostname labels
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 04935e6..42f7a5a 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -41,7 +41,7 @@ type Server struct {
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"`
AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
MaxRehandles *int `json:"max_rehandles,omitempty"`
- StrictSNIHost bool `json:"strict_sni_host,omitempty"` // TODO: see if we can turn this on by default when clientauth is configured
+ StrictSNIHost bool `json:"strict_sni_host,omitempty"`
tlsApp *caddytls.TLS
}
@@ -183,6 +183,32 @@ func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
return false
}
+// listenersIncludePort returns true if there are any
+// listeners in s that use otherPort.
+func (s *Server) listenersIncludePort(otherPort int) bool {
+ for _, lnAddr := range s.Listen {
+ _, addrs, err := caddy.ParseListenAddr(lnAddr)
+ if err == nil {
+ for _, a := range addrs {
+ _, port, err := net.SplitHostPort(a)
+ if err == nil && port == strconv.Itoa(otherPort) {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
+func (s *Server) hasTLSClientAuth() bool {
+ for _, cp := range s.TLSConnPolicies {
+ if cp.ClientAuthentication != nil && cp.ClientAuthentication.Active() {
+ return true
+ }
+ }
+ return false
+}
+
// AutoHTTPSConfig is used to disable automatic HTTPS
// or certain aspects of it for a specific server.
type AutoHTTPSConfig struct {
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index e061281..16fd4f1 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -17,6 +17,7 @@ package caddytls
import (
"crypto/tls"
"crypto/x509"
+ "encoding/base64"
"encoding/json"
"fmt"
"strings"
@@ -111,13 +112,12 @@ type ConnectionPolicy struct {
Matchers map[string]json.RawMessage `json:"match,omitempty"`
CertSelection json.RawMessage `json:"certificate_selection,omitempty"`
- CipherSuites []string `json:"cipher_suites,omitempty"`
- Curves []string `json:"curves,omitempty"`
- ALPN []string `json:"alpn,omitempty"`
- ProtocolMin string `json:"protocol_min,omitempty"`
- ProtocolMax string `json:"protocol_max,omitempty"`
-
- // TODO: Client auth
+ CipherSuites []string `json:"cipher_suites,omitempty"`
+ Curves []string `json:"curves,omitempty"`
+ ALPN []string `json:"alpn,omitempty"`
+ ProtocolMin string `json:"protocol_min,omitempty"`
+ ProtocolMax string `json:"protocol_max,omitempty"`
+ ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"`
matchers []ConnectionMatcher
certSelector certmagic.CertificateSelector
@@ -167,7 +167,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
tlsApp.SessionTickets.unregister(cfg)
})
- // TODO: Clean up active locks if app (or process) is being closed!
+ // TODO: Clean up session ticket active locks in storage if app (or process) is being closed!
// add all the cipher suites in order, without duplicates
cipherSuitesAdded := make(map[uint16]struct{})
@@ -212,7 +212,15 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
}
- // TODO: client auth, and other fields
+ // client authentication
+ if p.ClientAuthentication != nil {
+ err := p.ClientAuthentication.ConfigureTLSConfig(cfg)
+ if err != nil {
+ return fmt.Errorf("configuring TLS client authentication: %v", err)
+ }
+ }
+
+ // TODO: other fields
setDefaultTLSParams(cfg)
@@ -221,6 +229,119 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
return nil
}
+// ClientAuthentication configures TLS client auth.
+type ClientAuthentication struct {
+ // A list of base64 DER-encoded CA certificates
+ // against which to validate client certificates.
+ // Client certs which are not signed by any of
+ // these CAs will be rejected.
+ TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
+
+ // A list of base64 DER-encoded client leaf certs
+ // to accept. If this list is not empty, client certs
+ // which are not in this list will be rejected.
+ TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"`
+
+ // state established with the last call to ConfigureTLSConfig
+ trustedLeafCerts []*x509.Certificate
+ existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error
+}
+
+// Active returns true if clientauth has an actionable configuration.
+func (clientauth ClientAuthentication) Active() bool {
+ return len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedLeafCerts) > 0
+}
+
+// ConfigureTLSConfig sets up cfg to enforce clientauth's configuration.
+func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) error {
+ // if there's no actionable client auth, simply disable it
+ if !clientauth.Active() {
+ cfg.ClientAuth = tls.NoClientCert
+ return nil
+ }
+
+ // otherwise, at least require any client certificate
+ cfg.ClientAuth = tls.RequireAnyClientCert
+
+ // enforce CA verification by adding CA certs to the ClientCAs pool
+ if len(clientauth.TrustedCACerts) > 0 {
+ caPool := x509.NewCertPool()
+ for _, clientCAString := range clientauth.TrustedCACerts {
+ clientCA, err := decodeBase64DERCert(clientCAString)
+ if err != nil {
+ return fmt.Errorf("parsing certificate: %v", err)
+ }
+ caPool.AddCert(clientCA)
+ }
+ cfg.ClientCAs = caPool
+
+ // now ensure the standard lib will verify client certificates
+ cfg.ClientAuth = tls.RequireAndVerifyClientCert
+ }
+
+ // enforce leaf verification by writing our own verify function
+ if len(clientauth.TrustedLeafCerts) > 0 {
+ clientauth.trustedLeafCerts = []*x509.Certificate{}
+
+ for _, clientCertString := range clientauth.TrustedLeafCerts {
+ clientCert, err := decodeBase64DERCert(clientCertString)
+ if err != nil {
+ return fmt.Errorf("parsing certificate: %v", err)
+ }
+ clientauth.trustedLeafCerts = append(clientauth.trustedLeafCerts, clientCert)
+ }
+
+ // if a custom verification function already exists, wrap it
+ clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
+
+ cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
+ }
+
+ return nil
+}
+
+// verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate
+// callback to do custom client certificate verification. It is intended
+// for installation only by clientauth.ConfigureTLSConfig().
+func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+ // first use any pre-existing custom verification function
+ if clientauth.existingVerifyPeerCert != nil {
+ err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains)
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(rawCerts) == 0 {
+ return fmt.Errorf("no client certificate provided")
+ }
+
+ remoteLeafCert, err := x509.ParseCertificate(rawCerts[len(rawCerts)-1])
+ if err != nil {
+ return fmt.Errorf("can't parse the given certificate: %s", err.Error())
+ }
+
+ for _, trustedLeafCert := range clientauth.trustedLeafCerts {
+ if remoteLeafCert.Equal(trustedLeafCert) {
+ return nil
+ }
+ }
+
+ return fmt.Errorf("client leaf certificate failed validation")
+}
+
+// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
+func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
+ // decode base64
+ derBytes, err := base64.StdEncoding.DecodeString(certStr)
+ if err != nil {
+ return nil, err
+ }
+
+ // parse the DER-encoded certificate
+ return x509.ParseCertificate(derBytes)
+}
+
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
// and server preferences of cfg if they are not already set; it does not
// overwrite values, only fills in missing values.