forked from TrueCloudLab/certificates
Similarly to how ACME suppors multiple provisioners, it's now possible to load the right provisioner based on the URL.
329 lines
9.2 KiB
Go
329 lines
9.2 KiB
Go
package ca
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/pkg/errors"
|
|
"github.com/smallstep/certificates/acme"
|
|
acmeAPI "github.com/smallstep/certificates/acme/api"
|
|
"github.com/smallstep/certificates/api"
|
|
"github.com/smallstep/certificates/authority"
|
|
"github.com/smallstep/certificates/db"
|
|
"github.com/smallstep/certificates/logging"
|
|
"github.com/smallstep/certificates/monitoring"
|
|
"github.com/smallstep/certificates/scep"
|
|
scepAPI "github.com/smallstep/certificates/scep/api"
|
|
"github.com/smallstep/certificates/server"
|
|
"github.com/smallstep/nosql"
|
|
)
|
|
|
|
type options struct {
|
|
configFile string
|
|
password []byte
|
|
database db.AuthDB
|
|
}
|
|
|
|
func (o *options) apply(opts []Option) {
|
|
for _, fn := range opts {
|
|
fn(o)
|
|
}
|
|
}
|
|
|
|
// Option is the type of options passed to the CA constructor.
|
|
type Option func(o *options)
|
|
|
|
// WithConfigFile sets the given name as the configuration file name in the CA
|
|
// options.
|
|
func WithConfigFile(name string) Option {
|
|
return func(o *options) {
|
|
o.configFile = name
|
|
}
|
|
}
|
|
|
|
// WithPassword sets the given password as the configured password in the CA
|
|
// options.
|
|
func WithPassword(password []byte) Option {
|
|
return func(o *options) {
|
|
o.password = password
|
|
}
|
|
}
|
|
|
|
// WithDatabase sets the given authority database to the CA options.
|
|
func WithDatabase(db db.AuthDB) Option {
|
|
return func(o *options) {
|
|
o.database = db
|
|
}
|
|
}
|
|
|
|
// CA is the type used to build the complete certificate authority. It builds
|
|
// the HTTP server, set ups the middlewares and the HTTP handlers.
|
|
type CA struct {
|
|
auth *authority.Authority
|
|
config *authority.Config
|
|
srv *server.Server
|
|
opts *options
|
|
renewer *TLSRenewer
|
|
}
|
|
|
|
// New creates and initializes the CA with the given configuration and options.
|
|
func New(config *authority.Config, opts ...Option) (*CA, error) {
|
|
ca := &CA{
|
|
config: config,
|
|
opts: new(options),
|
|
}
|
|
ca.opts.apply(opts)
|
|
return ca.Init(config)
|
|
}
|
|
|
|
// Init initializes the CA with the given configuration.
|
|
func (ca *CA) Init(config *authority.Config) (*CA, error) {
|
|
if l := len(ca.opts.password); l > 0 {
|
|
ca.config.Password = string(ca.opts.password)
|
|
}
|
|
|
|
var opts []authority.Option
|
|
if ca.opts.database != nil {
|
|
opts = append(opts, authority.WithDatabase(ca.opts.database))
|
|
}
|
|
|
|
auth, err := authority.New(config, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsConfig, err := ca.getTLSConfig(auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Using chi as the main router
|
|
mux := chi.NewRouter()
|
|
handler := http.Handler(mux)
|
|
|
|
// Add regular CA api endpoints in / and /1.0
|
|
routerHandler := api.New(auth)
|
|
routerHandler.Route(mux)
|
|
mux.Route("/1.0", func(r chi.Router) {
|
|
routerHandler.Route(r)
|
|
})
|
|
|
|
//Add ACME api endpoints in /acme and /1.0/acme
|
|
dns := config.DNSNames[0]
|
|
u, err := url.Parse("https://" + config.Address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port := u.Port()
|
|
if port != "" && port != "443" {
|
|
dns = fmt.Sprintf("%s:%s", dns, port)
|
|
}
|
|
|
|
prefix := "acme"
|
|
acmeAuth, err := acme.New(auth, acme.AuthorityOptions{
|
|
Backdate: *config.AuthorityConfig.Backdate,
|
|
DB: auth.GetDatabase().(nosql.DB),
|
|
DNS: dns,
|
|
Prefix: prefix,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error creating ACME authority")
|
|
}
|
|
acmeRouterHandler := acmeAPI.New(acmeAuth)
|
|
mux.Route("/"+prefix, func(r chi.Router) {
|
|
acmeRouterHandler.Route(r)
|
|
})
|
|
// Use 2.0 because, at the moment, our ACME api is only compatible with v2.0
|
|
// of the ACME spec.
|
|
mux.Route("/2.0/"+prefix, func(r chi.Router) {
|
|
acmeRouterHandler.Route(r)
|
|
})
|
|
|
|
// TODO: THIS SHOULDN'T HAPPEN (or should become configurable)
|
|
// Current SCEP client I'm testing with doesn't seem to easily trust untrusted certs.
|
|
// Idea: provide a second mux/handler that runs without TLS. It probably should only
|
|
// have routes that are intended to be ran without TLS, like the SCEP ones. Look into
|
|
// option to not enable it in case no SCEP providers are configured. It might
|
|
// be nice to still include the SCEP routes in the secure handler too, for
|
|
// client that do understand HTTPS. The RFC does not seem to explicitly exclude HTTPS
|
|
// usage, but it mentions some caveats related to managing web PKI certificates as
|
|
// well as certificates via SCEP.
|
|
tlsConfig = nil
|
|
|
|
// TODO: get the SCEP service
|
|
scepPrefix := "scep"
|
|
scepAuthority, err := scep.New(auth, scep.AuthorityOptions{
|
|
IntermediateCertificatePath: config.IntermediateCert,
|
|
Service: auth.GetSCEPService(),
|
|
Backdate: *config.AuthorityConfig.Backdate,
|
|
DB: auth.GetDatabase().(nosql.DB),
|
|
DNS: dns,
|
|
Prefix: scepPrefix,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error creating SCEP authority")
|
|
}
|
|
scepRouterHandler := scepAPI.New(scepAuthority)
|
|
mux.Route("/"+scepPrefix, func(r chi.Router) {
|
|
scepRouterHandler.Route(r)
|
|
})
|
|
|
|
// helpful routine for logging all routes
|
|
//dumpRoutes(mux)
|
|
|
|
// Add monitoring if configured
|
|
if len(config.Monitoring) > 0 {
|
|
m, err := monitoring.New(config.Monitoring)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handler = m.Middleware(handler)
|
|
}
|
|
|
|
// Add logger if configured
|
|
if len(config.Logger) > 0 {
|
|
logger, err := logging.New("ca", config.Logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handler = logger.Middleware(handler)
|
|
}
|
|
|
|
ca.auth = auth
|
|
ca.srv = server.New(config.Address, handler, tlsConfig)
|
|
return ca, nil
|
|
}
|
|
|
|
// Run starts the CA calling to the server ListenAndServe method.
|
|
func (ca *CA) Run() error {
|
|
return ca.srv.ListenAndServe()
|
|
}
|
|
|
|
// Stop stops the CA calling to the server Shutdown method.
|
|
func (ca *CA) Stop() error {
|
|
ca.renewer.Stop()
|
|
if err := ca.auth.Shutdown(); err != nil {
|
|
log.Printf("error stopping ca.Authority: %+v\n", err)
|
|
}
|
|
return ca.srv.Shutdown()
|
|
}
|
|
|
|
// Reload reloads the configuration of the CA and calls to the server Reload
|
|
// method.
|
|
func (ca *CA) Reload() error {
|
|
config, err := authority.LoadConfiguration(ca.opts.configFile)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error reloading ca configuration")
|
|
}
|
|
|
|
logContinue := func(reason string) {
|
|
log.Println(reason)
|
|
log.Println("Continuing to run with the original configuration.")
|
|
log.Println("You can force a restart by sending a SIGTERM signal and then restarting the step-ca.")
|
|
}
|
|
|
|
// Do not allow reload if the database configuration has changed.
|
|
if !reflect.DeepEqual(ca.config.DB, config.DB) {
|
|
logContinue("Reload failed because the database configuration has changed.")
|
|
return errors.New("error reloading ca: database configuration cannot change")
|
|
}
|
|
|
|
newCA, err := New(config,
|
|
WithPassword(ca.opts.password),
|
|
WithConfigFile(ca.opts.configFile),
|
|
WithDatabase(ca.auth.GetDatabase()),
|
|
)
|
|
if err != nil {
|
|
logContinue("Reload failed because the CA with new configuration could not be initialized.")
|
|
return errors.Wrap(err, "error reloading ca")
|
|
}
|
|
|
|
if err = ca.srv.Reload(newCA.srv); err != nil {
|
|
logContinue("Reload failed because server could not be replaced.")
|
|
return errors.Wrap(err, "error reloading server")
|
|
}
|
|
|
|
// 1. Stop previous renewer
|
|
// 2. Safely shutdown any internal resources (e.g. key manager)
|
|
// 3. Replace ca properties
|
|
// Do not replace ca.srv
|
|
ca.renewer.Stop()
|
|
ca.auth.CloseForReload()
|
|
ca.auth = newCA.auth
|
|
ca.config = newCA.config
|
|
ca.opts = newCA.opts
|
|
ca.renewer = newCA.renewer
|
|
return nil
|
|
}
|
|
|
|
// getTLSConfig returns a TLSConfig for the CA server with a self-renewing
|
|
// server certificate.
|
|
func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
|
// Create initial TLS certificate
|
|
tlsCrt, err := auth.GetTLSCertificate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Start tls renewer with the new certificate.
|
|
// If a renewer was started, attempt to stop it before.
|
|
if ca.renewer != nil {
|
|
ca.renewer.Stop()
|
|
}
|
|
|
|
ca.renewer, err = NewTLSRenewer(tlsCrt, auth.GetTLSCertificate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ca.renewer.Run()
|
|
|
|
var tlsConfig *tls.Config
|
|
if ca.config.TLS != nil {
|
|
tlsConfig = ca.config.TLS.TLSConfig()
|
|
} else {
|
|
tlsConfig = &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
}
|
|
|
|
certPool := x509.NewCertPool()
|
|
for _, crt := range auth.GetRootCertificates() {
|
|
certPool.AddCert(crt)
|
|
}
|
|
|
|
// GetCertificate will only be called if the client supplies SNI
|
|
// information or if tlsConfig.Certificates is empty.
|
|
// When client requests are made using an IP address (as opposed to a domain
|
|
// name) the server does not receive any SNI and may fallback to using the
|
|
// first entry in the Certificates attribute; by setting the attribute to
|
|
// empty we are implicitly forcing GetCertificate to be the only mechanism
|
|
// by which the server can find it's own leaf Certificate.
|
|
tlsConfig.Certificates = []tls.Certificate{}
|
|
tlsConfig.GetCertificate = ca.renewer.GetCertificateForCA
|
|
|
|
// Add support for mutual tls to renew certificates
|
|
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
|
tlsConfig.ClientCAs = certPool
|
|
|
|
// Use server's most preferred ciphersuite
|
|
tlsConfig.PreferServerCipherSuites = true
|
|
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
func dumpRoutes(mux chi.Routes) {
|
|
// helpful routine for logging all routes //
|
|
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
|
fmt.Printf("%s %s\n", method, route)
|
|
return nil
|
|
}
|
|
if err := chi.Walk(mux, walkFunc); err != nil {
|
|
fmt.Printf("Logging err: %s\n", err.Error())
|
|
}
|
|
}
|