cacert-gosigner/internal/config/config.go

698 lines
18 KiB
Go
Raw Normal View History

2022-04-24 07:25:04 +00:00
/*
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"
"path"
"strings"
"time"
"gopkg.in/yaml.v3"
"git.cacert.org/cacert-gosigner/internal/x509/openssl"
"git.cacert.org/cacert-gosigner/internal/x509/revoking"
"git.cacert.org/cacert-gosigner/internal/x509/signing"
)
const minRSABits = 2048
type Serial struct {
Device string
Baud int
Timeout time.Duration
}
type Settings struct {
Organization *pkix.Name
ValidityYears struct {
Root, Subordinate int
}
URLPatterns struct {
Ocsp, CRL, Issuer string
}
Serial *Serial
}
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"`
Subordinate int `yaml:"subordinate"`
} `yaml:"validity-years"`
URLPatterns struct {
Ocsp string `yaml:"ocsp"`
CRL string `yaml:"crl"`
Issuer string `yaml:"issuer"`
} `yaml:"url-patterns"`
Serial struct {
Device string `yaml:"device"`
Baud int `yaml:"baud"`
TimeoutMillis int `yaml:"timeout-millis"`
} `yaml:"serial"`
}{}
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.Subordinate == 0 {
return SettingsError{"you must specify validity years for 'root' and 'subordinate'"}
}
if data.ValidityYears.Root < data.ValidityYears.Subordinate {
2022-04-24 09:24:15 +00:00
return SettingsError{"validity of root CA certificates must be equal or greater than those of" +
" subordinate CA certificates"}
}
if data.URLPatterns.Ocsp == "" {
return SettingsError{"you must specify an 'ocsp' URL pattern"}
}
2022-04-24 07:25:04 +00:00
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"}
}
2022-04-24 07:25:04 +00:00
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"}
}
2022-04-24 07:25:04 +00:00
if strings.Count(data.URLPatterns.Issuer, "%s") != 1 {
return SettingsError{"url-pattern 'issuer' must contain one '%s' placeholder"}
}
if data.Serial.Device == "" {
return SettingsError{"you must specify a serial 'device'"}
}
if data.Serial.Baud == 0 {
data.Serial.Baud = 115200
}
if data.Serial.TimeoutMillis == 0 {
data.Serial.TimeoutMillis = 5000
}
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.Subordinate = data.ValidityYears.Subordinate
s.URLPatterns.Ocsp = data.URLPatterns.Ocsp
s.URLPatterns.CRL = data.URLPatterns.CRL
s.URLPatterns.Issuer = data.URLPatterns.Issuer
s.Serial = &Serial{
Device: data.Serial.Device,
Baud: data.Serial.Baud,
Timeout: time.Duration(data.Serial.TimeoutMillis) * time.Millisecond,
}
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")
}
2022-04-24 07:25:04 +00:00
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")
}
2022-04-24 07:25:04 +00:00
k.Label = ks.Label
return nil
}
type CARepository interface {
revoking.Repository
signing.Repository
}
type SignerConfig struct {
global *Settings `yaml:"Settings"`
caMap map[string]*CaCertificateEntry `yaml:"CAs"`
keyStorage map[string]*KeyStorage `yaml:"KeyStorage"`
repositories map[string]CARepository
}
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)
}
2022-04-24 07:25:04 +00:00
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.Subordinate, 0, 0)
}
2022-04-24 07:25:04 +00:00
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
2022-04-24 07:25:04 +00:00
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)
}
2022-04-24 07:25:04 +00:00
return c.global.URLPatterns.Ocsp
}
func (c *SignerConfig) BuildCRLUrl(label string) string {
return fmt.Sprintf(c.global.URLPatterns.CRL, label)
}
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)
}
if entry.Parent == "" {
return nil, fmt.Errorf("parent for %s is empty", label)
}
parent, ok := c.caMap[entry.Parent]
if !ok {
return nil, fmt.Errorf("parent %s for %s not found in signer config", entry.Parent, label)
}
return parent, nil
}
// RootCAs returns the labels of all configured root CAs
func (c *SignerConfig) RootCAs() []string {
roots := make([]string, 0)
2022-04-24 07:25:04 +00:00
for label, entry := range c.caMap {
if entry.IsRoot() {
roots = append(roots, label)
}
}
return roots
}
// SubordinateCAs returns the labels of all configured subordinate CAs
func (c *SignerConfig) SubordinateCAs() []string {
subordinates := make([]string, 0)
2022-04-24 07:25:04 +00:00
for label, entry := range c.caMap {
if !entry.IsRoot() {
subordinates = append(subordinates, label)
}
}
return subordinates
}
2022-04-24 12:05:46 +00:00
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
}
func (c *SignerConfig) GetSerial() *Serial {
return c.global.Serial
}
func (c *SignerConfig) Repository(name string) (CARepository, error) {
var (
repo CARepository
ok bool
err error
)
repo, ok = c.repositories[name]
if !ok {
repo, err = openssl.NewFileRepository(path.Join("repos", name))
if err != nil {
return nil, fmt.Errorf("could not create repository for %s: %w", name, err)
}
c.repositories[name] = repo
}
return repo, 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)
}
2022-04-21 19:12:34 +00:00
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,
repositories: make(map[string]CARepository),
}, nil
}
type PrivateKeyInfo struct {
Algorithm x509.PublicKeyAlgorithm
EccCurve elliptic.Curve
RSABits int
CRLSignatureAlgorithm x509.SignatureAlgorithm
}
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"`
CRLSignatureAlgorithm string `yaml:"crl-signature-algorithm,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
2022-04-24 07:25:04 +00:00
if internalStructure.RSABits == nil {
2022-04-21 19:12:34 +00:00
return errors.New("element 'rsa-bits' with RSA key length required for algorithm RSA")
}
2022-04-24 07:25:04 +00:00
p.RSABits = *internalStructure.RSABits
if p.RSABits < minRSABits {
return fmt.Errorf("RSA keys must have a length of at least %d bits", minRSABits)
}
p.CRLSignatureAlgorithm = determineRSASignatureAlgorithm(internalStructure.CRLSignatureAlgorithm)
case "EC":
p.Algorithm = x509.ECDSA
2022-04-24 07:25:04 +00:00
2022-04-21 19:12:34 +00:00
if internalStructure.EccCurve == "" {
return errors.New("element 'ecc-curve' required for algorithm EC")
}
2022-04-24 07:25:04 +00:00
p.EccCurve, err = nameToCurve(internalStructure.EccCurve)
if err != nil {
return err
}
p.CRLSignatureAlgorithm = determineECDSASignatureAlgorithm(internalStructure.CRLSignatureAlgorithm)
2022-04-21 19:12:34 +00:00
case "":
return errors.New("element 'algorithm' must be specified as 'EC' or 'RSA'")
default:
2022-04-21 19:12:34 +00:00
return fmt.Errorf(
"unsupported key algorithm %s, use either 'EC' or 'RSA'",
internalStructure.Algorithm,
)
}
return nil
}
func determineRSASignatureAlgorithm(algorithm string) x509.SignatureAlgorithm {
switch strings.ToLower(algorithm) {
case "sha1withrsa", "sha1":
return x509.SHA1WithRSA
case "sha384withrsa", "sha384":
return x509.SHA384WithRSA
case "sha512withrsa", "sha512":
return x509.SHA512WithRSA
default:
return x509.SHA256WithRSA
}
}
func determineECDSASignatureAlgorithm(algorithm string) x509.SignatureAlgorithm {
switch strings.ToLower(algorithm) {
case "sha1withecdsa", "sha1":
return x509.ECDSAWithSHA1
case "sha384withecdsa", "sha384":
return x509.ECDSAWithSHA384
case "sha512withecdsa", "sha512":
return x509.ECDSAWithSHA512
default:
return x509.ECDSAWithSHA256
}
}
func (p *PrivateKeyInfo) MarshalYAML() (interface{}, error) {
internalStructure := struct {
Algorithm string `yaml:"algorithm"`
EccCurve string `yaml:"ecc-curve,omitempty"`
RSABits *int `yaml:"rsa-bits,omitempty"`
}{}
2022-04-24 07:25:04 +00:00
switch p.Algorithm {
case x509.RSA:
internalStructure.Algorithm = "RSA"
internalStructure.RSABits = &p.RSABits
case x509.ECDSA:
internalStructure.Algorithm = "EC"
2022-04-24 07:25:04 +00:00
curveName, err := curveToName(p.EccCurve)
if err != nil {
return nil, err
}
2022-04-24 07:25:04 +00:00
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)
}
}
var validProfileUsages = map[string]signing.ProfileUsage{
"ocsp": signing.UsageOCSP,
"client": signing.UsageClient,
"code": signing.UsageCode,
"person": signing.UsagePerson,
"server": signing.UsageServer,
"server_client": signing.UsageServerClient,
"org_client": signing.UsageOrgClient,
"org_code": signing.UsageOrgCode,
"org_email": signing.UsageOrgEmail,
"org_person": signing.UsageOrgPerson,
"org_server": signing.UsageOrgServer,
"org_server_client": signing.UsageOrgServerClient,
}
func ParseUsage(u string) (signing.ProfileUsage, error) {
usage, ok := validProfileUsages[u]
if !ok {
return signing.UsageInvalid, fmt.Errorf("unsupported profile usage: %s", u)
}
return usage, nil
}
type Profile struct {
Name string
UseFor signing.ProfileUsage
Years int
Months int
Days int
}
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
Profiles []Profile
}
func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error {
var m struct {
2022-04-24 07:25:04 +00:00
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"`
Profiles []struct {
Name string `yaml:"name"`
UseFor string `yaml:"use-for"`
Validity struct {
Years int `yaml:"years"`
Months int `yaml:"months"`
Days int `yaml:"days"`
} `yaml:"validity"`
} `yaml:"profiles"`
}
err := value.Decode(&m)
if err != nil {
2022-04-24 07:25:04 +00:00
return fmt.Errorf("could not unmarshal CA certificate entry: %w", err)
}
2022-04-21 19:12:34 +00:00
if m.KeyInfo == nil {
return errors.New("element 'key-info' must be set")
}
c.KeyInfo = m.KeyInfo
2022-04-21 19:12:34 +00:00
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"
}
if m.Profiles != nil {
c.Profiles = make([]Profile, len(m.Profiles))
for i, prof := range m.Profiles {
usage, err := ParseUsage(prof.UseFor)
if err != nil {
return fmt.Errorf("config error: %w", err)
}
if prof.Validity.Years > 0 || prof.Validity.Months > 0 || prof.Validity.Days > 0 {
prof.Validity.Years, prof.Validity.Months, prof.Validity.Days = defaultValidity(usage)
}
c.Profiles[i] = Profile{
Name: prof.Name,
UseFor: usage,
Years: prof.Validity.Years,
Months: prof.Validity.Months,
Days: prof.Validity.Days,
}
}
}
return nil
}
func defaultValidity(useFor signing.ProfileUsage) (years int, months int, days int) {
switch useFor {
case signing.UsagePerson, signing.UsageOrgPerson, signing.UsageOrgEmail:
return 2, 0, 0
default:
return 1, 0, 0
}
}
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
}