Address (most) PR comments

This commit is contained in:
Herman Slatman 2021-03-21 16:42:41 +01:00 committed by max furman
parent be528da709
commit 97b88c4d58
11 changed files with 116 additions and 197 deletions

View file

@ -466,34 +466,29 @@ func (a *Authority) init() error {
// TODO: decide if this is a good approach for providing the SCEP functionality // TODO: decide if this is a good approach for providing the SCEP functionality
// It currently mirrors the logic for the x509CAService // It currently mirrors the logic for the x509CAService
if a.requiresSCEPService() && a.scepService == nil { if a.requiresSCEPService() && a.scepService == nil {
var options casapi.Options var options scep.Options
if a.config.AuthorityConfig.Options != nil {
options = *a.config.AuthorityConfig.Options
}
// Read intermediate and create X509 signer and decrypter for default CAS. // Read intermediate and create X509 signer and decrypter for default CAS.
if options.Is(casapi.SoftCAS) { options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert) if err != nil {
if err != nil { return err
return err }
} options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.IntermediateKey,
SigningKey: a.config.IntermediateKey, Password: []byte(a.config.Password),
Password: []byte(a.config.Password), })
if err != nil {
return err
}
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey,
Password: []byte(a.config.Password),
}) })
if err != nil { if err != nil {
return err return err
} }
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
}
} }
a.scepService, err = scep.NewService(context.Background(), options) a.scepService, err = scep.NewService(context.Background(), options)

View file

@ -102,9 +102,3 @@ func (s *SCEP) GetChallengePassword() string {
func (s *SCEP) GetCapabilities() []string { func (s *SCEP) GetCapabilities() []string {
return s.Capabilities return s.Capabilities
} }
// Interface guards
var (
_ Interface = (*SCEP)(nil)
//_ scep.Provisioner = (*SCEP)(nil)
)

View file

@ -235,6 +235,15 @@ func (ca *CA) Init(config *config.Config) (*CA, error) {
ca.auth = auth ca.auth = auth
ca.srv = server.New(config.Address, handler, tlsConfig) ca.srv = server.New(config.Address, handler, tlsConfig)
// TODO: instead opt for having a single server.Server but two
// http.Servers handling the HTTP and HTTPS handler? The latter
// will probably introduce more complexity in terms of graceful
// reload.
if config.InsecureAddress != "" {
ca.insecureSrv = server.New(config.InsecureAddress, insecureHandler, nil)
}
return ca, nil return ca, nil
} }

View file

@ -16,6 +16,8 @@ type KeyManager interface {
Close() error Close() error
} }
// Decrypter is an interface implemented by KMSes that are used
// in operations that require decryption
type Decrypter interface { type Decrypter interface {
CreateDecrypter(req *CreateDecrypterRequest) (crypto.Decrypter, error) CreateDecrypter(req *CreateDecrypterRequest) (crypto.Decrypter, error)
} }

View file

@ -138,9 +138,6 @@ type CreateDecrypterRequest struct {
Decrypter crypto.Decrypter Decrypter crypto.Decrypter
DecryptionKey string DecryptionKey string
DecryptionKeyPEM []byte DecryptionKeyPEM []byte
TokenLabel string
PublicKey string
PublicKeyPEM []byte
Password []byte Password []byte
} }

View file

