Jan Dittberner
2de592d30c
This commit changes the wire protocol to split between command announcement and command payload to allow proper typing of sent and received msgpack messages. CRL fetching has been implemented as second command after the existing health check command.
621 lines
16 KiB
Go
621 lines
16 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"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"git.cacert.org/cacert-gosigner/pkg/x509/openssl"
|
|
"git.cacert.org/cacert-gosigner/pkg/x509/revoking"
|
|
"git.cacert.org/cacert-gosigner/pkg/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 {
|
|
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"}
|
|
}
|
|
|
|
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"}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
for label, entry := range c.caMap {
|
|
if !entry.IsRoot() {
|
|
subordinates = append(subordinates, label)
|
|
}
|
|
}
|
|
|
|
return subordinates
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
|
|
if internalStructure.RSABits == nil {
|
|
return errors.New("element 'rsa-bits' with RSA key length required for algorithm RSA")
|
|
}
|
|
|
|
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
|
|
|
|
if internalStructure.EccCurve == "" {
|
|
return errors.New("element 'ecc-curve' required for algorithm EC")
|
|
}
|
|
|
|
p.EccCurve, err = nameToCurve(internalStructure.EccCurve)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.CRLSignatureAlgorithm = determineECDSASignatureAlgorithm(internalStructure.CRLSignatureAlgorithm)
|
|
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 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"`
|
|
}{}
|
|
|
|
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
|
|
}
|