diff --git a/config.go b/config.go index c335eaa..d76841d 100644 --- a/config.go +++ b/config.go @@ -1,10 +1,14 @@ package main import ( + "crypto/x509" "fmt" "io/ioutil" "os" + "os/user" + "strconv" + valid "github.com/asaskevich/govalidator" "gopkg.in/yaml.v2" ) @@ -78,27 +82,218 @@ type ( } ) +// Helper function that checks whether a string corresponds to a group name. +func isValidGroup(name string) bool { + group, err := user.LookupGroup(name) + if err != nil { + return false + } + _, err = strconv.Atoi(group.Gid) + return err == nil +} + +// Helper function that checks whether a string corresponds to a user name. +func isValidUser(name string) bool { + user, err := user.Lookup(name) + if err != nil { + return false + } + _, err = strconv.Atoi(user.Uid) + return err == nil +} + +// Validate the UNIX socket configuration +func (c *tSocketConfig) Validate() error { + if c.Path == "" { + return fmt.Errorf("Missing socket path.") + } + if !valid.IsUnixFilePath(c.Path) { + return fmt.Errorf("Socket path '%s' is invalid.", c.Path) + } + if c.Group != "" && !isValidGroup(c.Group) { + return fmt.Errorf("Invalid group '%s'", c.Group) + } + return nil +} + +// Check the LDAP structure configuration. +func (c *tLdapStructureConfig) Validate() error { + if c.EndEntityCertificate == "" { + return fmt.Errorf("Missing end entity certificate attribute name.") + } + if c.CACertificate == "" { + return fmt.Errorf("Missing CA certificate attribute name.") + } + return nil +} + +// Check the TLS field in LDAP configuration entries. If no port is specified, +// default it based on the TLS field. +func (c *tLdapConnectionConfig) Validate() error { + if c.TLS != "yes" && c.TLS != "starttls" && c.TLS != "no" { + return fmt.Errorf("Invalid TLS mode '%s' (valid values are 'yes', 'starttls' and 'no'", c.TLS) + } + if c.CaChain != "" { + data, err := ioutil.ReadFile(c.CaChain) + if err != nil { + return fmt.Errorf("Unable to read CA chain from '%s': %w", c.CaChain, err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(data) { + return fmt.Errorf("Could not parse CA chain PEM from '%s'.", c.CaChain) + } + } + return nil +} + +// Copy defaults into a LDAP server configuration entry. +func (c *tLdapServerConfig) ApplyDefaults(dft tLdapConnectionConfig) { + if c.Port == 0 { + c.Port = dft.Port + } + if c.TLS == "" { + c.TLS = dft.TLS + } + // FIXME: I have no clue how I should handle TLSNoVerify + if c.CaChain == "" { + c.CaChain = dft.CaChain + } + if c.BindUser == "" { + c.BindUser = dft.BindUser + } + if c.BindPassword == "" { + c.BindPassword = dft.BindPassword + } + + // Default port based on TLS mode + if c.Port == 0 { + if c.TLS == "starttls" { + c.Port = 636 + } else { + c.Port = 389 + } + } +} + +// Validate a LDAP server configuration record. +func (c *tLdapServerConfig) Validate() error { + if c.Host == "" { + return fmt.Errorf("No host name in LDAP server configuration.") + } + if !valid.IsHost(c.Host) { + return fmt.Errorf("Invalid host name '%s'", c.Host) + } + return c.tLdapConnectionConfig.Validate() +} + +// Validate the LDAP configuration +func (c *tLdapConfig) Validate() error { + err := c.Structure.Validate() + if err != nil { + return err + } + err = c.Defaults.Validate() + if err != nil { + return err + } + if len(c.Servers) == 0 { + return fmt.Errorf("No LDAP servers have been configured.") + } + for i := range c.Servers { + c.Servers[i].ApplyDefaults(c.Defaults) + err = c.Servers[i].Validate() + if err != nil { + return err + } + } + return nil +} + +// Check that a list of files contains only valid paths +func checkFileList(files []string) error { + for _, path := range files { + if !valid.IsUnixFilePath(path) { + return fmt.Errorf("Invalid path '%s'", path) + } + } + return nil +} + +// Validate a certificate file configuration entry +func (c *tCertificateFileConfig) Validate() error { + if c.Path == "" { + return fmt.Errorf("Certificate file entry has no path.") + } + if !valid.IsUnixFilePath(c.Path) { + return fmt.Errorf("Certificate file path '%s' is invalid.", c.Path) + } + if c.Owner != "" && !isValidUser(c.Owner) { + return fmt.Errorf("Unknown user '%s'", c.Owner) + } + if c.Group != "" && !isValidGroup(c.Group) { + return fmt.Errorf("Invalid group '%s'", c.Group) + } + err := checkFileList(c.PrependFiles) + if err != nil { + return err + } + for _, path := range c.PrependFiles { + if !valid.IsUnixFilePath(path) { + return fmt.Errorf("Invalid path '%s'", path) + } + } + if c.Certificate == "" && len(c.CACertificates) == 0 && c.CAChainOf == "" { + return fmt.Errorf("Certificate path '%s' has no certificate or CA chain", c.Path) + } + if c.CAChainOf != "" && len(c.CACertificates) != 0 { + return fmt.Errorf("Certificate path '%s' uses both 'ca' and 'ca_chain_of'", c.Path) + } + err = checkFileList(c.AppendFiles) + if err != nil { + return err + } + return nil +} + +// Validate the configuration +func (c *tConfiguration) Validate() error { + err := c.Socket.Validate() + if err != nil { + return err + } + err = c.LdapConfig.Validate() + if err != nil { + return err + } + for _, cfc := range c.Certificates { + err = cfc.Validate() + if err != nil { + return err + } + } + return nil +} + +// Create a configuration data structure containing default values. func defaultConfiguration() tConfiguration { cfg := tConfiguration{} cfg.Socket.Mode = 0640 - cfg.LdapConfig.Defaults.Port = 389 cfg.LdapConfig.Defaults.TLS = "no" + cfg.LdapConfig.Structure.CAChaining = "seeAlso" return cfg } // Load and check the configuration file func loadConfiguration(file string) (tConfiguration, error) { cfg := defaultConfiguration() - cfgData, err := ioutil.ReadFile(file) if err != nil { return cfg, fmt.Errorf("Could not load configuration: %w", err) } - err = yaml.Unmarshal(cfgData, &cfg) if err != nil { return cfg, fmt.Errorf("Could not parse configuration: %w", err) } - - return cfg, nil + err = cfg.Validate() + return cfg, err } diff --git a/go.mod b/go.mod index f70c10a..80a2a59 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module nocternity.net/go/fetchcert go 1.17 require ( + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/gemnasium/logrus-graylog-hook/v3 v3.0.3 github.com/karrick/golf v1.4.0 github.com/sirupsen/logrus v1.8.1