knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/network/tls/config.go (about)

     1  /*
     2  Copyright 2026 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tls
    18  
    19  import (
    20  	cryptotls "crypto/tls"
    21  	"fmt"
    22  	"os"
    23  	"strings"
    24  )
    25  
    26  // Environment variable name suffixes for TLS configuration.
    27  // Use with a prefix to namespace them, e.g. "WEBHOOK_" + MinVersionEnvKey
    28  // reads the WEBHOOK_TLS_MIN_VERSION variable.
    29  const (
    30  	MinVersionEnvKey       = "TLS_MIN_VERSION"
    31  	MaxVersionEnvKey       = "TLS_MAX_VERSION"
    32  	CipherSuitesEnvKey     = "TLS_CIPHER_SUITES"
    33  	CurvePreferencesEnvKey = "TLS_CURVE_PREFERENCES"
    34  )
    35  
    36  // DefaultConfigFromEnv returns a tls.Config with secure defaults.
    37  // The prefix is prepended to each standard env-var suffix;
    38  // for example with prefix "WEBHOOK_" the function reads
    39  // WEBHOOK_TLS_MIN_VERSION, WEBHOOK_TLS_MAX_VERSION, etc.
    40  func DefaultConfigFromEnv(prefix string) (*cryptotls.Config, error) {
    41  	cfg := &cryptotls.Config{
    42  		MinVersion: cryptotls.VersionTLS13,
    43  	}
    44  
    45  	if v := os.Getenv(prefix + MinVersionEnvKey); v != "" {
    46  		ver, err := parseVersion(v)
    47  		if err != nil {
    48  			return nil, fmt.Errorf("invalid %s%s %q: %w", prefix, MinVersionEnvKey, v, err)
    49  		}
    50  		cfg.MinVersion = ver
    51  	}
    52  
    53  	if v := os.Getenv(prefix + MaxVersionEnvKey); v != "" {
    54  		ver, err := parseVersion(v)
    55  		if err != nil {
    56  			return nil, fmt.Errorf("invalid %s%s %q: %w", prefix, MaxVersionEnvKey, v, err)
    57  		}
    58  		cfg.MaxVersion = ver
    59  	}
    60  
    61  	if v := os.Getenv(prefix + CipherSuitesEnvKey); v != "" {
    62  		suites, err := parseCipherSuites(v)
    63  		if err != nil {
    64  			return nil, fmt.Errorf("invalid %s%s: %w", prefix, CipherSuitesEnvKey, err)
    65  		}
    66  		cfg.CipherSuites = suites
    67  	}
    68  
    69  	if v := os.Getenv(prefix + CurvePreferencesEnvKey); v != "" {
    70  		curves, err := parseCurvePreferences(v)
    71  		if err != nil {
    72  			return nil, fmt.Errorf("invalid %s%s: %w", prefix, CurvePreferencesEnvKey, err)
    73  		}
    74  		cfg.CurvePreferences = curves
    75  	}
    76  
    77  	return cfg, nil
    78  }
    79  
    80  // parseVersion converts a TLS version string to the corresponding
    81  // crypto/tls constant. Accepted values are "1.2" and "1.3".
    82  func parseVersion(v string) (uint16, error) {
    83  	switch v {
    84  	case "1.2":
    85  		return cryptotls.VersionTLS12, nil
    86  	case "1.3":
    87  		return cryptotls.VersionTLS13, nil
    88  	default:
    89  		return 0, fmt.Errorf("unsupported TLS version %q: must be %q or %q", v, "1.2", "1.3")
    90  	}
    91  }
    92  
    93  // parseCipherSuites parses a comma-separated list of TLS cipher-suite names
    94  // (e.g. "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")
    95  // into a slice of cipher-suite IDs. Names must match those returned by
    96  // crypto/tls.CipherSuiteName.
    97  func parseCipherSuites(s string) ([]uint16, error) {
    98  	lookup := cipherSuiteLookup()
    99  	parts := strings.Split(s, ",")
   100  	suites := make([]uint16, 0, len(parts))
   101  
   102  	for _, name := range parts {
   103  		name = strings.TrimSpace(name)
   104  		if name == "" {
   105  			continue
   106  		}
   107  		id, ok := lookup[name]
   108  		if !ok {
   109  			return nil, fmt.Errorf("unknown cipher suite %q", name)
   110  		}
   111  		suites = append(suites, id)
   112  	}
   113  
   114  	return suites, nil
   115  }
   116  
   117  // parseCurvePreferences parses a comma-separated list of elliptic-curve names
   118  // (e.g. "X25519,CurveP256") into a slice of crypto/tls.CurveID values.
   119  // Both Go constant names (CurveP256) and standard names (P-256) are accepted.
   120  func parseCurvePreferences(s string) ([]cryptotls.CurveID, error) {
   121  	parts := strings.Split(s, ",")
   122  	curves := make([]cryptotls.CurveID, 0, len(parts))
   123  
   124  	for _, name := range parts {
   125  		name = strings.TrimSpace(name)
   126  		if name == "" {
   127  			continue
   128  		}
   129  		id, ok := curvesByName[name]
   130  		if !ok {
   131  			return nil, fmt.Errorf("unknown curve %q", name)
   132  		}
   133  		curves = append(curves, id)
   134  	}
   135  
   136  	return curves, nil
   137  }
   138  
   139  func cipherSuiteLookup() map[string]uint16 {
   140  	m := make(map[string]uint16)
   141  	for _, cs := range cryptotls.CipherSuites() {
   142  		m[cs.Name] = cs.ID
   143  	}
   144  	return m
   145  }
   146  
   147  var curvesByName = map[string]cryptotls.CurveID{
   148  	"CurveP256":      cryptotls.CurveP256,
   149  	"CurveP384":      cryptotls.CurveP384,
   150  	"CurveP521":      cryptotls.CurveP521,
   151  	"X25519":         cryptotls.X25519,
   152  	"X25519MLKEM768": cryptotls.X25519MLKEM768,
   153  	"P-256":          cryptotls.CurveP256,
   154  	"P-384":          cryptotls.CurveP384,
   155  	"P-521":          cryptotls.CurveP521,
   156  }