@ -4,8 +4,6 @@ import (
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -18,6 +16,8 @@ import (
"github.com/smallstep/certificates/scep" "github.com/smallstep/certificates/scep"
"go.mozilla.org/pkcs7" "go.mozilla.org/pkcs7"
"github.com/pkg/errors"
microscep "github.com/micromdm/scep/scep" microscep "github.com/micromdm/scep/scep"
) )
@ -75,7 +75,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
request, err := decodeSCEPRequest(r) request, err := decodeSCEPRequest(r)
if err != nil { if err != nil {
writeError(w, fmt.Errorf("not a scep get request: %w", err)) writeError(w, errors.Wrap(err, "not a scep get request"))
return return
} }
@ -90,11 +90,11 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
case opnPKIOperation: case opnPKIOperation:
// TODO: implement the GET for PKI operation? Default CACAPS doesn't specify this is in use, though // TODO: implement the GET for PKI operation? Default CACAPS doesn't specify this is in use, though
default: default:
err = fmt.Errorf("unknown operation: %s", request.Operation) err = errors.Errorf("unknown operation: %s", request.Operation)
} }
if err != nil { if err != nil {
writeError(w, fmt.Errorf("get request failed: %w", err)) writeError(w, errors.Wrap(err, "get request failed"))
return return
} }
@ -106,7 +106,7 @@ func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
request, err := decodeSCEPRequest(r) request, err := decodeSCEPRequest(r)
if err != nil { if err != nil {
writeError(w, fmt.Errorf("not a scep post request: %w", err)) writeError(w, errors.Wrap(err, "not a scep post request"))
return return
} }
@ -117,11 +117,11 @@ func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
case opnPKIOperation: case opnPKIOperation:
response, err = h.PKIOperation(ctx, request) response, err = h.PKIOperation(ctx, request)
default: default:
err = fmt.Errorf("unknown operation: %s", request.Operation) err = errors.Errorf("unknown operation: %s", request.Operation)
} }
if err != nil { if err != nil {
writeError(w, fmt.Errorf("post request failed: %w", err)) writeError(w, errors.Wrap(err, "post request failed"))
return return
} }
@ -163,7 +163,7 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
Message: decodedMessage, Message: decodedMessage,
}, nil }, nil
default: default:
return SCEPRequest{}, fmt.Errorf("unsupported operation: %s", operation) return SCEPRequest{}, errors.Errorf("unsupported operation: %s", operation)
} }
case http.MethodPost: case http.MethodPost:
body, err := ioutil.ReadAll(io.LimitReader(r.Body, maxPayloadSize)) body, err := ioutil.ReadAll(io.LimitReader(r.Body, maxPayloadSize))
@ -175,7 +175,7 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
Message: body, Message: body,
}, nil }, nil
default: default:
return SCEPRequest{}, fmt.Errorf("unsupported method: %s", method) return SCEPRequest{}, errors.Errorf("unsupported method: %s", method)
} }
} }
@ -187,7 +187,7 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
name := chi.URLParam(r, "provisionerID") name := chi.URLParam(r, "provisionerID")
provisionerID, err := url.PathUnescape(name) provisionerID, err := url.PathUnescape(name)
if err != nil { if err != nil {
api.WriteError(w, fmt.Errorf("error url unescaping provisioner id '%s'", name)) api.WriteError(w, errors.Errorf("error url unescaping provisioner id '%s'", name))
return return
} }
@ -229,6 +229,9 @@ func (h *Handler) GetCACert(ctx context.Context) (SCEPResponse, error) {
if len(certs) == 1 { if len(certs) == 1 {
response.Data = certs[0].Raw response.Data = certs[0].Raw
} else { } else {
// create degenerate pkcs7 certificate structure, according to
// https://tools.ietf.org/html/rfc8894#section-4.2.1.2, because
// not signed or encrypted data has to be returned.
data, err := microscep.DegenerateCertificates(certs) data, err := microscep.DegenerateCertificates(certs)
if err != nil { if err != nil {
return SCEPResponse{}, err return SCEPResponse{}, err
@ -329,12 +332,11 @@ func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) {
w.Header().Set("Content-Type", contentHeader(response)) w.Header().Set("Content-Type", contentHeader(response))
_, err := w.Write(response.Data) _, err := w.Write(response.Data)
if err != nil { if err != nil {
writeError(w, fmt.Errorf("error when writing scep response: %w", err)) // This could end up as an error again writeError(w, errors.Wrap(err, "error when writing scep response")) // This could end up as an error again
} }
} }
func writeError(w http.ResponseWriter, err error) { func writeError(w http.ResponseWriter, err error) {
// TODO: this probably needs to use SCEP specific errors (i.e. failInfo)
scepError := &scep.Error{ scepError := &scep.Error{
Message: err.Error(), Message: err.Error(),
Status: http.StatusInternalServerError, // TODO: make this a param? Status: http.StatusInternalServerError, // TODO: make this a param?

View file

@ -1,32 +1,24 @@
package scep package scep
import ( import (
"bytes"
"context" "context"
"crypto/subtle"
"crypto/x509" "crypto/x509"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
database "github.com/smallstep/certificates/db"
"github.com/smallstep/nosql"
microx509util "github.com/micromdm/scep/crypto/x509util" microx509util "github.com/micromdm/scep/crypto/x509util"
microscep "github.com/micromdm/scep/scep" microscep "github.com/micromdm/scep/scep"
//"github.com/smallstep/certificates/scep/pkcs7" "github.com/pkg/errors"
"go.mozilla.org/pkcs7" "go.mozilla.org/pkcs7"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
) )
var (
certTable = []byte("scep_certs")
)
// Interface is the SCEP authority interface. // Interface is the SCEP authority interface.
type Interface interface { type Interface interface {
LoadProvisionerByID(string) (provisioner.Interface, error) LoadProvisionerByID(string) (provisioner.Interface, error)
@ -42,28 +34,21 @@ type Interface interface {
// Authority is the layer that handles all SCEP interactions. // Authority is the layer that handles all SCEP interactions.
type Authority struct { type Authority struct {
backdate provisioner.Duration db DB
db nosql.DB prefix string
prefix string dns string
dns string
// dir *directory
intermediateCertificate *x509.Certificate intermediateCertificate *x509.Certificate
service *Service
service *Service signAuth SignAuthority
signAuth SignAuthority
} }
// AuthorityOptions required to create a new SCEP Authority. // AuthorityOptions required to create a new SCEP Authority.
type AuthorityOptions struct { type AuthorityOptions struct {
// Service provides the SCEP functions to Authority // Service provides the certificate chain, the signer and the decrypter to the Authority
Service *Service Service *Service
// Backdate // DB is the database used by SCEP
Backdate provisioner.Duration DB DB
// DB is the database used by nosql. // DNS is the host used to generate accurate SCEP links. By default the authority
DB nosql.DB
// DNS the host used to generate accurate SCEP links. By default the authority
// will use the Host from the request, so this value will only be used if // will use the Host from the request, so this value will only be used if
// request.Host is empty. // request.Host is empty.
DNS string DNS string
@ -81,19 +66,7 @@ type SignAuthority interface {
// New returns a new Authority that implements the SCEP interface. // New returns a new Authority that implements the SCEP interface.
func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) { func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
if _, ok := ops.DB.(*database.SimpleDB); !ok {
// If it's not a SimpleDB then go ahead and bootstrap the DB with the
// necessary SCEP tables. SimpleDB should ONLY be used for testing.
tables := [][]byte{certTable}
for _, b := range tables {
if err := ops.DB.CreateTable(b); err != nil {
return nil, fmt.Errorf("%w: error creating table %s", err, string(b))
}
}
}
authority := &Authority{ authority := &Authority{
backdate: ops.Backdate,
db: ops.DB, db: ops.DB,
prefix: ops.Prefix, prefix: ops.Prefix,
dns: ops.DNS, dns: ops.DNS,
@ -102,7 +75,7 @@ func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
// TODO: this is not really nice to do; the Service should be removed // TODO: this is not really nice to do; the Service should be removed
// in its entirety to make this more interoperable with the rest of // in its entirety to make this more interoperable with the rest of
// step-ca. // step-ca, I think.
if ops.Service != nil { if ops.Service != nil {
authority.intermediateCertificate = ops.Service.certificateChain[0] authority.intermediateCertificate = ops.Service.certificateChain[0]
authority.service = ops.Service authority.service = ops.Service
@ -200,12 +173,12 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err
p7c, err := pkcs7.Parse(msg.P7.Content) p7c, err := pkcs7.Parse(msg.P7.Content)
if err != nil { if err != nil {
return err return errors.Wrap(err, "error parsing pkcs7 content")
} }
envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter) envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter)
if err != nil { if err != nil {
return err return errors.Wrap(err, "error decrypting encrypted pkcs7 content")
} }
msg.pkiEnvelope = envelope msg.pkiEnvelope = envelope
@ -214,19 +187,19 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err
case microscep.CertRep: case microscep.CertRep:
certs, err := microscep.CACerts(msg.pkiEnvelope) certs, err := microscep.CACerts(msg.pkiEnvelope)
if err != nil { if err != nil {
return err return errors.Wrap(err, "error extracting CA certs from pkcs7 degenerate data")
} }
msg.CertRepMessage.Certificate = certs[0] // TODO: check correctness of this msg.CertRepMessage.Certificate = certs[0]
return nil return nil
case microscep.PKCSReq, microscep.UpdateReq, microscep.RenewalReq: case microscep.PKCSReq, microscep.UpdateReq, microscep.RenewalReq:
csr, err := x509.ParseCertificateRequest(msg.pkiEnvelope) csr, err := x509.ParseCertificateRequest(msg.pkiEnvelope)
if err != nil { if err != nil {
return fmt.Errorf("parse CSR from pkiEnvelope: %w", err) return errors.Wrap(err, "parse CSR from pkiEnvelope")
} }
// check for challengePassword // check for challengePassword
cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope) cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope)
if err != nil { if err != nil {
return fmt.Errorf("scep: parse challenge password in pkiEnvelope: %w", err) return errors.Wrap(err, "parse challenge password in pkiEnvelope")
} }
msg.CSRReqMessage = &microscep.CSRReqMessage{ msg.CSRReqMessage = &microscep.CSRReqMessage{
RawDecrypted: msg.pkiEnvelope, RawDecrypted: msg.pkiEnvelope,
@ -235,7 +208,7 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err
} }
return nil return nil
case microscep.GetCRL, microscep.GetCert, microscep.CertPoll: case microscep.GetCRL, microscep.GetCert, microscep.CertPoll:
return fmt.Errorf("not implemented") //errNotImplemented return errors.Errorf("not implemented")
} }
return nil return nil
@ -266,16 +239,13 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
} }
// Template data // Template data
data := x509util.NewTemplateData() data := x509util.CreateTemplateData(csr.Subject.CommonName, csr.DNSNames)
data.SetCommonName(csr.Subject.CommonName)
data.SetSANs(csr.DNSNames)
data.SetCertificateRequest(csr)
// Get authorizations from the SCEP provisioner. // Get authorizations from the SCEP provisioner.
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
signOps, err := p.AuthorizeSign(ctx, "") signOps, err := p.AuthorizeSign(ctx, "")
if err != nil { if err != nil {
return nil, fmt.Errorf("error retrieving authorization options from SCEP provisioner: %w", err) return nil, errors.Wrap(err, "error retrieving authorization options from SCEP provisioner")
} }
opts := provisioner.SignOptions{ opts := provisioner.SignOptions{
@ -285,19 +255,20 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating template options from SCEP provisioner: %w", err) return nil, errors.Wrap(err, "error creating template options from SCEP provisioner")
} }
signOps = append(signOps, templateOptions) signOps = append(signOps, templateOptions)
certChain, err := a.signAuth.Sign(csr, opts, signOps...) certChain, err := a.signAuth.Sign(csr, opts, signOps...)
if err != nil { if err != nil {
return nil, fmt.Errorf("error generating certificate for order %w", err) return nil, errors.Wrap(err, "error generating certificate for order")
} }
// take the issued certificate (only); https://tools.ietf.org/html/rfc8894#section-3.3.2
cert := certChain[0] cert := certChain[0]
// create a degenerate cert structure // and create a degenerate cert structure
deg, err := degenerateCertificates([]*x509.Certificate{cert}) deg, err := microscep.DegenerateCertificates([]*x509.Certificate{cert})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -370,13 +341,12 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
CertRepMessage: cr, CertRepMessage: cr,
} }
// TODO: save more data? // store the newly created certificate
_, err = newCert(a.db, CertOptions{ err = newCert(a.db, CertOptions{
Leaf: certChain[0], Leaf: certChain[0],
Intermediates: certChain[1:], Intermediates: certChain[1:],
}) })
if err != nil { if err != nil {
fmt.Println(err)
return nil, err return nil, err
} }
@ -459,7 +429,7 @@ func (a *Authority) MatchChallengePassword(ctx context.Context, password string)
return false, err return false, err
} }
if p.GetChallengePassword() == password { if subtle.ConstantTimeCompare([]byte(p.GetChallengePassword()), []byte(password)) == 1 {
return true, nil return true, nil
} }
@ -491,21 +461,3 @@ func (a *Authority) GetCACaps(ctx context.Context) []string {
return caps return caps
} }
// degenerateCertificates creates degenerate certificates pkcs#7 type
func degenerateCertificates(certs []*x509.Certificate) ([]byte, error) {
var buf bytes.Buffer
for _, cert := range certs {
buf.Write(cert.Raw)
}
degenerate, err := pkcs7.DegenerateCertificate(buf.Bytes())
if err != nil {
return nil, err
}
return degenerate, nil
}
// Interface guards
var (
_ Interface = (*Authority)(nil)
)

View file

@ -2,79 +2,20 @@ package scep
import ( import (
"crypto/x509" "crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"time"
"github.com/smallstep/nosql" "github.com/pkg/errors"
) )
type certificate struct {
ID string `json:"id"`
Created time.Time `json:"created"`
Leaf []byte `json:"leaf"`
Intermediates []byte `json:"intermediates"`
}
// CertOptions options with which to create and store a cert object. // CertOptions options with which to create and store a cert object.
type CertOptions struct { type CertOptions struct {
Leaf *x509.Certificate Leaf *x509.Certificate
Intermediates []*x509.Certificate Intermediates []*x509.Certificate
} }
func newCert(db nosql.DB, ops CertOptions) (*certificate, error) { func newCert(db DB, ops CertOptions) error {
err := db.StoreCertificate(ops.Leaf)
// TODO: according to the RFC this should be IssuerAndSerialNumber,
// but sscep seems to use just the serial number for getcert
id := ops.Leaf.SerialNumber.String()
leaf := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ops.Leaf.Raw,
})
var intermediates []byte
for _, cert := range ops.Intermediates {
intermediates = append(intermediates, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})...)
}
cert := &certificate{
ID: id,
Leaf: leaf,
Intermediates: intermediates,
Created: time.Now().UTC(),
}
certB, err := json.Marshal(cert)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: error marshaling certificate", err) errors.Wrap(err, "error while storing certificate")
}
_, swapped, err := db.CmpAndSwap(certTable, []byte(id), nil, certB)
switch {
case err != nil:
return nil, fmt.Errorf("%w: error storing certificate", err)
case !swapped:
return nil, fmt.Errorf("error storing certificate; " +
"value has changed since last read")
default:
return cert, nil
} }
return nil
} }
// func getCert(db nosql.DB, id string) (*certificate, error) {
// b, err := db.Get(certTable, []byte(id))
// if nosql.IsErrNotFound(err) {
// return nil, fmt.Errorf("certificate %s not found", id)
// } else if err != nil {
// return nil, fmt.Errorf("error loading certificate")
// }
// var cert certificate
// if err := json.Unmarshal(b, &cert); err != nil {
// return nil, fmt.Errorf("%w: error unmarshaling certificate", err)
// }
// return &cert, nil
// }

