From 184e8e9f713bf39e82f4677452998bb003de6e6d Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 5 May 2020 12:35:32 -0600 Subject: pki: Embedded ACME server (#3198) * pki: Initial commit of embedded ACME server (#3021) * reverseproxy: Support auto-managed TLS client certificates (#3021) * A little cleanup after today's review session --- modules/caddypki/acmeserver/acmeserver.go | 165 ++++++++++++++++++++++++++++++ modules/caddypki/ca.go | 63 +++++++++++- modules/caddypki/command.go | 4 +- modules/caddypki/pki.go | 2 +- 4 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 modules/caddypki/acmeserver/acmeserver.go (limited to 'modules/caddypki') diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go new file mode 100644 index 0000000..8dc0f01 --- /dev/null +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -0,0 +1,165 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// 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 acmeserver + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddypki" + "github.com/go-chi/chi" + "github.com/smallstep/certificates/acme" + acmeAPI "github.com/smallstep/certificates/acme/api" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" + "github.com/smallstep/nosql" +) + +func init() { + caddy.RegisterModule(Handler{}) +} + +// Handler is an ACME server handler. +type Handler struct { + // The ID of the CA to use for signing. This refers to + // the ID given to the CA in the `pki` app. If omitted, + // the default ID is "local". + CA string `json:"ca,omitempty"` + + // The hostname or IP address by which ACME clients + // will access the server. This is used to populate + // the ACME directory endpoint. Default: localhost. + // TODO: this is probably not needed - check with smallstep + Host string `json:"host,omitempty"` + + // The path prefix under which to serve all ACME + // endpoints. All other requests will not be served + // by this handler and will be passed through to + // the next one. Default: "/acme/" + PathPrefix string `json:"path_prefix,omitempty"` + + acmeEndpoints http.Handler +} + +// CaddyModule returns the Caddy module information. +func (Handler) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.handlers.acme_server", + New: func() caddy.Module { return new(Handler) }, + } +} + +// Provision sets up the ACME server handler. +func (ash *Handler) Provision(ctx caddy.Context) error { + // set some defaults + if ash.CA == "" { + ash.CA = caddypki.DefaultCAID + } + if ash.Host == "" { + ash.Host = defaultHost + } + if ash.PathPrefix == "" { + ash.PathPrefix = defaultPathPrefix + } + + // get a reference to the configured CA + appModule, err := ctx.App("pki") + if err != nil { + return err + } + pkiApp := appModule.(*caddypki.PKI) + ca, ok := pkiApp.CAs[ash.CA] + if !ok { + return fmt.Errorf("no certificate authority configured with id: %s", ash.CA) + } + + dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", "db") + + // TODO: See https://github.com/smallstep/nosql/issues/7 + err = os.MkdirAll(dbFolder, 0755) + if err != nil { + return fmt.Errorf("making folder for ACME server database: %v", err) + } + + authorityConfig := caddypki.AuthorityConfig{ + AuthConfig: &authority.AuthConfig{ + Provisioners: provisioner.List{ + &provisioner.ACME{ + Name: ash.CA, + Type: provisioner.TypeACME.String(), + Claims: &provisioner.Claims{ + MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, + MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365}, + DefaultTLSDur: &provisioner.Duration{Duration: 12 * time.Hour}, + }, + }, + }, + }, + DB: &db.Config{ + Type: "badger", + DataSource: dbFolder, + }, + } + + auth, err := ca.NewAuthority(authorityConfig) + if err != nil { + return err + } + + acmeAuth, err := acme.NewAuthority( + auth.GetDatabase().(nosql.DB), // stores all the server state + ash.Host, // used for directory links; TODO: not needed + strings.Trim(ash.PathPrefix, "/"), // used for directory links + auth) // configures the signing authority + if err != nil { + return err + } + + // create the router for the ACME endpoints + acmeRouterHandler := acmeAPI.New(acmeAuth) + r := chi.NewRouter() + r.Route(ash.PathPrefix, func(r chi.Router) { + acmeRouterHandler.Route(r) + }) + ash.acmeEndpoints = r + + return nil +} + +func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + if strings.HasPrefix(r.URL.Path, ash.PathPrefix) { + ash.acmeEndpoints.ServeHTTP(w, r) + return nil + } + return next.ServeHTTP(w, r) +} + +const ( + defaultHost = "localhost" + defaultPathPrefix = "/acme/" +) + +// Interface guards +var ( + _ caddyhttp.MiddlewareHandler = (*Handler)(nil) + _ caddy.Provisioner = (*Handler)(nil) +) diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index 21a8bd5..610e7f6 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -15,6 +15,7 @@ package caddypki import ( + "crypto" "crypto/x509" "encoding/json" "fmt" @@ -24,6 +25,8 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/db" "github.com/smallstep/truststore" "go.uber.org/zap" ) @@ -171,6 +174,52 @@ func (ca CA) IntermediateKey() interface{} { return ca.interKey } +// NewAuthority returns a new Smallstep-powered signing authority for this CA. +func (ca CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authority, error) { + cfg := &authority.Config{ + // TODO: eliminate these placeholders / needless values + // see https://github.com/smallstep/certificates/issues/218 + Address: "placeholder_Address:1", + Root: []string{"placeholder_Root"}, + IntermediateCert: "placeholder_IntermediateCert", + IntermediateKey: "placeholder_IntermediateKey", + DNSNames: []string{"placeholder_DNSNames"}, + + AuthorityConfig: authorityConfig.AuthConfig, + DB: authorityConfig.DB, + } + // TODO: this also seems unnecessary, see above issue + if cfg.AuthorityConfig == nil { + cfg.AuthorityConfig = new(authority.AuthConfig) + } + + // get the root certificate and the issuer cert+key + rootCert := ca.RootCertificate() + var issuerCert *x509.Certificate + var issuerKey interface{} + if authorityConfig.SignWithRoot { + issuerCert = rootCert + var err error + issuerKey, err = ca.RootKey() + if err != nil { + return nil, fmt.Errorf("loading signing key: %v", err) + } + } else { + issuerCert = ca.IntermediateCertificate() + issuerKey = ca.IntermediateKey() + } + + auth, err := authority.New(cfg, + authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)), + authority.WithX509RootCerts(rootCert), + ) + if err != nil { + return nil, fmt.Errorf("initializing certificate authority: %v", err) + } + + return auth, nil +} + func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { rootCertPEM, err := ca.storage.Load(ca.storageKeyRootCert()) if err != nil { @@ -345,8 +394,20 @@ func (ca CA) installRoot() error { ) } +// AuthorityConfig is used to help a CA configure +// the underlying signing authority. +type AuthorityConfig struct { + SignWithRoot bool + + // TODO: should we just embed the underlying authority.Config struct type? + DB *db.Config + AuthConfig *authority.AuthConfig +} + const ( - defaultCAID = "local" + // DefaultCAID is the default CA ID. + DefaultCAID = "local" + defaultCAName = "Caddy Local Authority" defaultRootCommonName = "{pki.ca.name} - {time.now.year} ECC Root" defaultIntermediateCommonName = "{pki.ca.name} - ECC Intermediate" diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go index 9117f3f..34daefa 100644 --- a/modules/caddypki/command.go +++ b/modules/caddypki/command.go @@ -88,7 +88,7 @@ func cmdTrust(fs caddycmd.Flags) (int, error) { ca := CA{ storage: caddy.DefaultStorage, } - err := ca.Provision(ctx, defaultCAID, caddy.Log()) + err := ca.Provision(ctx, DefaultCAID, caddy.Log()) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -109,7 +109,7 @@ func cmdUntrust(fs caddycmd.Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments") } if ca == "" && cert == "" { - ca = defaultCAID + ca = DefaultCAID } if ca != "" { cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt") diff --git a/modules/caddypki/pki.go b/modules/caddypki/pki.go index f9aa372..7737079 100644 --- a/modules/caddypki/pki.go +++ b/modules/caddypki/pki.go @@ -52,7 +52,7 @@ func (p *PKI) Provision(ctx caddy.Context) error { // if this app is initialized at all, ensure there's // at least a default CA that can be used if len(p.CAs) == 0 { - p.CAs = map[string]*CA{defaultCAID: new(CA)} + p.CAs = map[string]*CA{DefaultCAID: new(CA)} } for caID, ca := range p.CAs { -- cgit v1.2.3