Configuration validation

* Validate most of the entries in the configuration file, with the
    exception of DNs / RDNs.
  * Use the govalidator library for DNS names and file paths
This commit is contained in:
Emmanuel BENOîT 2021-10-31 11:30:39 +01:00
parent e6aaa09795
commit f971c1e961
2 changed files with 201 additions and 5 deletions

205
config.go
View file

@ -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
}

1
go.mod
View file

@ -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