From 6bc0a86207c409db571637c77e2f1f3fe0fb91db Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 18 Apr 2024 16:12:30 +0200 Subject: [PATCH] Fix CA startup with Vault RA configuration --- authority/authority.go | 80 ++++++++++++++++++++++++++++------------ ca/ca.go | 1 + cas/apiv1/requests.go | 3 +- cas/apiv1/services.go | 17 +++++++++ cas/vaultcas/vaultcas.go | 3 +- scep/options.go | 20 +++++----- 6 files changed, 89 insertions(+), 35 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index f2118eac..e3da7f93 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "fmt" "log" "net/http" "strings" @@ -447,6 +448,7 @@ func (a *Authority) init() error { return err } a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate) + a.intermediateX509Certs = append(a.intermediateX509Certs, resp.IntermediateCertificates...) } } @@ -695,32 +697,42 @@ func (a *Authority) init() error { options := &scep.Options{ Roots: a.rootX509Certs, Intermediates: a.intermediateX509Certs, - SignerCert: a.intermediateX509Certs[0], } - if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: a.password, - }); err != nil { - return err + + // intermediate certificates can be empty in RA mode + if len(a.intermediateX509Certs) > 0 { + options.SignerCert = a.intermediateX509Certs[0] } - // TODO(hs): instead of creating the decrypter here, pass the - // intermediate key + chain down to the SCEP authority, - // and only instantiate it when required there. Is that possible? - // Also with entering passwords? - // TODO(hs): if moving the logic, try improving the logic for the - // decrypter password too? Right now it needs to be entered multiple - // times; I've observed it to be three times maximum, every time - // the intermediate key is read. - _, isRSA := options.Signer.Public().(*rsa.PublicKey) - if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSA { - if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ - DecryptionKey: a.config.IntermediateKey, - Password: a.password, - }); err == nil { - // only pass the decrypter down when it was successfully created, - // meaning it's an RSA key, and `CreateDecrypter` did not fail. - options.Decrypter = decrypter - options.DecrypterCert = options.Intermediates[0] + + // attempt to create the (default) SCEP signer if the intermediate + // key is configured. + if a.config.IntermediateKey != "" { + if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: a.password, + }); err != nil { + return err + } + + // TODO(hs): instead of creating the decrypter here, pass the + // intermediate key + chain down to the SCEP authority, + // and only instantiate it when required there. Is that possible? + // Also with entering passwords? + // TODO(hs): if moving the logic, try improving the logic for the + // decrypter password too? Right now it needs to be entered multiple + // times; I've observed it to be three times maximum, every time + // the intermediate key is read. + _, isRSAKey := options.Signer.Public().(*rsa.PublicKey) + if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSAKey { + if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ + DecryptionKey: a.config.IntermediateKey, + Password: a.password, + }); err == nil { + // only pass the decrypter down when it was successfully created, + // meaning it's an RSA key, and `CreateDecrypter` did not fail. + options.Decrypter = decrypter + options.DecrypterCert = options.Intermediates[0] + } } } @@ -811,6 +823,26 @@ func (a *Authority) init() error { return nil } +func (a *Authority) Mode() string { + if a.isRA() { + return fmt.Sprintf("RA (%s)", casapi.TypeOf(a.x509CAService).Name()) + } + + return "CA" // TODO(hs): more info? I.e. KMS type? +} + +func (a *Authority) isRA() bool { + if a.x509CAService == nil { + return false + } + switch casapi.TypeOf(a.x509CAService) { + case casapi.StepCAS, casapi.CloudCAS, casapi.VaultCAS, casapi.ExternalCAS: + return true + default: + return false + } +} + // initLogf is used to log initialization information. The output // can be disabled by starting the CA with the `--quiet` flag. func (a *Authority) initLogf(format string, v ...any) { diff --git a/ca/ca.go b/ca/ca.go index 0b426ded..d19f88d1 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -425,6 +425,7 @@ func (ca *CA) Run() error { log.Printf("Current context: %s", step.Contexts().GetCurrent().Name) } log.Printf("Config file: %s", ca.getConfigFileOutput()) + log.Printf("Mode: %s", ca.auth.Mode()) baseURL := fmt.Sprintf("https://%s%s", authorityInfo.DNSNames[0], ca.config.Address[strings.LastIndex(ca.config.Address, ":"):]) diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index fdbb285e..35cf9251 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -116,7 +116,8 @@ type GetCertificateAuthorityRequest struct { // GetCertificateAuthorityResponse is the response that contains // the root certificate. type GetCertificateAuthorityResponse struct { - RootCertificate *x509.Certificate + RootCertificate *x509.Certificate + IntermediateCertificates []*x509.Certificate } // CreateKeyRequest is the request used to generate a new key using a KMS. diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 00ecc2a8..bee58869 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -67,6 +67,23 @@ func (t Type) String() string { return strings.ToLower(string(t)) } +// Name returns the name of the CAS implementation. +func (t Type) Name() (name string) { + switch t { + case CloudCAS: + name = "GCP CAS" + case StepCAS: + name = "Step CAS" + case VaultCAS: + name = "Vault" + case ExternalCAS: + name = "External" + default: + name = "SoftCAS" // TODO(hs): different name? It's not a "CAS" CAS, really + } + return +} + // TypeOf returns the type of the given CertificateAuthorityService. func TypeOf(c CertificateAuthorityService) Type { if ct, ok := c.(interface{ Type() Type }); ok { diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 73d1b926..16adccc4 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -165,7 +165,8 @@ func (v *VaultCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest } return &apiv1.GetCertificateAuthorityResponse{ - RootCertificate: cert.root, + RootCertificate: cert.root, + IntermediateCertificates: cert.intermediates, }, nil } diff --git a/scep/options.go b/scep/options.go index 8bc30a61..d173a76c 100644 --- a/scep/options.go +++ b/scep/options.go @@ -37,19 +37,21 @@ func (o *Options) Validate() error { switch { case len(o.Intermediates) == 0: return errors.New("no intermediate certificate available for SCEP authority") - case o.Signer == nil: - return errors.New("no signer available for SCEP authority") case o.SignerCert == nil: return errors.New("no signer certificate available for SCEP authority") } - // check if the signer (intermediate CA) certificate has the same public key as - // the signer. According to the RFC it seems valid to have different keys for - // the intermediate and the CA signing new certificates, so this might change - // in the future. - signerPublicKey := o.Signer.Public().(comparablePublicKey) - if !signerPublicKey.Equal(o.SignerCert.PublicKey) { - return errors.New("mismatch between signer certificate and public key") + // the signer is optional, but if it's set, its public key must match the signer + // certificate public key. + if o.Signer != nil { + // check if the signer (intermediate CA) certificate has the same public key as + // the signer. According to the RFC it seems valid to have different keys for + // the intermediate and the CA signing new certificates, so this might change + // in the future. + signerPublicKey := o.Signer.Public().(comparablePublicKey) + if !signerPublicKey.Equal(o.SignerCert.PublicKey) { + return errors.New("mismatch between signer certificate and public key") + } } // decrypter can be nil in case a signing only key is used; validation complete.