95 lines
2.8 KiB
Go
95 lines
2.8 KiB
Go
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
|
|
}
|