fetchcert/config.go
Emmanuel BENOîT bccd467968 Configuration - Validate handlers for certificates
* Handlers referenced in certificate file sections must exist. No
    handlers may be referenced more than once by the same section.
2021-12-04 13:22:13 +01:00

329 lines
8.8 KiB
Go

package main
import (
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"os/user"
"strconv"
valid "github.com/asaskevich/govalidator"
"gopkg.in/yaml.v2"
)
type (
/* *
* CONFIGURATION DATA *
* */
// UNIX socket configuration. This includes the full path to the socket
// as well as the group name and mode.
tSocketConfig struct {
Path string `yaml:"path"`
Group string `yaml:"group"`
Mode os.FileMode `yaml:"mode"`
}
// LDAP connection configuration, used for servers and as a way to specify
// defaults.
tLdapConnectionConfig struct {
Port uint16 `yaml:"port"`
TLS string `yaml:"tls"`
TLSNoVerify bool `yaml:"tls_skip_verify"`
CaChain string `yaml:"ca_chain"`
BindUser string `yaml:"bind_user"`
BindPassword string `yaml:"bind_password"`
}
// LDAP server configuration. This defines how to connect to a
// single, specific LDAP server.
tLdapServerConfig struct {
Host string `yaml:"host"`
tLdapConnectionConfig
}
// LDAP attributes and base DN configuration
tLdapStructureConfig struct {
BaseDN string `yaml:"base_dn"`
EndEntityCertificate string `yaml:"end_entity"`
CACertificate string `yaml:"ca_certificate"`
CAChaining string `yaml:"ca_chaining"`
}
// LDAP configuration: LDAP structure, connection defaults and server
// connections.
tLdapConfig struct {
Structure tLdapStructureConfig `yaml:"structure"`
Defaults tLdapConnectionConfig `yaml:"defaults"`
Servers []tLdapServerConfig `yaml:"servers"`
}
// Handlers. Each handler has a name and contains a list of commands.
tHandlers map[string][]string
// Certificate file updates configuration.
tCertFileUpdateConfig struct {
PreCommands []string `yaml:"pre_commands"`
Handlers []string `yaml:"handlers"`
PostCommands []string `yaml:"post_commands"`
}
// Certificate file configuration.
tCertificateFileConfig struct {
Path string `yaml:"path"`
Mode os.FileMode `yaml:"mode"`
Owner string `yaml:"owner"`
Group string `yaml:"group"`
PrependFiles []string `yaml:"prepend_files"`
Certificate string `yaml:"certificate"`
CACertificates []string `yaml:"ca"`
CAChainOf string `yaml:"ca_chain_of"`
Reverse bool `yaml:"reverse"`
AppendFiles []string `yaml:"append_files"`
AfterUpdate tCertFileUpdateConfig `yaml:"after_update"`
}
// Main configuration.
tConfiguration struct {
Socket tSocketConfig `yaml:"socket"`
LdapConfig tLdapConfig `yaml:"ldap"`
Handlers tHandlers `yaml:"handlers"`
Certificates []tCertificateFileConfig `yaml:"certificates"`
}
)
// 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 the list of handles
func (c *tCertFileUpdateConfig) Validate(handlers *tHandlers) error {
set := make(map[string]bool)
for _, handler := range c.Handlers {
if _, exists := (*handlers)[handler]; !exists {
return fmt.Errorf("Handler '%s' does not exist.", handler)
}
if _, exists := set[handler]; exists {
return fmt.Errorf("Handler '%s' specified more than once.", handler)
}
set[handler] = true
}
return nil
}
// Validate a certificate file configuration entry
func (c *tCertificateFileConfig) Validate(handlers *tHandlers) error {
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
}
err = c.AfterUpdate.Validate(handlers)
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 idx, cfc := range c.Certificates {
if cfc.Path == "" {
return fmt.Errorf("Certificate file entry #%d has no path.", idx+1)
}
err = cfc.Validate(&c.Handlers)
if err != nil {
return fmt.Errorf("Certificate file %s (#%d): %s", cfc.Path, idx+1, err)
}
}
return nil
}
// Create a configuration data structure containing default values.
func defaultConfiguration() tConfiguration {
cfg := tConfiguration{}
cfg.Socket.Mode = 0640
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)
}
err = cfg.Validate()
return cfg, err
}