Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 1 | // Copyright 2015 Matthew Holt and The Caddy Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package caddytls |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "crypto/tls" |
| 20 | "crypto/x509" |
| 21 | "encoding/base64" |
| 22 | "encoding/json" |
| 23 | "encoding/pem" |
| 24 | "fmt" |
| 25 | "io" |
| 26 | "os" |
| 27 | "reflect" |
| 28 | "slices" |
| 29 | "strings" |
| 30 | |
| 31 | "github.com/mholt/acmez/v3" |
| 32 | "go.uber.org/zap" |
| 33 | "go.uber.org/zap/zapcore" |
| 34 | |
| 35 | "github.com/caddyserver/caddy/v2" |
| 36 | "github.com/caddyserver/caddy/v2/caddyconfig" |
| 37 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" |
| 38 | ) |
| 39 | |
| 40 | func init() { |
| 41 | caddy.RegisterModule(LeafCertClientAuth{}) |
| 42 | } |
| 43 | |
| 44 | // ConnectionPolicies govern the establishment of TLS connections. It is |
| 45 | // an ordered group of connection policies; the first matching policy will |
| 46 | // be used to configure TLS connections at handshake-time. |
| 47 | type ConnectionPolicies []*ConnectionPolicy |
| 48 | |
| 49 | // Provision sets up each connection policy. It should be called |
| 50 | // during the Validate() phase, after the TLS app (if any) is |
| 51 | // already set up. |
| 52 | func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { |
| 53 | for i, pol := range cp { |
| 54 | // matchers |
| 55 | mods, err := ctx.LoadModule(pol, "MatchersRaw") |
| 56 | if err != nil { |
| 57 | return fmt.Errorf("loading handshake matchers: %v", err) |
| 58 | } |
| 59 | for _, modIface := range mods.(map[string]any) { |
| 60 | cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher)) |
| 61 | } |
| 62 | |
| 63 | // enable HTTP/2 by default |
| 64 | if pol.ALPN == nil { |
| 65 | pol.ALPN = append(pol.ALPN, defaultALPN...) |
| 66 | } |
| 67 | |
| 68 | // pre-build standard TLS config so we don't have to at handshake-time |
| 69 | err = pol.buildStandardTLSConfig(ctx) |
| 70 | if err != nil { |
| 71 | return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) |
| 72 | } |
| 73 | |
| 74 | if pol.ClientAuthentication != nil && len(pol.ClientAuthentication.VerifiersRaw) > 0 { |
| 75 | clientCertValidations, err := ctx.LoadModule(pol.ClientAuthentication, "VerifiersRaw") |
| 76 | if err != nil { |
| 77 | return fmt.Errorf("loading client cert verifiers: %v", err) |
| 78 | } |
| 79 | for _, validator := range clientCertValidations.([]any) { |
| 80 | cp[i].ClientAuthentication.verifiers = append(cp[i].ClientAuthentication.verifiers, validator.(ClientCertificateVerifier)) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | if len(pol.HandshakeContextRaw) > 0 { |
| 85 | modIface, err := ctx.LoadModule(pol, "HandshakeContextRaw") |
| 86 | if err != nil { |
| 87 | return fmt.Errorf("loading handshake context module: %v", err) |
| 88 | } |
| 89 | cp[i].handshakeContext = modIface.(HandshakeContext) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | return nil |
| 94 | } |
| 95 | |
| 96 | // TLSConfig returns a standard-lib-compatible TLS configuration which |
| 97 | // selects the first matching policy based on the ClientHello. |
| 98 | func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { |
| 99 | // using ServerName to match policies is extremely common, especially in configs |
| 100 | // with lots and lots of different policies; we can fast-track those by indexing |
| 101 | // them by SNI, so we don't have to iterate potentially thousands of policies |
| 102 | // (TODO: this map does not account for wildcards, see if this is a problem in practice? look for reports of high connection latency with wildcard certs but low latency for non-wildcards in multi-thousand-cert deployments) |
| 103 | indexedBySNI := make(map[string]ConnectionPolicies) |
| 104 | if len(cp) > 30 { |
| 105 | for _, p := range cp { |
| 106 | for _, m := range p.matchers { |
| 107 | if sni, ok := m.(MatchServerName); ok { |
| 108 | for _, sniName := range sni { |
| 109 | // index for fast lookups during handshakes |
| 110 | indexName := asciiServerNameForMatch(sniName) |
| 111 | indexedBySNI[indexName] = append(indexedBySNI[indexName], p) |
| 112 | } |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | getConfigForClient := func(hello *tls.ClientHelloInfo) (*tls.Config, error) { |
| 119 | // filter policies by SNI first, if possible, to speed things up |
| 120 | // when there may be lots of policies |
| 121 | possiblePolicies := cp |
| 122 | if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok { |
| 123 | possiblePolicies = indexedPolicies |
| 124 | } |
| 125 | |
| 126 | policyLoop: |
| 127 | for _, pol := range possiblePolicies { |
| 128 | for _, matcher := range pol.matchers { |
| 129 | if !matcher.Match(hello) { |
| 130 | continue policyLoop |
| 131 | } |
| 132 | } |
| 133 | if pol.Drop { |
| 134 | return nil, fmt.Errorf("dropping connection") |
| 135 | } |
| 136 | return pol.TLSConfig, nil |
| 137 | } |
| 138 | |
| 139 | return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) |
| 140 | } |
| 141 | |
| 142 | tlsCfg := &tls.Config{ |
| 143 | MinVersion: tls.VersionTLS12, |
| 144 | GetConfigForClient: getConfigForClient, |
| 145 | } |
| 146 | |
| 147 | // enable ECH, if configured |
| 148 | if tlsAppIface, err := ctx.AppIfConfigured("tls"); err == nil { |
| 149 | tlsApp := tlsAppIface.(*TLS) |
| 150 | |
| 151 | if tlsApp.EncryptedClientHello != nil && len(tlsApp.EncryptedClientHello.configs) > 0 { |
| 152 | // if no publication was configured, we apply ECH to all server names by default, |
| 153 | // but the TLS app needs to know what they are in this case, since they don't appear |
| 154 | // in its config (remember, TLS connection policies are used by *other* apps to |
| 155 | // run TLS servers) -- we skip names with placeholders |
| 156 | if tlsApp.EncryptedClientHello.Publication == nil { |
| 157 | repl := caddy.NewReplacer() |
| 158 | for _, p := range cp { |
| 159 | var echNames []string |
| 160 | for _, m := range p.matchers { |
| 161 | if sni, ok := m.(MatchServerName); ok { |
| 162 | for _, name := range sni { |
| 163 | finalName := strings.ToLower(repl.ReplaceAll(name, "")) |
| 164 | echNames = append(echNames, finalName) |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | tlsApp.RegisterServerNames(echNames, p.ALPN) |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | getECHKeys := func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { |
| 173 | tlsApp.EncryptedClientHello.configsMu.RLock() |
| 174 | defer tlsApp.EncryptedClientHello.configsMu.RUnlock() |
| 175 | return tlsApp.EncryptedClientHello.stdlibReady, nil |
| 176 | } |
| 177 | |
| 178 | // crypto/tls decrypts the ClientHello with the keys from this config, |
| 179 | // but builds the retry configs it sends when ECH is rejected from the |
| 180 | // config returned by GetConfigForClient, so both need the keys. |
| 181 | tlsCfg.GetEncryptedClientHelloKeys = getECHKeys |
| 182 | for _, p := range cp { |
| 183 | if p.TLSConfig != nil && p.TLSConfig.GetEncryptedClientHelloKeys == nil { |
| 184 | p.TLSConfig.GetEncryptedClientHelloKeys = getECHKeys |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | return tlsCfg |
| 191 | } |
| 192 | |
| 193 | // ConnectionPolicy specifies the logic for handling a TLS handshake. |
| 194 | // An empty policy is valid; safe and sensible defaults will be used. |
| 195 | type ConnectionPolicy struct { |
| 196 | // How to match this policy with a TLS ClientHello. If |
| 197 | // this policy is the first to match, it will be used. |
| 198 | MatchersRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=tls.handshake_match"` |
| 199 | matchers []ConnectionMatcher |
| 200 | |
| 201 | // How to choose a certificate if more than one matched |
| 202 | // the given ServerName (SNI) value. |
| 203 | CertSelection *CustomCertSelectionPolicy `json:"certificate_selection,omitempty"` |
| 204 | |
| 205 | // The list of cipher suites to support. Caddy's |
| 206 | // defaults are modern and secure. |
| 207 | CipherSuites []string `json:"cipher_suites,omitempty"` |
| 208 | |
| 209 | // The list of elliptic curves to support. Caddy's |
| 210 | // defaults are modern and secure. |
| 211 | Curves []string `json:"curves,omitempty"` |
| 212 | |
| 213 | // Protocols to use for Application-Layer Protocol |
| 214 | // Negotiation (ALPN) during the handshake. |
| 215 | ALPN []string `json:"alpn,omitempty"` |
| 216 | |
| 217 | // Minimum TLS protocol version to allow. Default: `tls1.2` |
| 218 | ProtocolMin string `json:"protocol_min,omitempty"` |
| 219 | |
| 220 | // Maximum TLS protocol version to allow. Default: `tls1.3` |
| 221 | ProtocolMax string `json:"protocol_max,omitempty"` |
| 222 | |
| 223 | // Reject TLS connections. EXPERIMENTAL: May change. |
| 224 | Drop bool `json:"drop,omitempty"` |
| 225 | |
| 226 | // Enables and configures TLS client authentication. |
| 227 | ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"` |
| 228 | |
| 229 | // DefaultSNI becomes the ServerName in a ClientHello if there |
| 230 | // is no policy configured for the empty SNI value. |
| 231 | DefaultSNI string `json:"default_sni,omitempty"` |
| 232 | |
| 233 | // FallbackSNI becomes the ServerName in a ClientHello if |
| 234 | // the original ServerName doesn't match any certificates |
| 235 | // in the cache. The use cases for this are very niche; |
| 236 | // typically if a client is a CDN and passes through the |
| 237 | // ServerName of the downstream handshake but can accept |
| 238 | // a certificate with the origin's hostname instead, then |
| 239 | // you would set this to your origin's hostname. Note that |
| 240 | // Caddy must be managing a certificate for this name. |
| 241 | // |
| 242 | // This feature is EXPERIMENTAL and subject to change or removal. |
| 243 | FallbackSNI string `json:"fallback_sni,omitempty"` |
| 244 | |
| 245 | // Also known as "SSLKEYLOGFILE", TLS secrets will be written to |
| 246 | // this file in NSS key log format which can then be parsed by |
| 247 | // Wireshark and other tools. This is INSECURE as it allows other |
| 248 | // programs or tools to decrypt TLS connections. However, this |
| 249 | // capability can be useful for debugging and troubleshooting. |
| 250 | // **ENABLING THIS LOG COMPROMISES SECURITY!** |
| 251 | // |
| 252 | // This feature is EXPERIMENTAL and subject to change or removal. |
| 253 | InsecureSecretsLog string `json:"insecure_secrets_log,omitempty"` |
| 254 | |
| 255 | // A module that can manipulate the context passed into CertMagic's |
| 256 | // certificate management functions during TLS handshakes. |
| 257 | // EXPERIMENTAL - subject to change or removal. |
| 258 | HandshakeContextRaw json.RawMessage `json:"handshake_context,omitempty" caddy:"namespace=tls.context inline_key=module"` |
| 259 | handshakeContext HandshakeContext |
| 260 | |
| 261 | // TLSConfig is the fully-formed, standard lib TLS config |
| 262 | // used to serve TLS connections. Provision all |
| 263 | // ConnectionPolicies to populate this. It is exported only |
| 264 | // so it can be minimally adjusted after provisioning |
| 265 | // if necessary (like to adjust NextProtos to disable HTTP/2), |
| 266 | // and may be unexported in the future. |
| 267 | TLSConfig *tls.Config `json:"-"` |
| 268 | } |
| 269 | |
| 270 | type HandshakeContext interface { |
| 271 | // HandshakeContext returns a context to pass into CertMagic's |
| 272 | // GetCertificate function used to serve, load, and manage certs |
| 273 | // during TLS handshakes. Generally you'll start with the context |
| 274 | // from the ClientHelloInfo, but you may use other information |
| 275 | // from it as well. Return an error to abort the handshake. |
| 276 | HandshakeContext(*tls.ClientHelloInfo) (context.Context, error) |
| 277 | } |
| 278 | |
| 279 | func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { |
| 280 | tlsAppIface, err := ctx.App("tls") |
| 281 | if err != nil { |
| 282 | return fmt.Errorf("getting tls app: %v", err) |
| 283 | } |
| 284 | tlsApp := tlsAppIface.(*TLS) |
| 285 | |
| 286 | // fill in some "easy" default values, but for other values |
| 287 | // (such as slices), we should ensure that they start empty |
| 288 | // so the user-provided config can fill them in; then we will |
| 289 | // fill in a default config at the end if they are still unset |
| 290 | cfg := &tls.Config{ |
| 291 | NextProtos: p.ALPN, |
| 292 | GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { |
| 293 | // TODO: I don't love how this works: we pre-build certmagic configs |
| 294 | // so that handshakes are faster. Unfortunately, certmagic configs are |
| 295 | // comprised of settings from both a TLS connection policy and a TLS |
| 296 | // automation policy. The only two fields (as of March 2020; v2 beta 17) |
| 297 | // of a certmagic config that come from the TLS connection policy are |
| 298 | // CertSelection and DefaultServerName, so an automation policy is what |
| 299 | // builds the base certmagic config. Since the pre-built config is |
| 300 | // shared, I don't think we can change any of its fields per-handshake, |
| 301 | // hence the awkward shallow copy (dereference) here and the subsequent |
| 302 | // changing of some of its fields. I'm worried this dereference allocates |
| 303 | // more at handshake-time, but I don't know how to practically pre-build |
| 304 | // a certmagic config for each combination of conn policy + automation policy... |
| 305 | cfg := *tlsApp.getConfigForName(hello.ServerName) |
| 306 | if p.CertSelection != nil { |
| 307 | // you would think we could just set this whether or not |
| 308 | // p.CertSelection is nil, but that leads to panics if |
| 309 | // it is, because cfg.CertSelection is an interface, |
| 310 | // so it will have a non-nil value even if the actual |
| 311 | // value underlying it is nil (sigh) |
| 312 | cfg.CertSelection = p.CertSelection |
| 313 | } |
| 314 | cfg.DefaultServerName = p.DefaultSNI |
| 315 | cfg.FallbackServerName = p.FallbackSNI |
| 316 | |
| 317 | // TODO: experimental: if a handshake context module is configured, allow it |
| 318 | // to modify the context before passing it into CertMagic's GetCertificate |
| 319 | ctx := hello.Context() |
| 320 | if p.handshakeContext != nil { |
| 321 | ctx, err = p.handshakeContext.HandshakeContext(hello) |
| 322 | if err != nil { |
| 323 | return nil, fmt.Errorf("handshake context: %v", err) |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | return cfg.GetCertificateWithContext(ctx, hello) |
| 328 | }, |
| 329 | MinVersion: tls.VersionTLS12, |
| 330 | MaxVersion: tls.VersionTLS13, |
| 331 | } |
| 332 | |
| 333 | // session tickets support |
| 334 | if tlsApp.SessionTickets != nil { |
| 335 | cfg.SessionTicketsDisabled = tlsApp.SessionTickets.Disabled |
| 336 | |
| 337 | // session ticket key rotation |
| 338 | tlsApp.SessionTickets.register(cfg) |
| 339 | ctx.OnCancel(func() { |
| 340 | // do cleanup when the context is canceled because, |
| 341 | // though unlikely, it is possible that a context |
| 342 | // needing a TLS server config could exist for less |
| 343 | // than the lifetime of the whole app |
| 344 | tlsApp.SessionTickets.unregister(cfg) |
| 345 | }) |
| 346 | } |
| 347 | |
| 348 | // TODO: Clean up session ticket active locks in storage if app (or process) is being closed! |
| 349 | |
| 350 | // add all the cipher suites in order, without duplicates |
| 351 | cipherSuitesAdded := make(map[uint16]struct{}) |
| 352 | for _, csName := range p.CipherSuites { |
| 353 | csID := CipherSuiteID(csName) |
| 354 | if csID == 0 { |
| 355 | return fmt.Errorf("unsupported cipher suite: %s", csName) |
| 356 | } |
| 357 | if _, ok := cipherSuitesAdded[csID]; !ok { |
| 358 | cipherSuitesAdded[csID] = struct{}{} |
| 359 | cfg.CipherSuites = append(cfg.CipherSuites, csID) |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | // add all the curve preferences in order, without duplicates |
| 364 | curvesAdded := make(map[tls.CurveID]struct{}) |
| 365 | for _, curveName := range p.Curves { |
| 366 | curveID, ok := SupportedCurves[curveName] |
| 367 | if !ok { |
| 368 | return fmt.Errorf("unsupported curve: %s", curveName) |
| 369 | } |
| 370 | if _, ok := curvesAdded[curveID]; !ok { |
| 371 | curvesAdded[curveID] = struct{}{} |
| 372 | cfg.CurvePreferences = append(cfg.CurvePreferences, curveID) |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | // ensure ALPN includes the ACME TLS-ALPN protocol |
| 377 | alpnFound := slices.Contains(p.ALPN, acmez.ACMETLS1Protocol) |
| 378 | if !alpnFound && (cfg.NextProtos == nil || len(cfg.NextProtos) > 0) { |
| 379 | cfg.NextProtos = append(cfg.NextProtos, acmez.ACMETLS1Protocol) |
| 380 | } |
| 381 | |
| 382 | // min and max protocol versions |
| 383 | if p.ProtocolMin != "" { |
| 384 | minVersion, ok := SupportedProtocols[p.ProtocolMin] |
| 385 | if !ok { |
| 386 | return fmt.Errorf("unsupported protocol_min: %s", p.ProtocolMin) |
| 387 | } |
| 388 | cfg.MinVersion = minVersion |
| 389 | } |
| 390 | if p.ProtocolMax != "" { |
| 391 | maxVersion, ok := SupportedProtocols[p.ProtocolMax] |
| 392 | if !ok { |
| 393 | return fmt.Errorf("unsupported protocol_max: %s", p.ProtocolMax) |
| 394 | } |
| 395 | cfg.MaxVersion = maxVersion |
| 396 | } |
| 397 | if cfg.MinVersion > cfg.MaxVersion { |
| 398 | return fmt.Errorf("protocol min (%s) cannot be greater than protocol max (%s)", |
| 399 | ProtocolName(cfg.MinVersion), ProtocolName(cfg.MaxVersion)) |
| 400 | } |
| 401 | |
| 402 | // client authentication |
| 403 | if p.ClientAuthentication != nil { |
| 404 | if err := p.ClientAuthentication.provision(ctx); err != nil { |
| 405 | return fmt.Errorf("provisioning client CA: %v", err) |
| 406 | } |
| 407 | if err := p.ClientAuthentication.ConfigureTLSConfig(cfg); err != nil { |
| 408 | return fmt.Errorf("configuring TLS client authentication: %v", err) |
| 409 | } |
| 410 | |
| 411 | // Prevent privilege escalation in case multiple vhosts are configured for |
| 412 | // this TLS server; we could potentially figure out if that's the case, but |
| 413 | // that might be complex to get right every time. Actually, two proper |
| 414 | // solutions could leave tickets enabled, but I am not sure how to do them |
| 415 | // properly without significant time investment; there may be new Go |
| 416 | // APIs that alloaw this (Wrap/UnwrapSession?) but I do not know how to use |
| 417 | // them at this time. TODO: one of these is a possible future enhancement: |
| 418 | // A) Prevent resumptions across server identities (certificates): binding the ticket to the |
| 419 | // certificate we would serve in a full handshake, or even bind a ticket to the exact SNI |
| 420 | // it was issued under (though there are proposals for session resumption across hostnames). |
| 421 | // B) Prevent resumptions falsely authenticating a client: include the realm in the ticket, |
| 422 | // so that it can be validated upon resumption. |
| 423 | cfg.SessionTicketsDisabled = true |
| 424 | } |
| 425 | |
| 426 | if p.InsecureSecretsLog != "" { |
| 427 | filename, err := caddy.NewReplacer().ReplaceOrErr(p.InsecureSecretsLog, true, true) |
| 428 | if err != nil { |
| 429 | return err |
| 430 | } |
| 431 | filename, err = caddy.FastAbs(filename) |
| 432 | if err != nil { |
| 433 | return err |
| 434 | } |
| 435 | logFile, _, err := secretsLogPool.LoadOrNew(filename, func() (caddy.Destructor, error) { |
| 436 | w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) |
| 437 | return destructableWriter{w}, err |
| 438 | }) |
| 439 | if err != nil { |
| 440 | return err |
| 441 | } |
| 442 | ctx.OnCancel(func() { _, _ = secretsLogPool.Delete(filename) }) |
| 443 | |
| 444 | cfg.KeyLogWriter = logFile.(io.Writer) |
| 445 | |
| 446 | if c := tlsApp.logger.Check(zapcore.WarnLevel, "TLS SECURITY COMPROMISED: secrets logging is enabled!"); c != nil { |
| 447 | c.Write(zap.String("log_filename", filename)) |
| 448 | } |
| 449 | } |
| 450 | |
| 451 | setDefaultTLSParams(cfg) |
| 452 | |
| 453 | p.TLSConfig = cfg |
| 454 | |
| 455 | return nil |
| 456 | } |
| 457 | |
| 458 | // SettingsEmpty returns true if p's settings (fields |
| 459 | // except the matchers) are all empty/unset. |
| 460 | func (p ConnectionPolicy) SettingsEmpty() bool { |
| 461 | return p.CertSelection == nil && |
| 462 | p.CipherSuites == nil && |
| 463 | p.Curves == nil && |
| 464 | p.ALPN == nil && |
| 465 | p.ProtocolMin == "" && |
| 466 | p.ProtocolMax == "" && |
| 467 | p.ClientAuthentication == nil && |
| 468 | p.DefaultSNI == "" && |
| 469 | p.FallbackSNI == "" && |
| 470 | p.InsecureSecretsLog == "" |
| 471 | } |
| 472 | |
| 473 | // SettingsEqual returns true if p's settings (fields |
| 474 | // except the matchers) are the same as q. |
| 475 | func (p ConnectionPolicy) SettingsEqual(q ConnectionPolicy) bool { |
| 476 | p.MatchersRaw = nil |
| 477 | q.MatchersRaw = nil |
| 478 | return reflect.DeepEqual(p, q) |
| 479 | } |
| 480 | |
| 481 | // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax: |
| 482 | // |
| 483 | // connection_policy { |
| 484 | // alpn <values...> |
| 485 | // cert_selection { |
| 486 | // ... |
| 487 | // } |
| 488 | // ciphers <cipher_suites...> |
| 489 | // client_auth { |
| 490 | // ... |
| 491 | // } |
| 492 | // curves <curves...> |
| 493 | // default_sni <server_name> |
| 494 | // match { |
| 495 | // ... |
| 496 | // } |
| 497 | // protocols <min> [<max>] |
| 498 | // # EXPERIMENTAL: |
| 499 | // drop |
| 500 | // fallback_sni <server_name> |
| 501 | // insecure_secrets_log <log_file> |
| 502 | // } |
| 503 | func (cp *ConnectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 504 | _, wrapper := d.Next(), d.Val() |
| 505 | |
| 506 | // No same-line options are supported |
| 507 | if d.CountRemainingArgs() > 0 { |
| 508 | return d.ArgErr() |
| 509 | } |
| 510 | |
| 511 | var hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop, |
| 512 | hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool |
| 513 | for nesting := d.Nesting(); d.NextBlock(nesting); { |
| 514 | optionName := d.Val() |
| 515 | switch optionName { |
| 516 | case "alpn": |
| 517 | if d.CountRemainingArgs() == 0 { |
| 518 | return d.ArgErr() |
| 519 | } |
| 520 | cp.ALPN = append(cp.ALPN, d.RemainingArgs()...) |
| 521 | case "cert_selection": |
| 522 | if hasCertSelection { |
| 523 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 524 | } |
| 525 | p := &CustomCertSelectionPolicy{} |
| 526 | if err := p.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil { |
| 527 | return err |
| 528 | } |
| 529 | cp.CertSelection, hasCertSelection = p, true |
| 530 | case "client_auth": |
| 531 | if hasClientAuth { |
| 532 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 533 | } |
| 534 | ca := &ClientAuthentication{} |
| 535 | if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil { |
| 536 | return err |
| 537 | } |
| 538 | cp.ClientAuthentication, hasClientAuth = ca, true |
| 539 | case "ciphers": |
| 540 | if d.CountRemainingArgs() == 0 { |
| 541 | return d.ArgErr() |
| 542 | } |
| 543 | cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...) |
| 544 | case "curves": |
| 545 | if d.CountRemainingArgs() == 0 { |
| 546 | return d.ArgErr() |
| 547 | } |
| 548 | cp.Curves = append(cp.Curves, d.RemainingArgs()...) |
| 549 | case "default_sni": |
| 550 | if hasDefaultSNI { |
| 551 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 552 | } |
| 553 | if d.CountRemainingArgs() != 1 { |
| 554 | return d.ArgErr() |
| 555 | } |
| 556 | _, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true |
| 557 | case "drop": // EXPERIMENTAL |
| 558 | if hasDrop { |
| 559 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 560 | } |
| 561 | cp.Drop, hasDrop = true, true |
| 562 | case "fallback_sni": // EXPERIMENTAL |
| 563 | if hasFallbackSNI { |
| 564 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 565 | } |
| 566 | if d.CountRemainingArgs() != 1 { |
| 567 | return d.ArgErr() |
| 568 | } |
| 569 | _, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true |
| 570 | case "insecure_secrets_log": // EXPERIMENTAL |
| 571 | if hasInsecureSecretsLog { |
| 572 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 573 | } |
| 574 | if d.CountRemainingArgs() != 1 { |
| 575 | return d.ArgErr() |
| 576 | } |
| 577 | _, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true |
| 578 | case "match": |
| 579 | if hasMatch { |
| 580 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 581 | } |
| 582 | matcherSet, err := ParseCaddyfileNestedMatcherSet(d) |
| 583 | if err != nil { |
| 584 | return err |
| 585 | } |
| 586 | cp.MatchersRaw, hasMatch = matcherSet, true |
| 587 | case "protocols": |
| 588 | if hasProtocols { |
| 589 | return d.Errf("duplicate %s option '%s'", wrapper, optionName) |
| 590 | } |
| 591 | if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 { |
| 592 | return d.ArgErr() |
| 593 | } |
| 594 | _, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true |
| 595 | if d.NextArg() { |
| 596 | cp.ProtocolMax = d.Val() |
| 597 | } |
| 598 | default: |
| 599 | return d.ArgErr() |
| 600 | } |
| 601 | |
| 602 | // No nested blocks are supported |
| 603 | if d.NextBlock(nesting + 1) { |
| 604 | return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName) |
| 605 | } |
| 606 | } |
| 607 | |
| 608 | return nil |
| 609 | } |
| 610 | |
| 611 | // ClientAuthentication configures TLS client auth. |
| 612 | type ClientAuthentication struct { |
| 613 | // Certificate authority module which provides the certificate pool of trusted certificates |
| 614 | CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` |
| 615 | ca CA |
| 616 | |
| 617 | // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead. |
| 618 | // A list of base64 DER-encoded CA certificates |
| 619 | // against which to validate client certificates. |
| 620 | // Client certs which are not signed by any of |
| 621 | // these CAs will be rejected. |
| 622 | TrustedCACerts []string `json:"trusted_ca_certs,omitempty"` |
| 623 | |
| 624 | // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead. |
| 625 | // TrustedCACertPEMFiles is a list of PEM file names |
| 626 | // from which to load certificates of trusted CAs. |
| 627 | // Client certificates which are not signed by any of |
| 628 | // these CA certificates will be rejected. |
| 629 | TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"` |
| 630 | |
| 631 | // Deprecated: This field is deprecated and will be removed in |
| 632 | // a future version. Please use the `validators` field instead |
| 633 | // with the tls.client_auth.verifier.leaf module instead. |
| 634 | // |
| 635 | // A list of base64 DER-encoded client leaf certs |
| 636 | // to accept. If this list is not empty, client certs |
| 637 | // which are not in this list will be rejected. |
| 638 | TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"` |
| 639 | |
| 640 | // Client certificate verification modules. These can perform |
| 641 | // custom client authentication checks, such as ensuring the |
| 642 | // certificate is not revoked. |
| 643 | VerifiersRaw []json.RawMessage `json:"verifiers,omitempty" caddy:"namespace=tls.client_auth.verifier inline_key=verifier"` |
| 644 | |
| 645 | verifiers []ClientCertificateVerifier |
| 646 | |
| 647 | // The mode for authenticating the client. Allowed values are: |
| 648 | // |
| 649 | // Mode | Description |
| 650 | // -----|--------------- |
| 651 | // `request` | Ask clients for a certificate, but allow even if there isn't one; do not verify it |
| 652 | // `require` | Require clients to present a certificate, but do not verify it |
| 653 | // `verify_if_given` | Ask clients for a certificate; allow even if there isn't one, but verify it if there is |
| 654 | // `require_and_verify` | Require clients to present a valid certificate that is verified |
| 655 | // |
| 656 | // The `verify_if_given` and `require_and_verify` modes need a |
| 657 | // trust pool (`ca`). To verify against the system's root |
| 658 | // certificates, use the `tls.ca_pool.source.system` module. |
| 659 | // |
| 660 | // The default mode is `require_and_verify` if a trust pool is |
| 661 | // provided; otherwise, the default mode is `require`. |
| 662 | Mode string `json:"mode,omitempty"` |
| 663 | |
| 664 | existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error |
| 665 | } |
| 666 | |
| 667 | // UnmarshalCaddyfile parses the Caddyfile segment to set up the client authentication. Syntax: |
| 668 | // |
| 669 | // client_auth { |
| 670 | // mode [request|require|verify_if_given|require_and_verify] |
| 671 | // trust_pool <module> { |
| 672 | // ... |
| 673 | // } |
| 674 | // verifier <module> |
| 675 | // } |
| 676 | // |
| 677 | // If `mode` is not provided, it defaults to `require_and_verify` if `trust_pool` is provided. |
| 678 | // Otherwise, it defaults to `require`. The `verify_if_given` and `require_and_verify` modes |
| 679 | // need a `trust_pool`; use `trust_pool system` to verify against the system roots. |
| 680 | func (ca *ClientAuthentication) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 681 | for d.NextArg() { |
| 682 | // consume any tokens on the same line, if any. |
| 683 | } |
| 684 | for nesting := d.Nesting(); d.NextBlock(nesting); { |
| 685 | subdir := d.Val() |
| 686 | switch subdir { |
| 687 | case "mode": |
| 688 | if d.CountRemainingArgs() > 1 { |
| 689 | return d.ArgErr() |
| 690 | } |
| 691 | if !d.Args(&ca.Mode) { |
| 692 | return d.ArgErr() |
| 693 | } |
| 694 | case "trusted_ca_cert": |
| 695 | caddy.Log().Warn("The 'trusted_ca_cert' field is deprecated. Use the 'trust_pool' field instead.") |
| 696 | if len(ca.CARaw) != 0 { |
| 697 | return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'") |
| 698 | } |
| 699 | if !d.NextArg() { |
| 700 | return d.ArgErr() |
| 701 | } |
| 702 | ca.TrustedCACerts = append(ca.TrustedCACerts, d.Val()) |
| 703 | case "trusted_leaf_cert": |
| 704 | if !d.NextArg() { |
| 705 | return d.ArgErr() |
| 706 | } |
| 707 | ca.TrustedLeafCerts = append(ca.TrustedLeafCerts, d.Val()) |
| 708 | case "trusted_ca_cert_file": |
| 709 | caddy.Log().Warn("The 'trusted_ca_cert_file' field is deprecated. Use the 'trust_pool' field instead.") |
| 710 | if len(ca.CARaw) != 0 { |
| 711 | return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'") |
| 712 | } |
| 713 | if !d.NextArg() { |
| 714 | return d.ArgErr() |
| 715 | } |
| 716 | filename := d.Val() |
| 717 | ders, err := convertPEMFilesToDER(filename) |
| 718 | if err != nil { |
| 719 | return d.WrapErr(err) |
| 720 | } |
| 721 | ca.TrustedCACerts = append(ca.TrustedCACerts, ders...) |
| 722 | case "trusted_leaf_cert_file": |
| 723 | if !d.NextArg() { |
| 724 | return d.ArgErr() |
| 725 | } |
| 726 | filename := d.Val() |
| 727 | ders, err := convertPEMFilesToDER(filename) |
| 728 | if err != nil { |
| 729 | return d.WrapErr(err) |
| 730 | } |
| 731 | ca.TrustedLeafCerts = append(ca.TrustedLeafCerts, ders...) |
| 732 | case "trust_pool": |
| 733 | if len(ca.TrustedCACerts) != 0 { |
| 734 | return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'") |
| 735 | } |
| 736 | if !d.NextArg() { |
| 737 | return d.ArgErr() |
| 738 | } |
| 739 | modName := d.Val() |
| 740 | mod, err := caddyfile.UnmarshalModule(d, "tls.ca_pool.source."+modName) |
| 741 | if err != nil { |
| 742 | return d.WrapErr(err) |
| 743 | } |
| 744 | caMod, ok := mod.(CA) |
| 745 | if !ok { |
| 746 | return fmt.Errorf("trust_pool module '%s' is not a certificate pool provider", caMod) |
| 747 | } |
| 748 | ca.CARaw = caddyconfig.JSONModuleObject(caMod, "provider", modName, nil) |
| 749 | case "verifier": |
| 750 | if !d.NextArg() { |
| 751 | return d.ArgErr() |
| 752 | } |
| 753 | |
| 754 | vType := d.Val() |
| 755 | modID := "tls.client_auth.verifier." + vType |
| 756 | unm, err := caddyfile.UnmarshalModule(d, modID) |
| 757 | if err != nil { |
| 758 | return err |
| 759 | } |
| 760 | |
| 761 | _, ok := unm.(ClientCertificateVerifier) |
| 762 | if !ok { |
| 763 | return d.Errf("module '%s' is not a caddytls.ClientCertificateVerifier", modID) |
| 764 | } |
| 765 | ca.VerifiersRaw = append(ca.VerifiersRaw, caddyconfig.JSONModuleObject(unm, "verifier", vType, nil)) |
| 766 | default: |
| 767 | return d.Errf("unknown subdirective for client_auth: %s", subdir) |
| 768 | } |
| 769 | } |
| 770 | |
| 771 | // only trust_ca_cert or trust_ca_cert_file was specified |
| 772 | if len(ca.TrustedCACerts) > 0 { |
| 773 | fileMod := &InlineCAPool{} |
| 774 | fileMod.TrustedCACerts = append(fileMod.TrustedCACerts, ca.TrustedCACerts...) |
| 775 | ca.CARaw = caddyconfig.JSONModuleObject(fileMod, "provider", "inline", nil) |
| 776 | ca.TrustedCACertPEMFiles, ca.TrustedCACerts = nil, nil |
| 777 | } |
| 778 | return nil |
| 779 | } |
| 780 | |
| 781 | func convertPEMFilesToDER(filename string) ([]string, error) { |
| 782 | certDataPEM, err := os.ReadFile(filename) |
| 783 | if err != nil { |
| 784 | return nil, err |
| 785 | } |
| 786 | var ders []string |
| 787 | // while block is not nil, we have more certificates in the file |
| 788 | for block, rest := pem.Decode(certDataPEM); block != nil; block, rest = pem.Decode(rest) { |
| 789 | if block.Type != "CERTIFICATE" { |
| 790 | return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) |
| 791 | } |
| 792 | ders = append( |
| 793 | ders, |
| 794 | base64.StdEncoding.EncodeToString(block.Bytes), |
| 795 | ) |
| 796 | } |
| 797 | // if we decoded nothing, return an error |
| 798 | if len(ders) == 0 { |
| 799 | return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) |
| 800 | } |
| 801 | return ders, nil |
| 802 | } |
| 803 | |
| 804 | func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { |
| 805 | if len(clientauth.CARaw) > 0 && (len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0) { |
| 806 | return fmt.Errorf("conflicting config for client authentication trust CA") |
| 807 | } |
| 808 | |
| 809 | // convert all named file paths to inline |
| 810 | if len(clientauth.TrustedCACertPEMFiles) > 0 { |
| 811 | for _, fpath := range clientauth.TrustedCACertPEMFiles { |
| 812 | ders, err := convertPEMFilesToDER(fpath) |
| 813 | if err != nil { |
| 814 | return err |
| 815 | } |
| 816 | clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...) |
| 817 | } |
| 818 | } |
| 819 | |
| 820 | // if we have TrustedCACerts explicitly set, create an 'inline' CA and return |
| 821 | if len(clientauth.TrustedCACerts) > 0 { |
| 822 | caPool := InlineCAPool{ |
| 823 | TrustedCACerts: clientauth.TrustedCACerts, |
| 824 | } |
| 825 | err := caPool.Provision(ctx) |
| 826 | if err != nil { |
| 827 | return err |
| 828 | } |
| 829 | clientauth.ca = caPool |
| 830 | } |
| 831 | |
| 832 | // if we don't have any CARaw set, there's not much work to do |
| 833 | if clientauth.CARaw == nil { |
| 834 | return nil |
| 835 | } |
| 836 | caRaw, err := ctx.LoadModule(clientauth, "CARaw") |
| 837 | if err != nil { |
| 838 | return err |
| 839 | } |
| 840 | ca, ok := caRaw.(CA) |
| 841 | if !ok { |
| 842 | return fmt.Errorf("'ca' module '%s' is not a certificate pool provider", ca) |
| 843 | } |
| 844 | clientauth.ca = ca |
| 845 | |
| 846 | return nil |
| 847 | } |
| 848 | |
| 849 | // Active returns true if clientauth has an actionable configuration. |
| 850 | func (clientauth ClientAuthentication) Active() bool { |
| 851 | return len(clientauth.TrustedCACerts) > 0 || |
| 852 | len(clientauth.TrustedCACertPEMFiles) > 0 || |
| 853 | len(clientauth.TrustedLeafCerts) > 0 || // TODO: DEPRECATED |
| 854 | len(clientauth.VerifiersRaw) > 0 || |
| 855 | len(clientauth.Mode) > 0 || |
| 856 | clientauth.CARaw != nil || clientauth.ca != nil |
| 857 | } |
| 858 | |
| 859 | // ConfigureTLSConfig sets up cfg to enforce clientauth's configuration. |
| 860 | func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) error { |
| 861 | // if there's no actionable client auth, simply disable it |
| 862 | if !clientauth.Active() { |
| 863 | cfg.ClientAuth = tls.NoClientCert |
| 864 | return nil |
| 865 | } |
| 866 | |
| 867 | // enforce desired mode of client authentication |
| 868 | if len(clientauth.Mode) > 0 { |
| 869 | switch clientauth.Mode { |
| 870 | case "request": |
| 871 | cfg.ClientAuth = tls.RequestClientCert |
| 872 | case "require": |
| 873 | cfg.ClientAuth = tls.RequireAnyClientCert |
| 874 | case "verify_if_given": |
| 875 | cfg.ClientAuth = tls.VerifyClientCertIfGiven |
| 876 | case "require_and_verify": |
| 877 | cfg.ClientAuth = tls.RequireAndVerifyClientCert |
| 878 | default: |
| 879 | return fmt.Errorf("client auth mode not recognized: %s", clientauth.Mode) |
| 880 | } |
| 881 | } else { |
| 882 | // otherwise, set a safe default mode |
| 883 | if len(clientauth.TrustedCACerts) > 0 || |
| 884 | len(clientauth.TrustedCACertPEMFiles) > 0 || |
| 885 | clientauth.CARaw != nil || clientauth.ca != nil { |
| 886 | cfg.ClientAuth = tls.RequireAndVerifyClientCert |
| 887 | } else { |
| 888 | cfg.ClientAuth = tls.RequireAnyClientCert |
| 889 | } |
| 890 | } |
| 891 | |
| 892 | // without ClientCAs, Go verifies client certs against the system roots |
| 893 | verifyMode := cfg.ClientAuth == tls.VerifyClientCertIfGiven || cfg.ClientAuth == tls.RequireAndVerifyClientCert |
| 894 | if verifyMode && clientauth.ca == nil { |
| 895 | return fmt.Errorf("client auth modes verify_if_given and require_and_verify need a trust pool; use the 'system' trust pool to verify against system roots") |
| 896 | } |
| 897 | |
| 898 | // enforce CA verification by adding CA certs to the ClientCAs pool |
| 899 | if clientauth.ca != nil { |
| 900 | cfg.ClientCAs = clientauth.ca.CertPool() |
| 901 | } |
| 902 | |
| 903 | // TODO: DEPRECATED: Only here for backwards compatibility. |
| 904 | // If leaf cert is specified, enforce by adding a client auth module |
| 905 | if len(clientauth.TrustedLeafCerts) > 0 { |
| 906 | caddy.Log().Named("tls.connection_policy").Warn("trusted_leaf_certs is deprecated; use leaf verifier module instead") |
| 907 | var trustedLeafCerts []*x509.Certificate |
| 908 | for _, clientCertString := range clientauth.TrustedLeafCerts { |
| 909 | clientCert, err := decodeBase64DERCert(clientCertString) |
| 910 | if err != nil { |
| 911 | return fmt.Errorf("parsing certificate: %v", err) |
| 912 | } |
| 913 | trustedLeafCerts = append(trustedLeafCerts, clientCert) |
| 914 | } |
| 915 | clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{trustedLeafCerts: trustedLeafCerts}) |
| 916 | } |
| 917 | |
| 918 | // if a custom verification function already exists, wrap it |
| 919 | clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate |
| 920 | cfg.VerifyConnection = clientauth.verifyConnection |
| 921 | return nil |
| 922 | } |
| 923 | |
| 924 | // verifyConnection is for use as a tls.Config.VerifyConnection callback |
| 925 | // to do custom client certificate verification. It is intended for |
| 926 | // installation only by clientauth.ConfigureTLSConfig(). |
| 927 | // |
| 928 | // Unlike VerifyPeerCertificate, VerifyConnection is called on every |
| 929 | // connection including resumed sessions, preventing session-resumption bypass. |
| 930 | func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error { |
| 931 | rawCerts := make([][]byte, len(cs.PeerCertificates)) |
| 932 | for i, cert := range cs.PeerCertificates { |
| 933 | rawCerts[i] = cert.Raw |
| 934 | } |
| 935 | |
| 936 | // first use any pre-existing custom verification function |
| 937 | if clientauth.existingVerifyPeerCert != nil { |
| 938 | if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil { |
| 939 | return err |
| 940 | } |
| 941 | } |
| 942 | |
| 943 | // crypto/tls has already enforced whether the mode requires a |
| 944 | // certificate, so the verifiers only check one that was presented |
| 945 | if len(rawCerts) == 0 { |
| 946 | return nil |
| 947 | } |
| 948 | for _, verifier := range clientauth.verifiers { |
| 949 | if err := verifier.VerifyClientCertificate(rawCerts, cs.VerifiedChains); err != nil { |
| 950 | return err |
| 951 | } |
| 952 | } |
| 953 | return nil |
| 954 | } |
| 955 | |
| 956 | // decodeBase64DERCert base64-decodes, then DER-decodes, certStr. |
| 957 | func decodeBase64DERCert(certStr string) (*x509.Certificate, error) { |
| 958 | derBytes, err := base64.StdEncoding.DecodeString(certStr) |
| 959 | if err != nil { |
| 960 | return nil, err |
| 961 | } |
| 962 | return x509.ParseCertificate(derBytes) |
| 963 | } |
| 964 | |
| 965 | // setDefaultTLSParams sets the default TLS cipher suites, protocol versions, |
| 966 | // and server preferences of cfg if they are not already set; it does not |
| 967 | // overwrite values, only fills in missing values. |
| 968 | func setDefaultTLSParams(cfg *tls.Config) { |
| 969 | if len(cfg.CipherSuites) == 0 { |
| 970 | cfg.CipherSuites = getOptimalDefaultCipherSuites() |
| 971 | } |
| 972 | |
| 973 | // Not a cipher suite, but still important for mitigating protocol downgrade attacks |
| 974 | // (prepend since having it at end breaks http2 due to non-h2-approved suites before it) |
| 975 | cfg.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, cfg.CipherSuites...) |
| 976 | |
| 977 | if len(cfg.CurvePreferences) == 0 { |
| 978 | cfg.CurvePreferences = defaultCurves |
| 979 | } |
| 980 | |
| 981 | // crypto/tls docs: |
| 982 | // "If EncryptedClientHelloKeys is set, MinVersion, if set, must be VersionTLS13." |
| 983 | if cfg.EncryptedClientHelloKeys != nil && cfg.MinVersion != 0 && cfg.MinVersion < tls.VersionTLS13 { |
| 984 | cfg.MinVersion = tls.VersionTLS13 |
| 985 | } |
| 986 | } |
| 987 | |
| 988 | // LeafCertClientAuth verifies the client's leaf certificate. |
| 989 | type LeafCertClientAuth struct { |
| 990 | LeafCertificateLoadersRaw []json.RawMessage `json:"leaf_certs_loaders,omitempty" caddy:"namespace=tls.leaf_cert_loader inline_key=loader"` |
| 991 | trustedLeafCerts []*x509.Certificate |
| 992 | } |
| 993 | |
| 994 | // CaddyModule returns the Caddy module information. |
| 995 | func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo { |
| 996 | return caddy.ModuleInfo{ |
| 997 | ID: "tls.client_auth.verifier.leaf", |
| 998 | New: func() caddy.Module { return new(LeafCertClientAuth) }, |
| 999 | } |
| 1000 | } |
| 1001 | |
| 1002 | func (l *LeafCertClientAuth) Provision(ctx caddy.Context) error { |
| 1003 | if l.LeafCertificateLoadersRaw == nil { |
| 1004 | return nil |
| 1005 | } |
| 1006 | val, err := ctx.LoadModule(l, "LeafCertificateLoadersRaw") |
| 1007 | if err != nil { |
| 1008 | return fmt.Errorf("could not parse leaf certificates loaders: %s", err.Error()) |
| 1009 | } |
| 1010 | trustedLeafCertloaders := []LeafCertificateLoader{} |
| 1011 | for _, loader := range val.([]any) { |
| 1012 | trustedLeafCertloaders = append(trustedLeafCertloaders, loader.(LeafCertificateLoader)) |
| 1013 | } |
| 1014 | trustedLeafCertificates := []*x509.Certificate{} |
| 1015 | for _, loader := range trustedLeafCertloaders { |
| 1016 | certs, err := loader.LoadLeafCertificates() |
| 1017 | if err != nil { |
| 1018 | return fmt.Errorf("could not load leaf certificates: %s", err.Error()) |
| 1019 | } |
| 1020 | trustedLeafCertificates = append(trustedLeafCertificates, certs...) |
| 1021 | } |
| 1022 | l.trustedLeafCerts = trustedLeafCertificates |
| 1023 | return nil |
| 1024 | } |
| 1025 | |
| 1026 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. |
| 1027 | func (l *LeafCertClientAuth) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
| 1028 | d.NextArg() |
| 1029 | |
| 1030 | // accommodate the use of one-liners |
| 1031 | if d.CountRemainingArgs() > 1 { |
| 1032 | d.NextArg() |
| 1033 | modName := d.Val() |
| 1034 | mod, err := caddyfile.UnmarshalModule(d, "tls.leaf_cert_loader."+modName) |
| 1035 | if err != nil { |
| 1036 | return d.WrapErr(err) |
| 1037 | } |
| 1038 | vMod, ok := mod.(LeafCertificateLoader) |
| 1039 | if !ok { |
| 1040 | return fmt.Errorf("leaf module '%s' is not a leaf certificate loader", vMod) |
| 1041 | } |
| 1042 | l.LeafCertificateLoadersRaw = append( |
| 1043 | l.LeafCertificateLoadersRaw, |
| 1044 | caddyconfig.JSONModuleObject(vMod, "loader", modName, nil), |
| 1045 | ) |
| 1046 | return nil |
| 1047 | } |
| 1048 | |
| 1049 | // accommodate the use of nested blocks |
| 1050 | for nesting := d.Nesting(); d.NextBlock(nesting); { |
| 1051 | modName := d.Val() |
| 1052 | mod, err := caddyfile.UnmarshalModule(d, "tls.leaf_cert_loader."+modName) |
| 1053 | if err != nil { |
| 1054 | return d.WrapErr(err) |
| 1055 | } |
| 1056 | vMod, ok := mod.(LeafCertificateLoader) |
| 1057 | if !ok { |
| 1058 | return fmt.Errorf("leaf module '%s' is not a leaf certificate loader", vMod) |
| 1059 | } |
| 1060 | l.LeafCertificateLoadersRaw = append( |
| 1061 | l.LeafCertificateLoadersRaw, |
| 1062 | caddyconfig.JSONModuleObject(vMod, "loader", modName, nil), |
| 1063 | ) |
| 1064 | } |
| 1065 | return nil |
| 1066 | } |
| 1067 | |
| 1068 | func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { |
| 1069 | if len(rawCerts) == 0 { |
| 1070 | return fmt.Errorf("no client certificate provided") |
| 1071 | } |
| 1072 | |
| 1073 | remoteLeafCert, err := x509.ParseCertificate(rawCerts[0]) |
| 1074 | if err != nil { |
| 1075 | return fmt.Errorf("can't parse the given certificate: %s", err.Error()) |
| 1076 | } |
| 1077 | |
| 1078 | if slices.ContainsFunc(l.trustedLeafCerts, remoteLeafCert.Equal) { |
| 1079 | return nil |
| 1080 | } |
| 1081 | |
| 1082 | return fmt.Errorf("client leaf certificate failed validation") |
| 1083 | } |
| 1084 | |
| 1085 | // PublicKeyAlgorithm is a JSON-unmarshalable wrapper type. |
| 1086 | type PublicKeyAlgorithm x509.PublicKeyAlgorithm |
| 1087 | |
| 1088 | // UnmarshalJSON satisfies json.Unmarshaler. |
| 1089 | func (a *PublicKeyAlgorithm) UnmarshalJSON(b []byte) error { |
| 1090 | algoStr := strings.ToLower(strings.Trim(string(b), `"`)) |
| 1091 | algo, ok := publicKeyAlgorithms[algoStr] |
| 1092 | if !ok { |
| 1093 | return fmt.Errorf("unrecognized public key algorithm: %s (expected one of %v)", |
| 1094 | algoStr, publicKeyAlgorithms) |
| 1095 | } |
| 1096 | *a = PublicKeyAlgorithm(algo) |
| 1097 | return nil |
| 1098 | } |
| 1099 | |
| 1100 | // ConnectionMatcher is a type which matches TLS handshakes. |
| 1101 | type ConnectionMatcher interface { |
| 1102 | Match(*tls.ClientHelloInfo) bool |
| 1103 | } |
| 1104 | |
| 1105 | // LeafCertificateLoader is a type that loads the trusted leaf certificates |
| 1106 | // for the tls.leaf_cert_loader modules |
| 1107 | type LeafCertificateLoader interface { |
| 1108 | LoadLeafCertificates() ([]*x509.Certificate, error) |
| 1109 | } |
| 1110 | |
| 1111 | // ClientCertificateVerifier is a type which verifies client certificates. |
| 1112 | // It is called during verifyConnection in the TLS handshake, only when |
| 1113 | // the client presented a certificate. |
| 1114 | type ClientCertificateVerifier interface { |
| 1115 | VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error |
| 1116 | } |
| 1117 | |
| 1118 | var defaultALPN = []string{"h2", "http/1.1"} |
| 1119 | |
| 1120 | type destructableWriter struct{ *os.File } |
| 1121 | |
| 1122 | func (d destructableWriter) Destruct() error { return d.Close() } |
| 1123 | |
| 1124 | var secretsLogPool = caddy.NewUsagePool() |
| 1125 | |
| 1126 | // Interface guards |
| 1127 | var ( |
| 1128 | _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil) |
| 1129 | _ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil) |
| 1130 | _ caddyfile.Unmarshaler = (*LeafCertClientAuth)(nil) |
| 1131 | ) |
| 1132 | |
| 1133 | // ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested |
| 1134 | // matcher set, and returns its raw module map value. |
| 1135 | func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) { |
| 1136 | matcherMap := make(map[string]ConnectionMatcher) |
| 1137 | |
| 1138 | // in case there are multiple instances of the same matcher, concatenate |
| 1139 | // their tokens (we expect that UnmarshalCaddyfile should be able to |
| 1140 | // handle more than one segment); otherwise, we'd overwrite other |
| 1141 | // instances of the matcher in this set |
| 1142 | tokensByMatcherName := make(map[string][]caddyfile.Token) |
| 1143 | for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { |
| 1144 | matcherName := d.Val() |
| 1145 | tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) |
| 1146 | } |
| 1147 | |
| 1148 | for matcherName, tokens := range tokensByMatcherName { |
| 1149 | mod, err := caddy.GetModule("tls.handshake_match." + matcherName) |
| 1150 | if err != nil { |
| 1151 | return nil, d.Errf("getting matcher module '%s': %v", matcherName, err) |
| 1152 | } |
| 1153 | unm, ok := mod.New().(caddyfile.Unmarshaler) |
| 1154 | if !ok { |
| 1155 | return nil, d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) |
| 1156 | } |
| 1157 | err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens)) |
| 1158 | if err != nil { |
| 1159 | return nil, err |
| 1160 | } |
| 1161 | cm, ok := unm.(ConnectionMatcher) |
| 1162 | if !ok { |
| 1163 | return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName) |
| 1164 | } |
| 1165 | matcherMap[matcherName] = cm |
| 1166 | } |
| 1167 | |
| 1168 | matcherSet := make(caddy.ModuleMap) |
| 1169 | for name, matcher := range matcherMap { |
| 1170 | jsonBytes, err := json.Marshal(matcher) |
| 1171 | if err != nil { |
| 1172 | return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err) |
| 1173 | } |
| 1174 | matcherSet[name] = jsonBytes |
| 1175 | } |
| 1176 | |
| 1177 | return matcherSet, nil |
| 1178 | } |