cacert-gosigner/pkg/config/config.go

508 lines
13 KiB
Go

/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"crypto"
"crypto/elliptic"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"fmt"
"io"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type Settings struct {
Organization *pkix.Name
ValidityYears struct {
Root, Intermediary int
}
URLPatterns struct {
Ocsp, CRL, Issuer string
}
}
type SettingsError struct {
msg string
}
func (se SettingsError) Error() string {
return fmt.Sprintf("invalid Settings %s", se.msg)
}
func (s *Settings) UnmarshalYAML(n *yaml.Node) error {
data := struct {
Organization struct {
Country []string `yaml:"country"`
Organization []string `yaml:"organization"`
Locality []string `yaml:"locality"`
StreetAddress []string `yaml:"street-address"`
PostalCode []string `yaml:"postal-code"`
} `yaml:"organization"`
ValidityYears struct {
Root int `yaml:"root"`
Intermediary int `yaml:"intermediary"`
} `yaml:"validity-years"`
URLPatterns struct {
Ocsp string `yaml:"ocsp"`
CRL string `yaml:"crl"`
Issuer string `yaml:"issuer"`
} `yaml:"url-patterns"`
}{}
err := n.Decode(&data)
if err != nil {
return fmt.Errorf("could not decode YAML: %w", err)
}
if data.Organization.Organization == nil {
return SettingsError{"you need to specify 'organization'"}
}
if data.ValidityYears.Root == 0 || data.ValidityYears.Intermediary == 0 {
return SettingsError{"you must specify validity years for 'root' and 'intermediary'"}
}
if data.ValidityYears.Root < data.ValidityYears.Intermediary {
return SettingsError{"validity of root CA certificates must be equal or greater than those of" +
" intermediary CA certificates"}
}
if data.URLPatterns.Ocsp == "" {
return SettingsError{"you must specify an 'ocsp' URL pattern"}
}
if strings.Count(data.URLPatterns.Ocsp, "%s") > 1 {
return SettingsError{"url-pattern 'ocsp' must contain zero or one '%s' placeholder"}
}
if data.URLPatterns.CRL == "" {
return SettingsError{"you must specify an 'crl' URL pattern"}
}
if strings.Count(data.URLPatterns.CRL, "%s") != 1 {
return SettingsError{"url-pattern 'crl' must contain one '%s' placeholder"}
}
if data.URLPatterns.Issuer == "" {
return SettingsError{"you must specify an 'issuer' URL pattern"}
}
if strings.Count(data.URLPatterns.Issuer, "%s") != 1 {
return SettingsError{"url-pattern 'issuer' must contain one '%s' placeholder"}
}
s.Organization = &pkix.Name{}
s.Organization.Organization = data.Organization.Organization
s.Organization.Country = data.Organization.Country
s.Organization.Locality = data.Organization.Locality
s.Organization.StreetAddress = data.Organization.StreetAddress
s.Organization.PostalCode = data.Organization.PostalCode
s.ValidityYears.Root = data.ValidityYears.Root
s.ValidityYears.Intermediary = data.ValidityYears.Intermediary
s.URLPatterns.Ocsp = data.URLPatterns.Ocsp
s.URLPatterns.CRL = data.URLPatterns.CRL
s.URLPatterns.Issuer = data.URLPatterns.Issuer
return nil
}
type KeyStorage struct {
Label string
Module string
}
func (k *KeyStorage) UnmarshalYAML(n *yaml.Node) error {
ks := struct {
TokenType string `yaml:"type"`
Label string `yaml:"label"`
Module string `yaml:"module"`
}{}
err := n.Decode(&ks)
if err != nil {
return fmt.Errorf("could not decode YAML: %w", err)
}
switch ks.TokenType {
case "softhsm":
k.Module = SoftHsmModule
case "p11module":
if ks.Module == "" {
return errors.New("specify a 'module' field when using the 'p11module' type")
}
k.Module = ks.Module
default:
return fmt.Errorf("unsupported KeyStorage type '%s'", ks.TokenType)
}
if ks.Label == "" {
return errors.New("element 'label' must be specified")
}
k.Label = ks.Label
return nil
}
type SignerConfig struct {
global *Settings `yaml:"Settings"`
caMap map[string]*CaCertificateEntry `yaml:"CAs"`
keyStorage map[string]*KeyStorage `yaml:"KeyStorage"`
}
func (c *SignerConfig) GetCADefinition(label string) (*CaCertificateEntry, error) {
entry, ok := c.caMap[label]
if !ok {
return nil, fmt.Errorf("no CA definition found for label %s", label)
}
return entry, nil
}
func (c *SignerConfig) CalculateValidity(cert *CaCertificateEntry, relativeTo time.Time) (time.Time, time.Time) {
var notBefore, notAfter time.Time
notBefore = relativeTo
if cert.IsRoot() {
notAfter = notBefore.AddDate(c.global.ValidityYears.Root, 0, 0)
} else {
notAfter = notBefore.AddDate(c.global.ValidityYears.Intermediary, 0, 0)
}
return notBefore, notAfter
}
func (c *SignerConfig) CalculateSubject(cert *CaCertificateEntry) pkix.Name {
subject := pkix.Name{
Country: c.global.Organization.Country,
Organization: c.global.Organization.Organization,
Locality: c.global.Organization.Locality,
StreetAddress: c.global.Organization.StreetAddress,
PostalCode: c.global.Organization.PostalCode,
}
subject.CommonName = cert.CommonName
return subject
}
func (c *SignerConfig) CertificateFileName(label string) string {
return fmt.Sprintf("%s.crt", label)
}
func (c *SignerConfig) BuildIssuerURL(cert *CaCertificateEntry) string {
return fmt.Sprintf(c.global.URLPatterns.Issuer, cert.Parent)
}
func (c *SignerConfig) BuildOCSPURL(cert *CaCertificateEntry) string {
// in case the configuration specifies a placeholder
if strings.Count(c.global.URLPatterns.Ocsp, "%s") == 1 {
return fmt.Sprintf(c.global.URLPatterns.Ocsp, cert.Parent)
}
return c.global.URLPatterns.Ocsp
}
func (c *SignerConfig) BuildCRLUrl(cert *CaCertificateEntry) string {
return fmt.Sprintf(c.global.URLPatterns.CRL, cert.Parent)
}
func (c *SignerConfig) GetParentCA(label string) (*CaCertificateEntry, error) {
entry, ok := c.caMap[label]
if !ok {
return nil, fmt.Errorf("no CA definition for %s", label)
}
if entry.IsRoot() {
return nil, fmt.Errorf("CA %s is a root CA and has no parent", label)
}
return c.caMap[entry.Parent], nil
}
// RootCAs returns the labels of all configured root CAs
func (c *SignerConfig) RootCAs() []string {
roots := make([]string, 0)
for label, entry := range c.caMap {
if entry.IsRoot() {
roots = append(roots, label)
}
}
return roots
}
// IntermediaryCAs returns the labels of all configured intermediary CAs
func (c *SignerConfig) IntermediaryCAs() []string {
intermediaries := make([]string, 0)
for label, entry := range c.caMap {
if !entry.IsRoot() {
intermediaries = append(intermediaries, label)
}
}
return intermediaries
}
func (c *SignerConfig) GetKeyStorage(label string) (*KeyStorage, error) {
keyStorage, ok := c.keyStorage[label]
if !ok {
return nil, fmt.Errorf("could not find storage definition with label %s", label)
}
return keyStorage, nil
}
// LoadConfiguration reads YAML configuration from the given reader as a SignerConfig structure
func LoadConfiguration(r io.Reader) (*SignerConfig, error) {
config := struct {
Global *Settings `yaml:"Settings"`
CAs map[string]*CaCertificateEntry `yaml:"CAs"`
KeyStorage map[string]*KeyStorage `yaml:"KeyStorage"`
}{}
decoder := yaml.NewDecoder(r)
err := decoder.Decode(&config)
if err != nil {
return nil, fmt.Errorf("could not parse YAML configuration: %w", err)
}
if config.Global == nil {
return nil, errors.New("configuration entry 'Settings' is missing or empty")
}
if config.CAs == nil {
return nil, errors.New("configuration entry 'CAs' is missing or empty")
}
if config.KeyStorage == nil {
return nil, errors.New("configuration entry 'KeyStorage' is missing or empty")
}
return &SignerConfig{
global: config.Global,
caMap: config.CAs,
keyStorage: config.KeyStorage,
}, nil
}
type PrivateKeyInfo struct {
Algorithm x509.PublicKeyAlgorithm
EccCurve elliptic.Curve
RSABits int
}
func (p *PrivateKeyInfo) UnmarshalYAML(value *yaml.Node) error {
internalStructure := struct {
Algorithm string `yaml:"algorithm"`
EccCurve string `yaml:"ecc-curve,omitempty"`
RSABits *int `yaml:"rsa-bits,omitempty"`
}{}
err := value.Decode(&internalStructure)
if err != nil {
return fmt.Errorf("could not unmarshal private key info: %w", err)
}
switch internalStructure.Algorithm {
case "RSA":
p.Algorithm = x509.RSA
if internalStructure.RSABits == nil {
return errors.New("element 'rsa-bits' with RSA key length required for algorithm RSA")
}
p.RSABits = *internalStructure.RSABits
case "EC":
p.Algorithm = x509.ECDSA
if internalStructure.EccCurve == "" {
return errors.New("element 'ecc-curve' required for algorithm EC")
}
p.EccCurve, err = nameToCurve(internalStructure.EccCurve)
if err != nil {
return err
}
case "":
return errors.New("element 'algorithm' must be specified as 'EC' or 'RSA'")
default:
return fmt.Errorf(
"unsupported key algorithm %s, use either 'EC' or 'RSA'",
internalStructure.Algorithm,
)
}
return nil
}
func (p *PrivateKeyInfo) MarshalYAML() (interface{}, error) {
internalStructure := struct {
Algorithm string `yaml:"algorithm"`
EccCurve string `yaml:"ecc-curve,omitempty"`
RSABits *int `yaml:"rsa-bits,omitempty"`
}{}
switch p.Algorithm {
case x509.RSA:
internalStructure.Algorithm = "RSA"
internalStructure.RSABits = &p.RSABits
case x509.ECDSA:
internalStructure.Algorithm = "EC"
curveName, err := curveToName(p.EccCurve)
if err != nil {
return nil, err
}
internalStructure.EccCurve = curveName
}
return internalStructure, nil
}
func curveToName(curve elliptic.Curve) (string, error) {
switch curve {
case elliptic.P224():
return "P-224", nil
case elliptic.P256():
return "P-256", nil
case elliptic.P384():
return "P-384", nil
case elliptic.P521():
return "P-521", nil
default:
return "", fmt.Errorf("unsupported EC curve %s", curve)
}
}
// nameToCurve maps a curve name to an elliptic curve
func nameToCurve(name string) (elliptic.Curve, error) {
switch name {
case "P-224", "secp224r1":
return elliptic.P224(), nil
case "P-256", "secp256r1":
return elliptic.P256(), nil
case "P-384", "secp384r1":
return elliptic.P384(), nil
case "P-521", "secp521r1":
return elliptic.P521(), nil
default:
return nil, fmt.Errorf("unsupported EC curve %s", name)
}
}
type CaCertificateEntry struct {
KeyInfo *PrivateKeyInfo
CommonName string
MaxPathLen int // maximum path length should be 0 for CAs that issue end entity certificates
ExtKeyUsage []x509.ExtKeyUsage
Certificate *x509.Certificate
KeyPair crypto.Signer
Parent string
Storage string
}
func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error {
var m struct {
KeyInfo *PrivateKeyInfo `yaml:"key-info"`
CommonName string `yaml:"common-name"`
// maximum path length should be 0 for CAs that issue end entity certificates
MaxPathLen int `yaml:"max-path-len,omitempty"`
ExtKeyUsage []string `yaml:"ext-key-usages,omitempty"`
Parent string `yaml:"parent"`
Storage string `yaml:"storage"`
}
err := value.Decode(&m)
if err != nil {
return fmt.Errorf("could not unmarshal CA certificate entry: %w", err)
}
if m.KeyInfo == nil {
return errors.New("element 'key-info' must be set")
}
c.KeyInfo = m.KeyInfo
if m.CommonName == "" {
return errors.New("element 'common-name' must be set")
}
c.CommonName = m.CommonName
c.MaxPathLen = m.MaxPathLen
if m.Parent == "" && m.ExtKeyUsage != nil {
return errors.New("a root CA must not specify 'ext-key-usages'")
}
c.Parent = m.Parent
if m.ExtKeyUsage != nil {
c.ExtKeyUsage, err = mapExtKeyUsageNames(m.ExtKeyUsage)
if err != nil {
return err
}
}
if m.Storage != "" {
c.Storage = m.Storage
} else {
c.Storage = "default"
}
return nil
}
func (c *CaCertificateEntry) IsRoot() bool {
return c.Parent == ""
}
func mapExtKeyUsageNames(usages []string) ([]x509.ExtKeyUsage, error) {
extKeyUsages := make([]x509.ExtKeyUsage, len(usages))
for idx, usage := range usages {
switch usage {
case "client":
extKeyUsages[idx] = x509.ExtKeyUsageClientAuth
case "code":
extKeyUsages[idx] = x509.ExtKeyUsageCodeSigning
case "email":
extKeyUsages[idx] = x509.ExtKeyUsageEmailProtection
case "server":
extKeyUsages[idx] = x509.ExtKeyUsageServerAuth
case "ocsp":
extKeyUsages[idx] = x509.ExtKeyUsageOCSPSigning
default:
return nil, fmt.Errorf("unsupported extended key usage %s", usage)
}
}
return extKeyUsages, nil
}