2020-05-05 21:35:32 +03:00
|
|
|
// 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"
|
2020-11-23 23:58:26 +03:00
|
|
|
"regexp"
|
2020-05-05 21:35:32 +03:00
|
|
|
"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"
|
2021-06-03 21:18:25 +03:00
|
|
|
acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql"
|
2020-05-05 21:35:32 +03:00
|
|
|
"github.com/smallstep/certificates/authority"
|
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
|
|
"github.com/smallstep/certificates/db"
|
|
|
|
"github.com/smallstep/nosql"
|
2020-11-23 23:03:58 +03:00
|
|
|
"go.uber.org/zap"
|
2020-05-05 21:35:32 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
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.
|
2020-06-03 18:59:36 +03:00
|
|
|
// COMPATIBILITY NOTE / TODO: This property may go away in the
|
|
|
|
// future, as it is currently only required due to
|
|
|
|
// limitations in the underlying library. Do not rely
|
|
|
|
// on this property long-term; check release notes.
|
2020-05-05 21:35:32 +03:00
|
|
|
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/"
|
2020-06-03 18:59:36 +03:00
|
|
|
// COMPATIBILITY NOTE / TODO: This property may go away in the
|
|
|
|
// future, as it is currently only required due to
|
|
|
|
// limitations in the underlying library. Do not rely
|
|
|
|
// on this property long-term; check release notes.
|
2020-05-05 21:35:32 +03:00
|
|
|
PathPrefix string `json:"path_prefix,omitempty"`
|
|
|
|
|
2021-02-27 05:27:58 +03:00
|
|
|
// If true, the CA's root will be the issuer instead of
|
|
|
|
// the intermediate. This is NOT recommended and should
|
|
|
|
// only be used when devices/clients do not properly
|
|
|
|
// validate certificate chains. EXPERIMENTAL: Might be
|
|
|
|
// changed or removed in the future.
|
|
|
|
SignWithRoot bool `json:"sign_with_root,omitempty"`
|
|
|
|
|
2020-05-05 21:35:32 +03:00
|
|
|
acmeEndpoints http.Handler
|
2020-11-23 23:58:26 +03:00
|
|
|
logger *zap.Logger
|
2020-05-05 21:35:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2020-11-23 23:58:26 +03:00
|
|
|
ash.logger = ctx.Logger(ash)
|
2020-05-05 21:35:32 +03:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2020-11-23 23:58:26 +03:00
|
|
|
database, err := ash.openDatabase()
|
2020-05-05 21:35:32 +03:00
|
|
|
if err != nil {
|
2020-11-23 23:58:26 +03:00
|
|
|
return err
|
2020-11-23 23:03:58 +03:00
|
|
|
}
|
|
|
|
|
2020-05-05 21:35:32 +03:00
|
|
|
authorityConfig := caddypki.AuthorityConfig{
|
2021-02-27 05:27:58 +03:00
|
|
|
SignWithRoot: ash.SignWithRoot,
|
2020-05-05 21:35:32 +03:00
|
|
|
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},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2020-11-23 23:58:26 +03:00
|
|
|
DB: database,
|
2020-05-05 21:35:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
auth, err := ca.NewAuthority(authorityConfig)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-06-03 21:18:25 +03:00
|
|
|
var acmeDB acme.DB
|
|
|
|
if authorityConfig.DB != nil {
|
|
|
|
acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("configuring ACME DB: %v", err)
|
|
|
|
}
|
2020-05-05 21:35:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// create the router for the ACME endpoints
|
2021-06-03 21:18:25 +03:00
|
|
|
acmeRouterHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{
|
|
|
|
CA: auth,
|
|
|
|
DB: acmeDB, // stores all the server state
|
|
|
|
DNS: ash.Host, // used for directory links; TODO: not needed (follow-up upstream with step-ca)
|
|
|
|
Prefix: ash.PathPrefix, // used for directory links
|
|
|
|
})
|
|
|
|
|
|
|
|
// extract its http.Handler so we can use it directly
|
2020-05-05 21:35:32 +03:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-11-23 23:58:26 +03:00
|
|
|
func (ash Handler) getDatabaseKey() string {
|
|
|
|
key := ash.CA
|
|
|
|
key = strings.ToLower(key)
|
|
|
|
key = strings.TrimSpace(key)
|
|
|
|
return keyCleaner.ReplaceAllLiteralString(key, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cleanup implements caddy.CleanerUpper and closes any idle databases.
|
|
|
|
func (ash Handler) Cleanup() error {
|
|
|
|
key := ash.getDatabaseKey()
|
|
|
|
deleted, err := databasePool.Delete(key)
|
|
|
|
if deleted {
|
|
|
|
ash.logger.Debug("unloading unused CA database", zap.String("db_key", key))
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
ash.logger.Error("closing CA database", zap.String("db_key", key), zap.Error(err))
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ash Handler) openDatabase() (*db.AuthDB, error) {
|
|
|
|
key := ash.getDatabaseKey()
|
|
|
|
database, loaded, err := databasePool.LoadOrNew(key, func() (caddy.Destructor, error) {
|
|
|
|
dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key)
|
|
|
|
dbPath := filepath.Join(dbFolder, "db")
|
|
|
|
|
|
|
|
err := os.MkdirAll(dbFolder, 0755)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("making folder for CA database: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
dbConfig := &db.Config{
|
|
|
|
Type: "bbolt",
|
|
|
|
DataSource: dbPath,
|
|
|
|
}
|
|
|
|
database, err := db.New(dbConfig)
|
|
|
|
return databaseCloser{&database}, err
|
|
|
|
})
|
|
|
|
|
|
|
|
if loaded {
|
|
|
|
ash.logger.Debug("loaded preexisting CA database", zap.String("db_key", key))
|
|
|
|
}
|
|
|
|
|
|
|
|
return database.(databaseCloser).DB, err
|
|
|
|
}
|
|
|
|
|
2020-05-05 21:35:32 +03:00
|
|
|
const (
|
|
|
|
defaultHost = "localhost"
|
|
|
|
defaultPathPrefix = "/acme/"
|
|
|
|
)
|
|
|
|
|
2020-11-23 23:58:26 +03:00
|
|
|
var keyCleaner = regexp.MustCompile(`[^\w.-_]`)
|
|
|
|
var databasePool = caddy.NewUsagePool()
|
|
|
|
|
|
|
|
type databaseCloser struct {
|
|
|
|
DB *db.AuthDB
|
|
|
|
}
|
|
|
|
|
|
|
|
func (closer databaseCloser) Destruct() error {
|
|
|
|
return (*closer.DB).Shutdown()
|
|
|
|
}
|
|
|
|
|
2020-05-05 21:35:32 +03:00
|
|
|
// Interface guards
|
|
|
|
var (
|
|
|
|
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
|
|
|
_ caddy.Provisioner = (*Handler)(nil)
|
|
|
|
)
|