mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-11-09 14:03:02 +09:00
936 lines
25 KiB
Go
936 lines
25 KiB
Go
package aghtls
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
|
"github.com/c2h5oh/datasize"
|
|
"github.com/google/go-cmp/cmp"
|
|
)
|
|
|
|
// DefaultManagerConfig is the configuration structure for [NewDefaultManager].
|
|
type DefaultManagerConfig struct {
|
|
// Logger is used for logging the operation of the manager. It must not be
|
|
// nil.
|
|
Logger *slog.Logger
|
|
|
|
// Watcher is used to watch the TLS certificate and key files. It must not
|
|
// be nil.
|
|
Watcher aghos.FSWatcher
|
|
|
|
// ExtendedTLSConfig is the initial extended TLS configuration. It may be
|
|
// nil.
|
|
ExtendedTLSConfig *ExtendedTLSConfig
|
|
|
|
// ServePlainDNS is used to determine whether to serve DNS over plain
|
|
// UDP/TCP.
|
|
ServePlainDNS bool
|
|
}
|
|
|
|
// DefaultManager is the default implementation of the [Manager] interface.
|
|
//
|
|
// TODO(e.burkov): Add tests.
|
|
type DefaultManager struct {
|
|
certLastMod time.Time
|
|
watcher aghos.FSWatcher
|
|
logger *slog.Logger
|
|
|
|
// mu protects tlsConf, extTLSConf, certLastMod, tlsCert, and pair.
|
|
mu *sync.Mutex
|
|
tlsConf *tls.Config
|
|
extTLSConf *ExtendedTLSConfig
|
|
tlsCert *tls.Certificate
|
|
rootCerts *x509.CertPool
|
|
updates chan UpdateSignal
|
|
pair TLSPair
|
|
customCipherIDs []uint16
|
|
}
|
|
|
|
// NewDefaultManager returns a new properly initialized default manager. conf
|
|
// must be non-nil and valid. mgr is always non-nil while any returned error
|
|
// indicates that the TLS configuration isn't valid. Thus TLS may be
|
|
// initialized later, e.g. via the web UI.
|
|
func NewDefaultManager(
|
|
ctx context.Context,
|
|
conf *DefaultManagerConfig,
|
|
) (mgr *DefaultManager, err error) {
|
|
mgr = &DefaultManager{
|
|
logger: conf.Logger,
|
|
mu: &sync.Mutex{},
|
|
pair: TLSPair{},
|
|
// Buffer the channel to avoid missing updates.
|
|
updates: make(chan UpdateSignal, 1),
|
|
watcher: conf.Watcher,
|
|
extTLSConf: &ExtendedTLSConfig{},
|
|
}
|
|
|
|
if conf.ExtendedTLSConfig != nil {
|
|
mgr.extTLSConf = conf.ExtendedTLSConfig
|
|
}
|
|
|
|
mgr.rootCerts = SystemRootCAs(ctx, conf.Logger)
|
|
mgr.extTLSConf.ServePlainDNS = conf.ServePlainDNS
|
|
mgr.extTLSConf.Status = TLSConfigStatus{}
|
|
|
|
mgr.parseCustomCiphers(ctx)
|
|
|
|
// There is no need to lock m.mu here, since the manager isn't shared with
|
|
// other goroutines yet.
|
|
if !mgr.extTLSConf.Enabled {
|
|
return mgr, nil
|
|
}
|
|
|
|
err = mgr.Set(ctx, TLSPair{
|
|
CertPath: mgr.extTLSConf.CertificatePath,
|
|
KeyPath: mgr.extTLSConf.PrivateKeyPath,
|
|
})
|
|
if err != nil {
|
|
mgr.logger.ErrorContext(ctx, "setting tls files", slogutil.KeyError, err)
|
|
}
|
|
|
|
err = mgr.prepareTLSConfig(ctx)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return mgr, err
|
|
}
|
|
|
|
return mgr, nil
|
|
}
|
|
|
|
// parseCustomCiphers parses the custom TLS ciphers specified in the extended
|
|
// TLS configuration and sets them in the TLS manager. If no custom ciphers are
|
|
// specified, it uses the default ciphers.
|
|
func (mgr *DefaultManager) parseCustomCiphers(ctx context.Context) {
|
|
if len(mgr.extTLSConf.OverrideTLSCiphers) == 0 {
|
|
mgr.logger.InfoContext(ctx, "using default ciphers")
|
|
|
|
return
|
|
}
|
|
|
|
var err error
|
|
mgr.customCipherIDs, err = ParseCiphers(mgr.extTLSConf.OverrideTLSCiphers)
|
|
if err != nil {
|
|
// Should not happen because upstreams are already validated. See
|
|
// [validateTLSCipherIDs].
|
|
panic(err)
|
|
}
|
|
|
|
mgr.logger.InfoContext(ctx, "overriding ciphers", "ciphers", mgr.extTLSConf.OverrideTLSCiphers)
|
|
}
|
|
|
|
// prepareTLSConfig prepares the TLS configuration for the mgr. It returns an
|
|
// error if the TLS configuration is invalid.
|
|
func (mgr *DefaultManager) prepareTLSConfig(ctx context.Context) (err error) {
|
|
err = LoadTLSConfig(ctx, mgr.logger, mgr, mgr.extTLSConf, &mgr.extTLSConf.Status)
|
|
if err != nil {
|
|
mgr.extTLSConf.Enabled = false
|
|
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
cert, err := tls.X509KeyPair(mgr.extTLSConf.CertificateChainData, mgr.extTLSConf.PrivateKeyData)
|
|
if err != nil {
|
|
// DNSCrypt provides its own certificate, meaning we can ignore TLS
|
|
// certificate parsing errors.
|
|
if mgr.extTLSConf.PortDNSCrypt != 0 && mgr.extTLSConf.DNSCryptConfigFile != "" {
|
|
mgr.logger.InfoContext(ctx, "dnscrypt is configured")
|
|
|
|
return nil
|
|
}
|
|
|
|
mgr.extTLSConf.Enabled = false
|
|
|
|
return fmt.Errorf("parsing tls certificate: %w", err)
|
|
}
|
|
|
|
slices.Sort(cert.Leaf.DNSNames)
|
|
|
|
mgr.tlsCert = &cert
|
|
mgr.setCertFileTime(ctx)
|
|
mgr.tlsConf = &tls.Config{
|
|
RootCAs: mgr.rootCerts,
|
|
CipherSuites: mgr.customCipherIDs,
|
|
GetCertificate: mgr.onGetCertificate,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// type check
|
|
var _ Manager = (*DefaultManager)(nil)
|
|
|
|
// Set implements the [Manager] interface for *DefaultManager.
|
|
func (mgr *DefaultManager) Set(ctx context.Context, certKey TLSPair) (err error) {
|
|
mgr.mu.Lock()
|
|
defer mgr.mu.Unlock()
|
|
|
|
return mgr.setLocked(ctx, certKey)
|
|
}
|
|
|
|
// setLocked sets the TLS certificate and key. mgr.mu must be locked.
|
|
func (mgr *DefaultManager) setLocked(ctx context.Context, certKey TLSPair) (err error) {
|
|
mgr.logger.DebugContext(ctx, "setting", "cert", certKey.CertPath, "key", certKey.KeyPath)
|
|
|
|
var errs []error
|
|
|
|
old := mgr.pair
|
|
|
|
errs = mgr.appendUnwatchErr(errs, "old cert", old.CertPath)
|
|
errs = mgr.appendUnwatchErr(errs, "old key", old.KeyPath)
|
|
errs = mgr.appendWatchErr(errs, "new cert", certKey.CertPath)
|
|
errs = mgr.appendWatchErr(errs, "new key", certKey.KeyPath)
|
|
|
|
mgr.pair = certKey
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// appendUnwatchErr stops watching a file at path p described by what and
|
|
// appends an error to the errs slice, if any. Empty p is ignored.
|
|
func (mgr *DefaultManager) appendUnwatchErr(errs []error, what, p string) (result []error) {
|
|
if p == "" {
|
|
return errs
|
|
}
|
|
|
|
err := mgr.watcher.Remove(p)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("unwatching %s %s: %w", what, p, err))
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// appendWatchErr starts watching a file at path p described by what and
|
|
// appends an error to the errs slice, if any. Empty p is ignored.
|
|
func (mgr *DefaultManager) appendWatchErr(errs []error, what, p string) (result []error) {
|
|
if p == "" {
|
|
return errs
|
|
}
|
|
|
|
err := mgr.watcher.Add(p)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("watching %s %s: %w", what, p, err))
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// Refresh implements the [service.Refresher] interface for *DefaultManager.
|
|
func (mgr *DefaultManager) Refresh(ctx context.Context) (err error) {
|
|
mgr.logger.DebugContext(ctx, "refreshing")
|
|
|
|
select {
|
|
case mgr.updates <- UpdateSignal{}:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("refreshing: %w", ctx.Err())
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Start implements the [service.Interface] interface for *DefaultManager.
|
|
func (mgr *DefaultManager) Start(ctx context.Context) (err error) {
|
|
err = mgr.watcher.Start(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("starting watcher: %w", err)
|
|
}
|
|
|
|
go mgr.handleEvents(ctx)
|
|
go mgr.handleCertFileChange(ctx)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown implements the [service.Interface] interface for *DefaultManager.
|
|
func (mgr *DefaultManager) Shutdown(ctx context.Context) (err error) {
|
|
defer close(mgr.updates)
|
|
|
|
err = mgr.watcher.Shutdown(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("shutting down watcher: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Updates implements the [Manager] interface for *DefaultManager.
|
|
func (mgr *DefaultManager) Updates(ctx context.Context) (updates <-chan UpdateSignal) {
|
|
return mgr.updates
|
|
}
|
|
|
|
// handleEvents handles changes of the tracked files. It is intended to be run
|
|
// in a separate goroutine.
|
|
func (mgr *DefaultManager) handleEvents(ctx context.Context) {
|
|
defer slogutil.RecoverAndLog(ctx, mgr.logger)
|
|
|
|
eventsCh := mgr.watcher.Events()
|
|
if eventsCh == nil {
|
|
mgr.logger.DebugContext(ctx, "watcher does not emit events")
|
|
|
|
return
|
|
}
|
|
|
|
for range eventsCh {
|
|
err := mgr.Refresh(ctx)
|
|
if err != nil {
|
|
mgr.logger.ErrorContext(ctx, "refreshing", slogutil.KeyError, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleCertFileChange handles changes in the certificate file. It's intended
|
|
// to be run as a goroutine.
|
|
func (mgr *DefaultManager) handleCertFileChange(ctx context.Context) {
|
|
defer slogutil.RecoverAndLog(ctx, mgr.logger)
|
|
|
|
updates := mgr.Updates(ctx)
|
|
if updates == nil {
|
|
mgr.logger.ErrorContext(ctx, "no updates channel")
|
|
|
|
return
|
|
}
|
|
|
|
for range updates {
|
|
mgr.logger.DebugContext(ctx, "reloading")
|
|
|
|
mgr.reload(ctx)
|
|
}
|
|
}
|
|
|
|
// reload updates the configuration and restarts the TLS manager. It logs any
|
|
// encountered errors.
|
|
//
|
|
// TODO(s.chzhen): Consider returning an error.
|
|
func (mgr *DefaultManager) reload(ctx context.Context) {
|
|
mgr.mu.Lock()
|
|
defer mgr.mu.Unlock()
|
|
|
|
tlsConfPtr := mgr.extTLSConf
|
|
|
|
if !tlsConfPtr.Enabled || len(tlsConfPtr.CertificatePath) == 0 {
|
|
return
|
|
}
|
|
|
|
certPath := tlsConfPtr.CertificatePath
|
|
fi, err := os.Stat(certPath)
|
|
if err != nil {
|
|
mgr.logger.ErrorContext(ctx, "checking certificate file", slogutil.KeyError, err)
|
|
|
|
return
|
|
}
|
|
|
|
if fi.ModTime().UTC().Equal(mgr.certLastMod) {
|
|
mgr.logger.InfoContext(ctx, "certificate file is not modified")
|
|
|
|
return
|
|
}
|
|
|
|
mgr.logger.InfoContext(ctx, "certificate file is modified")
|
|
|
|
extTLSConf := *tlsConfPtr
|
|
status := &TLSConfigStatus{}
|
|
|
|
err = LoadTLSConfig(ctx, mgr.logger, mgr, &extTLSConf, status)
|
|
if err != nil {
|
|
mgr.logger.WarnContext(ctx, "reloading interrupted", slogutil.KeyError, err)
|
|
|
|
return
|
|
}
|
|
|
|
err = mgr.updateTLSCert(&extTLSConf)
|
|
if err != nil {
|
|
mgr.logger.WarnContext(ctx, "failed to update tls certificate", slogutil.KeyError, err)
|
|
|
|
return
|
|
}
|
|
|
|
extTLSConf.Status = *status
|
|
|
|
mgr.extTLSConf = &extTLSConf
|
|
mgr.certLastMod = fi.ModTime().UTC()
|
|
}
|
|
|
|
// CipherSuites implements the [Manager] interface for *DefaultManager. It
|
|
// returns the list of TLS cipher suite IDs.
|
|
func (mgr *DefaultManager) CipherSuites() (cs []uint16) {
|
|
return mgr.customCipherIDs
|
|
}
|
|
|
|
// TLSConfig implements the [Manager] interface for *DefaultManager.
|
|
func (mgr *DefaultManager) TLSConfig() (conf *tls.Config) {
|
|
mgr.mu.Lock()
|
|
defer mgr.mu.Unlock()
|
|
|
|
return mgr.tlsConf.Clone()
|
|
}
|
|
|
|
// RootCAs implements the [Manager] interface for *DefaultManager.
|
|
func (mgr *DefaultManager) RootCAs() (root *x509.CertPool) {
|
|
return mgr.rootCerts
|
|
}
|
|
|
|
// HasIPAddrs implements the [Manager] interface for *DefaultManager. It
|
|
// returns true if the current TLS configuration has at least one certificate
|
|
// with an IP address in its SAN extension.
|
|
func (mgr *DefaultManager) HasIPAddrs() (ok bool) {
|
|
mgr.mu.Lock()
|
|
defer mgr.mu.Unlock()
|
|
|
|
if mgr.tlsCert == nil || mgr.tlsCert.Leaf == nil {
|
|
return false
|
|
}
|
|
|
|
return CertificateHasIP(mgr.tlsCert.Leaf)
|
|
}
|
|
|
|
// ExtendedTLSConfig implements the [Manager] provider interface for
|
|
// *DefaultManager. It returns a deep copy of the stored extended TLS
|
|
// configuration.
|
|
func (mgr *DefaultManager) ExtendedTLSConfig() (extTLSConf *ExtendedTLSConfig) {
|
|
mgr.mu.Lock()
|
|
defer mgr.mu.Unlock()
|
|
|
|
return mgr.extTLSConf.Clone()
|
|
}
|
|
|
|
// SetExtendedTLSConfig implements the [Manager] interface for *DefaultManager.
|
|
// It updates the TLS configuration with the given one. newConf must not be
|
|
// nil. newConf is always modified. If restartHTTPS is true, the HTTPS server
|
|
// must be restarted. If error is not nil, restartHTTPS cannot be true.
|
|
func (mgr *DefaultManager) SetExtendedTLSConfig(
|
|
ctx context.Context,
|
|
servePlainDNS aghalg.NullBool,
|
|
newConf *ExtendedTLSConfig,
|
|
) (restartHTTPS bool, err error) {
|
|
mgr.mu.Lock()
|
|
defer mgr.mu.Unlock()
|
|
|
|
err = mgr.updateTLSCert(newConf)
|
|
if err != nil {
|
|
mgr.logger.ErrorContext(ctx, "updating tls certificate", slogutil.KeyError, err)
|
|
|
|
// Don't wrap the error, because it is informative enough as is.
|
|
return false, err
|
|
}
|
|
|
|
if servePlainDNS != aghalg.NBNull {
|
|
newConf.ServePlainDNS = servePlainDNS == aghalg.NBTrue
|
|
} else {
|
|
newConf.ServePlainDNS = mgr.extTLSConf.ServePlainDNS
|
|
}
|
|
|
|
if !setPrivateFieldsAndCompare(mgr.extTLSConf, newConf) {
|
|
mgr.logger.InfoContext(ctx, "config has changed, restarting https server")
|
|
restartHTTPS = true
|
|
} else {
|
|
mgr.logger.InfoContext(ctx, "config has not changed")
|
|
}
|
|
|
|
mgr.extTLSConf = newConf
|
|
|
|
certPath, keyPath := "", ""
|
|
if newConf.Enabled {
|
|
certPath, keyPath = newConf.CertificatePath, newConf.PrivateKeyPath
|
|
}
|
|
|
|
err = mgr.setLocked(ctx, TLSPair{
|
|
CertPath: certPath,
|
|
KeyPath: keyPath,
|
|
})
|
|
if err != nil {
|
|
mgr.logger.ErrorContext(ctx, "setting tls files", slogutil.KeyError, err)
|
|
}
|
|
|
|
mgr.setCertFileTime(ctx)
|
|
|
|
return restartHTTPS, nil
|
|
}
|
|
|
|
// updateTLSCert loads and updates a TLS certificate for m.tlsConf. If
|
|
// m.tlsConf is nil, it will be initialized. extTLSConf must not be nil. m.mu
|
|
// must be locked.
|
|
func (mgr *DefaultManager) updateTLSCert(extTLSConf *ExtendedTLSConfig) (err error) {
|
|
if len(extTLSConf.CertificateChainData) == 0 || len(extTLSConf.PrivateKeyData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
cert, err := tls.X509KeyPair(extTLSConf.CertificateChainData, extTLSConf.PrivateKeyData)
|
|
if err != nil {
|
|
return fmt.Errorf("loading tls certificate: %w", err)
|
|
}
|
|
|
|
slices.Sort(cert.Leaf.DNSNames)
|
|
|
|
if mgr.tlsConf == nil {
|
|
mgr.tlsConf = &tls.Config{
|
|
RootCAs: mgr.rootCerts,
|
|
CipherSuites: mgr.customCipherIDs,
|
|
MinVersion: tls.VersionTLS12,
|
|
GetCertificate: mgr.onGetCertificate,
|
|
}
|
|
}
|
|
|
|
mgr.tlsCert = &cert
|
|
|
|
return nil
|
|
}
|
|
|
|
// onGetCertificate gets [*tls.Certificate] from [*tls.Config]. If
|
|
// [DefaultManager.extTLSConf.Enabled] is false, nil is returned.
|
|
//
|
|
// TODO(m.kazantsev): Consider using tls.SupportsCertificate.
|
|
func (mgr *DefaultManager) onGetCertificate(
|
|
chi *tls.ClientHelloInfo) (cert *tls.Certificate, err error,
|
|
) {
|
|
mgr.mu.Lock()
|
|
defer mgr.mu.Unlock()
|
|
|
|
if !mgr.extTLSConf.Enabled || mgr.tlsConf == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
tlsCert := *mgr.tlsCert
|
|
|
|
return &tlsCert, nil
|
|
}
|
|
|
|
// setPrivateFieldsAndCompare sets any missing properties in conf to match those
|
|
// in c and returns true if TLS configurations are equal. conf must not be nil.
|
|
// It sets the following properties because these are not accepted from the
|
|
// frontend:
|
|
//
|
|
// [ExtendedTLSConfig.DNSCryptConfigFile]
|
|
// [ExtendedTLSConfig.OverrideTLSCiphers]
|
|
// [ExtendedTLSConfig.PortDNSCrypt]
|
|
func setPrivateFieldsAndCompare(
|
|
currentTLSConf *ExtendedTLSConfig,
|
|
newTLSConf *ExtendedTLSConfig,
|
|
) (equal bool) {
|
|
newTLSConf.OverrideTLSCiphers = slices.Clone(currentTLSConf.OverrideTLSCiphers)
|
|
|
|
newTLSConf.DNSCryptConfigFile = currentTLSConf.DNSCryptConfigFile
|
|
newTLSConf.PortDNSCrypt = currentTLSConf.PortDNSCrypt
|
|
|
|
// TODO(a.garipov): Define a custom comparer.
|
|
return cmp.Equal(currentTLSConf, newTLSConf)
|
|
}
|
|
|
|
// setCertFileTime sets [tlsManager.certLastMod] from the certificate. If there
|
|
// are errors, setCertFileTime logs them. m.mu is expected to be locked.
|
|
func (mgr *DefaultManager) setCertFileTime(ctx context.Context) {
|
|
if mgr.extTLSConf.CertificatePath == "" {
|
|
return
|
|
}
|
|
|
|
fi, err := os.Stat(mgr.extTLSConf.CertificatePath)
|
|
if err != nil {
|
|
mgr.logger.ErrorContext(ctx, "looking up certificate path", slogutil.KeyError, err)
|
|
|
|
return
|
|
}
|
|
|
|
mgr.certLastMod = fi.ModTime().UTC()
|
|
}
|
|
|
|
// LoadTLSConfig loads and validates the TLS configuration. It also sets
|
|
// [aghtls.ExtendedTLSConfig.CertificateChainData] and
|
|
// [aghtls.ExtendedTLSConfig.PrivateKeyData] properties. The returned error is
|
|
// also set in [aghtls.TLSConfigStatus.WarningValidation]. All arguments must
|
|
// not be nil.
|
|
func LoadTLSConfig(
|
|
ctx context.Context,
|
|
logger *slog.Logger,
|
|
tlsConfProvider Manager,
|
|
extTLSConf *ExtendedTLSConfig,
|
|
status *TLSConfigStatus,
|
|
) (err error) {
|
|
defer func() {
|
|
err = checkIfValidStatus(ctx, logger, status, err)
|
|
}()
|
|
|
|
err = loadCertificateChainData(extTLSConf)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
err = loadPrivateKeyData(extTLSConf)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
err = validateCertificates(
|
|
ctx,
|
|
logger,
|
|
tlsConfProvider,
|
|
status,
|
|
extTLSConf.CertificateChainData,
|
|
extTLSConf.PrivateKeyData,
|
|
extTLSConf.ServerName,
|
|
)
|
|
|
|
return errors.Annotate(err, "validating certificate pair: %w")
|
|
}
|
|
|
|
// checkIfValidStatus checks if status is valid. If it is valid, certErr is set
|
|
// to nil. Otherwise, certErr is returned as is. logger and status must not be
|
|
// nil.
|
|
func checkIfValidStatus(
|
|
ctx context.Context,
|
|
logger *slog.Logger,
|
|
status *TLSConfigStatus,
|
|
certErr error,
|
|
) (err error) {
|
|
if certErr == nil {
|
|
return nil
|
|
}
|
|
|
|
status.WarningValidation = certErr.Error()
|
|
if status.ValidCert && status.ValidKey && status.ValidPair {
|
|
// Do not return warnings since those aren't critical, just log.
|
|
logger.WarnContext(
|
|
ctx,
|
|
"error while loading tls configuration",
|
|
slogutil.KeyError, certErr,
|
|
)
|
|
|
|
certErr = nil
|
|
}
|
|
|
|
return certErr
|
|
}
|
|
|
|
// loadCertificateChainData loads PEM-encoded certificates chain data to the
|
|
// TLS configuration. tlsConf must be not nil. tlsConf.CertificateChainData
|
|
// struct field will be modified in case tlsConfig.CertificatePath is not an
|
|
// empty string. extTLSConf must not be nil.
|
|
func loadCertificateChainData(extTLSConf *ExtendedTLSConfig) (err error) {
|
|
extTLSConf.CertificateChainData = []byte(extTLSConf.CertificateChain)
|
|
if extTLSConf.CertificatePath != "" {
|
|
if extTLSConf.CertificateChain != "" {
|
|
return errors.Error("certificate data and file can't be set together")
|
|
}
|
|
|
|
extTLSConf.CertificateChainData, err = os.ReadFile(extTLSConf.CertificatePath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading cert file: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadPrivateKeyData loads PEM-encoded private key data to the TLS
|
|
// configuration. tlsConf must be not nil. tlsConf.PrivateKeyData struct field
|
|
// will be modified in case tlsConfig.PrivateKeyPath is not an empty string.
|
|
// extTLSConf must not be nil.
|
|
func loadPrivateKeyData(extTLSConf *ExtendedTLSConfig) (err error) {
|
|
extTLSConf.PrivateKeyData = []byte(extTLSConf.PrivateKey)
|
|
if extTLSConf.PrivateKeyPath != "" {
|
|
if extTLSConf.PrivateKey != "" {
|
|
return errors.Error("private key data and file can't be set together")
|
|
}
|
|
|
|
extTLSConf.PrivateKeyData, err = os.ReadFile(extTLSConf.PrivateKeyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading key file: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateCertChain verifies certs using the first as the main one and others
|
|
// as intermediate. srvName stands for the expected DNS name. certs must not
|
|
// be empty. logger must not be nil.
|
|
func validateCertChain(
|
|
ctx context.Context,
|
|
logger *slog.Logger,
|
|
rootCAs *x509.CertPool,
|
|
certs []*x509.Certificate,
|
|
srvName string,
|
|
) (err error) {
|
|
main, others := certs[0], certs[1:]
|
|
|
|
pool := x509.NewCertPool()
|
|
for _, cert := range others {
|
|
pool.AddCert(cert)
|
|
}
|
|
|
|
othersLen := len(others)
|
|
if othersLen > 0 {
|
|
logger.InfoContext(
|
|
ctx,
|
|
"verifying certificate chain: got an intermediate cert",
|
|
"num", othersLen,
|
|
)
|
|
}
|
|
|
|
opts := x509.VerifyOptions{
|
|
DNSName: srvName,
|
|
Roots: rootCAs,
|
|
Intermediates: pool,
|
|
}
|
|
_, err = main.Verify(opts)
|
|
if err != nil {
|
|
return fmt.Errorf("certificate does not verify: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// errNoIPInCert is the error that is returned from [parseCertChain]
|
|
// if the leaf certificate doesn't contain IPs.
|
|
const errNoIPInCert errors.Error = `certificates has no IP addresses; ` +
|
|
`DNS-over-TLS won't be advertised via DDR`
|
|
|
|
// parseCertChain parses the certificate chain from raw data, and returns it.
|
|
// If ok is true, the returned error, if any, is not critical. logger must not
|
|
// be nil.
|
|
func parseCertChain(
|
|
ctx context.Context,
|
|
logger *slog.Logger,
|
|
chain []byte,
|
|
) (parsedCerts []*x509.Certificate, ok bool, err error) {
|
|
logger.DebugContext(ctx, "parsing certificate chain", "size", datasize.ByteSize(len(chain)))
|
|
|
|
var certs []*pem.Block
|
|
for decoded, pemblock := pem.Decode(chain); decoded != nil; {
|
|
if decoded.Type == "CERTIFICATE" {
|
|
certs = append(certs, decoded)
|
|
}
|
|
|
|
decoded, pemblock = pem.Decode(pemblock)
|
|
}
|
|
|
|
parsedCerts, err = parsePEMCerts(certs)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
logger.InfoContext(ctx, "parsing multiple pem certificates", "num", len(parsedCerts))
|
|
|
|
if !CertificateHasIP(parsedCerts[0]) {
|
|
err = errNoIPInCert
|
|
}
|
|
|
|
return parsedCerts, true, err
|
|
}
|
|
|
|
// parsePEMCerts parses multiple PEM-encoded certificates.
|
|
func parsePEMCerts(certs []*pem.Block) (parsedCerts []*x509.Certificate, err error) {
|
|
for i, cert := range certs {
|
|
var parsed *x509.Certificate
|
|
parsed, err = x509.ParseCertificate(cert.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing certificate at index %d: %w", i, err)
|
|
}
|
|
|
|
parsedCerts = append(parsedCerts, parsed)
|
|
}
|
|
|
|
if len(parsedCerts) == 0 {
|
|
return nil, errors.Error("empty certificate")
|
|
}
|
|
|
|
return parsedCerts, nil
|
|
}
|
|
|
|
// validatePKey validates the private key, returning its type. It returns an
|
|
// empty string if error occurs.
|
|
func validatePKey(pkey []byte) (keyType string, err error) {
|
|
var key *pem.Block
|
|
|
|
// Go through all pem blocks, but take first valid pem block and drop the
|
|
// rest.
|
|
for decoded, pemblock := pem.Decode([]byte(pkey)); decoded != nil; {
|
|
if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") {
|
|
key = decoded
|
|
|
|
break
|
|
}
|
|
|
|
decoded, pemblock = pem.Decode(pemblock)
|
|
}
|
|
|
|
if key == nil {
|
|
return "", errors.Error("no valid keys were found")
|
|
}
|
|
|
|
_, keyType, err = parsePrivateKey(key.Bytes)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing private key: %w", err)
|
|
}
|
|
|
|
if keyType == keyTypeED25519 {
|
|
return "", errors.Error(
|
|
"ED25519 keys are not supported by browsers; " +
|
|
"did you mean to use X25519 for key exchange?",
|
|
)
|
|
}
|
|
|
|
return keyType, nil
|
|
}
|
|
|
|
// validateCertificates processes certificate data and its private key. status
|
|
// must not be nil, since it's used to accumulate the validation results.
|
|
// logger and tlsManager must not be nil. Other parameters are optional.
|
|
func validateCertificates(
|
|
ctx context.Context,
|
|
logger *slog.Logger,
|
|
tlsManager Manager,
|
|
status *TLSConfigStatus,
|
|
certChain []byte,
|
|
pkey []byte,
|
|
serverName string,
|
|
) (err error) {
|
|
// Check only the public certificate separately from the key.
|
|
if len(certChain) > 0 {
|
|
var ok bool
|
|
ok, err = validateCertificate(
|
|
ctx,
|
|
logger,
|
|
tlsManager.RootCAs(),
|
|
status,
|
|
certChain,
|
|
serverName,
|
|
)
|
|
if !ok {
|
|
// Don't wrap the error, since it's informative enough as is.
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Validate the private key by parsing it.
|
|
if len(pkey) > 0 {
|
|
var keyErr error
|
|
status.KeyType, keyErr = validatePKey(pkey)
|
|
if keyErr != nil {
|
|
// Don't wrap the error, since it's informative enough as is.
|
|
return keyErr
|
|
}
|
|
|
|
// Set status.ValidKey to true to signal the frontend that the
|
|
// key is valid.
|
|
status.ValidKey = true
|
|
}
|
|
|
|
// If both are set, validate together.
|
|
if len(certChain) > 0 && len(pkey) > 0 {
|
|
_, pairErr := tls.X509KeyPair(certChain, pkey)
|
|
if pairErr != nil {
|
|
return fmt.Errorf("certificate-key pair: %w", pairErr)
|
|
}
|
|
|
|
status.ValidPair = true
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// validateCertificate processes certificate data. status must not be nil, as
|
|
// it is used to accumulate the validation results. logger and rootCAs must not
|
|
// be nil. Other parameters are optional. If ok is true, the returned error, if
|
|
// any, is not critical.
|
|
func validateCertificate(
|
|
ctx context.Context,
|
|
logger *slog.Logger,
|
|
rootCAs *x509.CertPool,
|
|
status *TLSConfigStatus,
|
|
certChain []byte,
|
|
serverName string,
|
|
) (ok bool, err error) {
|
|
// parseErr is a non-critical parse warning.
|
|
var parseErr error
|
|
var certs []*x509.Certificate
|
|
|
|
// Set status.ValidCert to true to signal the frontend that the
|
|
// certificate opens successfully and certificate chain is valid.
|
|
certs, status.ValidCert, parseErr = parseCertChain(ctx, logger, certChain)
|
|
if !status.ValidCert {
|
|
// Don't wrap the error, since it's informative enough as is.
|
|
return false, parseErr
|
|
}
|
|
|
|
mainCert := certs[0]
|
|
status.Subject = mainCert.Subject.String()
|
|
status.Issuer = mainCert.Issuer.String()
|
|
status.NotAfter = mainCert.NotAfter
|
|
status.NotBefore = mainCert.NotBefore
|
|
status.DNSNames = mainCert.DNSNames
|
|
|
|
err = validateCertChain(ctx, logger, rootCAs, certs, serverName)
|
|
if err != nil {
|
|
// Let self-signed certs through and don't return this error to set
|
|
// its message into the status.WarningValidation afterwards.
|
|
return true, err
|
|
}
|
|
|
|
status.ValidChain = true
|
|
|
|
// Propagate the non-critical parse warning.
|
|
return true, parseErr
|
|
}
|
|
|
|
// Key types.
|
|
const (
|
|
keyTypeECDSA = "ECDSA"
|
|
keyTypeED25519 = "ED25519"
|
|
keyTypeRSA = "RSA"
|
|
)
|
|
|
|
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
|
|
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
|
|
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
|
//
|
|
// TODO(a.garipov): Find out if this version of parsePrivateKey from the stdlib
|
|
// is actually necessary.
|
|
func parsePrivateKey(der []byte) (key crypto.PrivateKey, typ string, err error) {
|
|
if key, err = x509.ParsePKCS1PrivateKey(der); err == nil {
|
|
return key, keyTypeRSA, nil
|
|
}
|
|
|
|
if key, err = x509.ParsePKCS8PrivateKey(der); err == nil {
|
|
switch key := key.(type) {
|
|
case *rsa.PrivateKey:
|
|
return key, keyTypeRSA, nil
|
|
case *ecdsa.PrivateKey:
|
|
return key, keyTypeECDSA, nil
|
|
case ed25519.PrivateKey:
|
|
return key, keyTypeED25519, nil
|
|
default:
|
|
return nil, "", fmt.Errorf(
|
|
"tls: found unknown private key type %T in PKCS#8 wrapping",
|
|
key,
|
|
)
|
|
}
|
|
}
|
|
|
|
if key, err = x509.ParseECPrivateKey(der); err == nil {
|
|
return key, keyTypeECDSA, nil
|
|
}
|
|
|
|
return nil, "", errors.Error("tls: failed to parse private key")
|
|
}
|