certificates/ca/ca.go

517 lines
15 KiB
Go
Raw Normal View History

2018-10-05 21:48:36 +00:00
package ca
import (
"crypto/tls"
"crypto/x509"
2019-05-27 00:41:10 +00:00
"fmt"
"log"
2018-10-05 21:48:36 +00:00
"net/http"
2019-05-27 00:41:10 +00:00
"net/url"
"reflect"
2022-04-04 19:16:37 +00:00
"strings"
"sync"
2018-10-05 21:48:36 +00:00
"github.com/go-chi/chi"
"github.com/pkg/errors"
2019-05-27 00:41:10 +00:00
"github.com/smallstep/certificates/acme"
acmeAPI "github.com/smallstep/certificates/acme/api"
2021-03-05 07:10:46 +00:00
acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority"
adminAPI "github.com/smallstep/certificates/authority/admin/api"
2021-05-03 19:48:20 +00:00
"github.com/smallstep/certificates/authority/config"
"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"
2019-05-27 00:41:10 +00:00
"github.com/smallstep/nosql"
2022-04-04 19:29:27 +00:00
"go.step.sm/cli-utils/step"
2022-04-04 19:16:37 +00:00
"go.step.sm/crypto/x509util"
2018-10-05 21:48:36 +00:00
)
type options struct {
configFile string
linkedCAToken string
2022-03-22 02:55:21 +00:00
quiet bool
password []byte
issuerPassword []byte
sshHostPassword []byte
sshUserPassword []byte
database db.AuthDB
2018-10-05 21:48:36 +00:00
}
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
}
}
// WithSSHHostPassword sets the given password to decrypt the key used to sign
// ssh host certificates.
func WithSSHHostPassword(password []byte) Option {
return func(o *options) {
o.sshHostPassword = password
}
}
// WithSSHUserPassword sets the given password to decrypt the key used to sign
// ssh user certificates.
func WithSSHUserPassword(password []byte) Option {
return func(o *options) {
o.sshUserPassword = password
}
}
2021-03-25 18:06:37 +00:00
// WithIssuerPassword sets the given password as the configured certificate
// issuer password in the CA options.
func WithIssuerPassword(password []byte) Option {
return func(o *options) {
o.issuerPassword = password
}
}
// WithDatabase sets the given authority database to the CA options.
func WithDatabase(d db.AuthDB) Option {
return func(o *options) {
o.database = d
}
}
// WithLinkedCAToken sets the token used to authenticate with the linkedca.
func WithLinkedCAToken(token string) Option {
return func(o *options) {
o.linkedCAToken = token
}
}
2022-03-22 02:55:21 +00:00
// WithQuiet sets the quiet flag.
func WithQuiet(quiet bool) Option {
return func(o *options) {
o.quiet = quiet
}
}
2018-10-05 21:48:36 +00:00
// 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 *config.Config
srv *server.Server
insecureSrv *server.Server
opts *options
renewer *TLSRenewer
2018-10-05 21:48:36 +00:00
}
// New creates and initializes the CA with the given configuration and options.
func New(cfg *config.Config, opts ...Option) (*CA, error) {
2018-10-05 21:48:36 +00:00
ca := &CA{
config: cfg,
2018-10-05 21:48:36 +00:00
opts: new(options),
}
ca.opts.apply(opts)
return ca.Init(cfg)
2018-10-05 21:48:36 +00:00
}
// Init initializes the CA with the given configuration.
func (ca *CA) Init(cfg *config.Config) (*CA, error) {
// Set password, it's ok to set nil password, the ca will prompt for them if
// they are required.
opts := []authority.Option{
authority.WithPassword(ca.opts.password),
authority.WithSSHHostPassword(ca.opts.sshHostPassword),
authority.WithSSHUserPassword(ca.opts.sshUserPassword),
authority.WithIssuerPassword(ca.opts.issuerPassword),
}
if ca.opts.linkedCAToken != "" {
opts = append(opts, authority.WithLinkedCAToken(ca.opts.linkedCAToken))
}
if ca.opts.database != nil {
opts = append(opts, authority.WithDatabase(ca.opts.database))
}
auth, err := authority.New(cfg, opts...)
2018-10-05 21:48:36 +00:00
if err != nil {
return nil, err
}
ca.auth = auth
2018-10-05 21:48:36 +00:00
tlsConfig, err := ca.getTLSConfig(auth)
if err != nil {
return nil, err
}
// Using chi as the main router
mux := chi.NewRouter()
handler := http.Handler(mux)
insecureMux := chi.NewRouter()
insecureHandler := http.Handler(insecureMux)
2019-05-27 00:41:10 +00:00
// Add regular CA api endpoints in / and /1.0
2018-10-05 21:48:36 +00:00
routerHandler := api.New(auth)
routerHandler.Route(mux)
mux.Route("/1.0", func(r chi.Router) {
routerHandler.Route(r)
})
2019-05-27 00:41:10 +00:00
//Add ACME api endpoints in /acme and /1.0/acme
dns := cfg.DNSNames[0]
u, err := url.Parse("https://" + cfg.Address)
2019-05-27 00:41:10 +00:00
if err != nil {
return nil, err
}
port := u.Port()
if port != "" && port != "443" {
dns = fmt.Sprintf("%s:%s", dns, port)
}
2021-05-07 00:03:12 +00:00
// ACME Router
2019-05-27 00:41:10 +00:00
prefix := "acme"
2021-03-25 03:46:02 +00:00
var acmeDB acme.DB
if cfg.DB == nil {
2021-03-25 03:46:02 +00:00
acmeDB = nil
} else {
acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB))
2021-03-25 03:46:02 +00:00
if err != nil {
return nil, errors.Wrap(err, "error configuring ACME DB interface")
}
2021-03-06 21:06:43 +00:00
}
acmeHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{
Backdate: *cfg.AuthorityConfig.Backdate,
2021-03-06 21:06:43 +00:00
DB: acmeDB,
DNS: dns,
Prefix: prefix,
2021-03-05 07:10:46 +00:00
CA: auth,
})
2019-05-27 00:41:10 +00:00
mux.Route("/"+prefix, func(r chi.Router) {
2021-03-06 21:06:43 +00:00
acmeHandler.Route(r)
2019-05-27 00:41:10 +00:00
})
// 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) {
2021-03-06 21:06:43 +00:00
acmeHandler.Route(r)
2019-05-27 00:41:10 +00:00
})
2021-05-20 20:01:58 +00:00
// Admin API Router
if cfg.AuthorityConfig.EnableAdmin {
adminDB := auth.GetAdminDatabase()
if adminDB != nil {
2022-02-08 12:26:30 +00:00
acmeAdminResponder := adminAPI.NewACMEAdminResponder()
policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB, acmeDB, cfg.AuthorityConfig.DeploymentType)
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder, policyAdminResponder)
mux.Route("/admin", func(r chi.Router) {
adminHandler.Route(r)
})
}
2021-05-07 00:03:12 +00:00
}
if ca.shouldServeSCEPEndpoints() {
scepPrefix := "scep"
scepAuthority, err := scep.New(auth, scep.AuthorityOptions{
Service: auth.GetSCEPService(),
DNS: dns,
Prefix: scepPrefix,
})
if err != nil {
return nil, errors.Wrap(err, "error creating SCEP authority")
}
scepRouterHandler := scepAPI.New(scepAuthority)
// According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10),
// SCEP operations are performed using HTTP, so that's why the API is mounted
// to the insecure mux.
insecureMux.Route("/"+scepPrefix, func(r chi.Router) {
scepRouterHandler.Route(r)
})
// The RFC also mentions usage of HTTPS, but seems to advise
// against it, because of potential interoperability issues.
// Currently I think it's not bad to use HTTPS also, so that's
// why I've kept the API endpoints in both muxes and both HTTP
// as well as HTTPS can be used to request certificates
// using SCEP.
mux.Route("/"+scepPrefix, func(r chi.Router) {
scepRouterHandler.Route(r)
})
}
// helpful routine for logging all routes
//dumpRoutes(mux)
2019-05-27 00:41:10 +00:00
2018-10-05 21:48:36 +00:00
// Add monitoring if configured
if len(cfg.Monitoring) > 0 {
m, err := monitoring.New(cfg.Monitoring)
2018-10-05 21:48:36 +00:00
if err != nil {
return nil, err
}
handler = m.Middleware(handler)
insecureHandler = m.Middleware(insecureHandler)
2018-10-05 21:48:36 +00:00
}
// Add logger if configured
if len(cfg.Logger) > 0 {
logger, err := logging.New("ca", cfg.Logger)
2018-10-05 21:48:36 +00:00
if err != nil {
return nil, err
}
handler = logger.Middleware(handler)
insecureHandler = logger.Middleware(insecureHandler)
2018-10-05 21:48:36 +00:00
}
ca.srv = server.New(cfg.Address, handler, tlsConfig)
2021-03-21 15:42:41 +00:00
// only start the insecure server if the insecure address is configured
// and, currently, also only when it should serve SCEP endpoints.
if ca.shouldServeSCEPEndpoints() && cfg.InsecureAddress != "" {
// 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.
ca.insecureSrv = server.New(cfg.InsecureAddress, insecureHandler, nil)
2021-03-21 15:42:41 +00:00
}
2018-10-05 21:48:36 +00:00
return ca, nil
}
// Run starts the CA calling to the server ListenAndServe method.
func (ca *CA) Run() error {
var wg sync.WaitGroup
errs := make(chan error, 1)
2022-03-22 02:55:21 +00:00
if !ca.opts.quiet {
2022-03-30 23:07:16 +00:00
authorityInfo := ca.auth.GetInfo()
2022-04-05 17:59:25 +00:00
log.Printf("Starting %s", step.Version())
2022-04-04 19:29:27 +00:00
log.Printf("Documentation: https://u.step.sm/docs/ca")
log.Printf("Community Discord: https://u.step.sm/discord")
2022-04-05 17:59:25 +00:00
if step.Contexts().GetCurrent() != nil {
log.Printf("Current context: %s", step.Contexts().GetCurrent().Name)
}
2022-04-04 19:29:27 +00:00
log.Printf("Config file: %s", ca.opts.configFile)
2022-04-05 17:59:25 +00:00
baseURL := fmt.Sprintf("https://%s%s",
2022-04-04 19:16:37 +00:00
authorityInfo.DNSNames[0],
ca.config.Address[strings.LastIndex(ca.config.Address, ":"):])
2022-04-05 17:59:25 +00:00
log.Printf("The primary server URL is %s", baseURL)
log.Printf("Root certificates are available at %s/roots.pem", baseURL)
2022-04-04 19:16:37 +00:00
if len(authorityInfo.DNSNames) > 1 {
log.Printf("Additional configured hostnames: %s",
strings.Join(authorityInfo.DNSNames[1:], ", "))
}
2022-03-30 22:48:42 +00:00
for _, crt := range authorityInfo.RootX509Certs {
log.Printf("X.509 Root Fingerprint: %s", x509util.Fingerprint(crt))
2022-03-30 22:48:42 +00:00
}
2022-03-30 23:05:26 +00:00
if authorityInfo.SSHCAHostPublicKey != nil {
log.Printf("SSH Host CA Key: %s\n", authorityInfo.SSHCAHostPublicKey)
2022-03-30 22:48:42 +00:00
}
2022-03-30 23:05:26 +00:00
if authorityInfo.SSHCAUserPublicKey != nil {
log.Printf("SSH User CA Key: %s\n", authorityInfo.SSHCAUserPublicKey)
2022-03-30 22:48:42 +00:00
}
2022-03-22 02:55:21 +00:00
}
if ca.insecureSrv != nil {
wg.Add(1)
go func() {
defer wg.Done()
errs <- ca.insecureSrv.ListenAndServe()
}()
}
wg.Add(1)
go func() {
defer wg.Done()
errs <- ca.srv.ListenAndServe()
}()
// wait till error occurs; ensures the servers keep listening
err := <-errs
wg.Wait()
return err
2018-10-05 21:48:36 +00:00
}
// Stop stops the CA calling to the server Shutdown method.
func (ca *CA) Stop() error {
2018-11-27 23:57:13 +00:00
ca.renewer.Stop()
if err := ca.auth.Shutdown(); err != nil {
log.Printf("error stopping ca.Authority: %+v\n", err)
}
var insecureShutdownErr error
if ca.insecureSrv != nil {
insecureShutdownErr = ca.insecureSrv.Shutdown()
}
secureErr := ca.srv.Shutdown()
if insecureShutdownErr != nil {
return insecureShutdownErr
}
return secureErr
2018-10-05 21:48:36 +00:00
}
// Reload reloads the configuration of the CA and calls to the server Reload
// method.
func (ca *CA) Reload() error {
cfg, err := config.LoadConfiguration(ca.opts.configFile)
2018-10-05 21:48:36 +00:00
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.")
2018-10-05 21:48:36 +00:00
}
// Do not allow reload if the database configuration has changed.
if !reflect.DeepEqual(ca.config.DB, cfg.DB) {
logContinue("Reload failed because the database configuration has changed.")
return errors.New("error reloading ca: database configuration cannot change")
}
newCA, err := New(cfg,
WithPassword(ca.opts.password),
WithSSHHostPassword(ca.opts.sshHostPassword),
WithSSHUserPassword(ca.opts.sshUserPassword),
WithIssuerPassword(ca.opts.issuerPassword),
WithLinkedCAToken(ca.opts.linkedCAToken),
2022-03-22 02:55:21 +00:00
WithQuiet(ca.opts.quiet),
WithConfigFile(ca.opts.configFile),
WithDatabase(ca.auth.GetDatabase()),
)
2018-10-05 21:48:36 +00:00
if err != nil {
logContinue("Reload failed because the CA with new configuration could not be initialized.")
2018-10-05 21:48:36 +00:00
return errors.Wrap(err, "error reloading ca")
}
if ca.insecureSrv != nil {
if err = ca.insecureSrv.Reload(newCA.insecureSrv); err != nil {
logContinue("Reload failed because insecure server could not be replaced.")
return errors.Wrap(err, "error reloading insecure server")
}
}
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
2021-02-12 01:38:14 +00:00
// 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
2018-10-05 21:48:36 +00:00
}
// 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,
}
}
// 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{}
2018-11-27 23:57:13 +00:00
tlsConfig.GetCertificate = ca.renewer.GetCertificateForCA
2018-10-05 21:48:36 +00:00
2022-01-19 10:31:33 +00:00
// initialize a certificate pool with root CA certificates to trust when doing mTLS.
certPool := x509.NewCertPool()
for _, crt := range auth.GetRootCertificates() {
certPool.AddCert(crt)
}
2022-01-19 10:31:33 +00:00
// adding the intermediate CA certificates to the pool will allow clients that
// do mTLS but don't send an intermediate to successfully connect. The intermediates
// added here are used when building a certificate chain.
intermediates := tlsCrt.Certificate[1:]
for _, certBytes := range intermediates {
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
certPool.AddCert(cert)
}
2018-10-05 21:48:36 +00:00
// Add support for mutual tls to renew certificates
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
tlsConfig.ClientCAs = certPool
return tlsConfig, nil
}
2021-11-26 16:27:42 +00:00
// shouldServeSCEPEndpoints returns if the CA should be
// configured with endpoints for SCEP. This is assumed to be
// true if a SCEPService exists, which is true in case a
// SCEP provisioner was configured.
func (ca *CA) shouldServeSCEPEndpoints() bool {
return ca.auth.GetSCEPService() != nil
}
//nolint // ignore linters to allow keeping this function around for debugging
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())
}
}