package controlplane import ( "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "errors" "fmt" "net/http" "os" "strings" ) func newTLSConfig(config ServerConfig) (*tls.Config, error) { if (config.TLSCertFile == "") != (config.TLSKeyFile == "") { return nil, errors.New("TLS cert and key must be configured together") } if config.TLSCertFile == "" { if config.MTLSClientCAFile != "" || config.MTLSRequireNodeCert || config.MTLSRevokedCertsFile != "" { return nil, errors.New("mTLS settings require TLS cert and key") } return nil, nil } certificate, err := tls.LoadX509KeyPair(config.TLSCertFile, config.TLSKeyFile) if err != nil { return nil, fmt.Errorf("load TLS certificate: %w", err) } result := &tls.Config{ MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, } if config.MTLSClientCAFile == "" { if config.MTLSRequireNodeCert || config.MTLSRevokedCertsFile != "" { return nil, errors.New("mTLS client CA is required for node certificates or revocation") } return result, nil } caBytes, err := os.ReadFile(config.MTLSClientCAFile) if err != nil { return nil, fmt.Errorf("read mTLS client CA: %w", err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(caBytes) { return nil, errors.New("mTLS client CA does not contain a PEM certificate") } result.ClientCAs = pool // Web users may use HTTPS without a client certificate. Node routes enforce // a certificate separately, so one TLS listener serves both audiences. result.ClientAuth = tls.VerifyClientCertIfGiven if config.MTLSRevokedCertsFile != "" { if _, err := os.Stat(config.MTLSRevokedCertsFile); err != nil { return nil, fmt.Errorf("check revoked certificate file: %w", err) } } return result, nil } func (s *Server) clientCertificateAllowed(r *http.Request) bool { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { return !s.config.MTLSRequireNodeCert } if s.config.MTLSRevokedCertsFile == "" { return true } revoked, err := readRevokedFingerprints(s.config.MTLSRevokedCertsFile) if err != nil { return false } fingerprint := sha256.Sum256(r.TLS.PeerCertificates[0].Raw) _, isRevoked := revoked[hex.EncodeToString(fingerprint[:])] return !isRevoked } func readRevokedFingerprints(path string) (map[string]struct{}, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } result := make(map[string]struct{}) for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(strings.SplitN(line, "#", 2)[0]) if line == "" { continue } line = strings.ReplaceAll(strings.ToLower(line), ":", "") decoded, err := hex.DecodeString(line) if err != nil || len(decoded) != sha256.Size { return nil, errors.New("revoked certificate list contains an invalid SHA-256 fingerprint") } result[line] = struct{}{} } return result, nil }