fetchcert/config.go
Emmanuel BENOîT 658ee30bc6 Server socket
* The UNIX socket will be listened on for control messages
  * A message containing a single 'Q' will cause it to exit
  * A message containing a single 'R' will cause a configuration reload.
    If the new configuration is incorrect, the old configuration will be
    kept. A new socket will be opened if the path has changed (failure
    when doing so will restore the previous configuration as well).
  * A message starting with 'U' requests an update. The next character
    may be '!' to force updates or anything else to update only as
    needed. The rest of the string is the selector: either a DN or '*'.
  * The selector is ignored in this commit; all certificates are
    re-examined.
2021-11-06 17:12:08 +01:00

299 lines
7.7 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"`
}
// 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 []string `yaml:"after_update"`
}
// Main configuration.
tConfiguration struct {
Socket tSocketConfig `yaml:"socket"`
LdapConfig tLdapConfig `yaml:"ldap"`
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 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.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
}