7
scep/database.go Normal file
View file

@ -0,0 +1,7 @@
package scep
import "crypto/x509"
type DB interface {
StoreCertificate(crt *x509.Certificate) error
}

31
scep/options.go Normal file
View file

@ -0,0 +1,31 @@
package scep
import (
"crypto"
"crypto/x509"
)
type Options struct {
// CertificateChain is the issuer certificate, along with any other bundled certificates
// to be returned in the chain for consumers. Configured in ca.json crt property.
CertificateChain []*x509.Certificate
// Signer signs CSRs in SCEP. Configured in ca.json key property.
Signer crypto.Signer `json:"-"`
// Decrypter decrypts encrypted SCEP messages. Configured in ca.json key property.
Decrypter crypto.Decrypter `json:"-"`
}
// Validate checks the fields in Options.
func (o *Options) Validate() error {
// var typ Type
// if o == nil {
// typ = Type(SoftCAS)
// } else {
// typ = Type(o.Type)
// }
// // Check that the type can be loaded.
// if _, ok := LoadCertificateAuthorityServiceNewFunc(typ); !ok {
// return errors.Errorf("unsupported cas type %s", typ)
// }
return nil
}

View file

@ -4,9 +4,6 @@ import (
"context" "context"
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"strings"
"github.com/smallstep/certificates/cas/apiv1"
) )
// Service is a wrapper for crypto.Signer and crypto.Decrypter // Service is a wrapper for crypto.Signer and crypto.Decrypter
@ -16,20 +13,12 @@ type Service struct {
decrypter crypto.Decrypter decrypter crypto.Decrypter
} }
func NewService(ctx context.Context, opts apiv1.Options) (*Service, error) { func NewService(ctx context.Context, opts Options) (*Service, error) {
if err := opts.Validate(); err != nil { if err := opts.Validate(); err != nil {
return nil, err return nil, err
} }
t := apiv1.Type(strings.ToLower(opts.Type))
if t == apiv1.DefaultCAS {
t = apiv1.SoftCAS
}
// TODO: silence the linter (temporarily)
_ = t
// TODO: should this become similar to the New CertificateAuthorityService as in x509CAService? // TODO: should this become similar to the New CertificateAuthorityService as in x509CAService?
return &Service{ return &Service{
certificateChain: opts.CertificateChain, certificateChain: opts.CertificateChain,