2020-05-06 02:35:32 +08: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-24 04:58:26 +08:00
"regexp"
2020-05-06 02:35:32 +08: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-04 02:18:25 +08:00
acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql"
2020-05-06 02:35:32 +08:00
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"github.com/smallstep/nosql"
2020-11-24 04:03:58 +08:00
"go.uber.org/zap"
2020-05-06 02:35:32 +08: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" `
2022-12-06 15:12:26 +08:00
// The lifetime for issued certificates
Lifetime caddy . Duration ` json:"lifetime,omitempty" `
2020-05-06 02:35:32 +08:00
// The hostname or IP address by which ACME clients
// will access the server. This is used to populate
2021-07-02 07:20:51 +08:00
// the ACME directory endpoint. If not set, the Host
// header of the request will be used.
2020-06-03 23:59:36 +08:00
// COMPATIBILITY NOTE / TODO: This property may go away in the
2021-07-02 07:20:51 +08:00
// future. Do not rely on this property long-term; check release notes.
2020-05-06 02:35:32 +08: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
2021-07-02 07:20:51 +08:00
// the next one. Default: "/acme/".
2020-06-03 23:59:36 +08: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-06 02:35:32 +08:00
PathPrefix string ` json:"path_prefix,omitempty" `
2021-02-27 10:27:58 +08: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-06 02:35:32 +08:00
acmeEndpoints http . Handler
2020-11-24 04:58:26 +08:00
logger * zap . Logger
2020-05-06 02:35:32 +08: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 {
2022-09-17 06:55:30 +08:00
ash . logger = ctx . Logger ( )
2020-05-06 02:35:32 +08:00
// set some defaults
if ash . CA == "" {
ash . CA = caddypki . DefaultCAID
}
if ash . PathPrefix == "" {
ash . PathPrefix = defaultPathPrefix
}
2022-12-06 15:12:26 +08:00
if ash . Lifetime == 0 {
ash . Lifetime = caddy . Duration ( 12 * time . Hour )
}
2020-05-06 02:35:32 +08:00
// get a reference to the configured CA
appModule , err := ctx . App ( "pki" )
if err != nil {
return err
}
pkiApp := appModule . ( * caddypki . PKI )
2021-12-14 07:13:29 +08:00
ca , err := pkiApp . GetCA ( ctx , ash . CA )
2021-12-14 03:25:35 +08:00
if err != nil {
return err
2020-05-06 02:35:32 +08:00
}
2022-12-06 15:12:26 +08:00
// make sure leaf cert lifetime is less than the intermediate cert lifetime. this check only
// applies for caddy-managed intermediate certificates
if ca . Intermediate == nil && ash . Lifetime >= ca . IntermediateLifetime {
return fmt . Errorf ( "certificate lifetime (%s) should be less than intermediate certificate lifetime (%s)" , time . Duration ( ash . Lifetime ) , time . Duration ( ca . IntermediateLifetime ) )
}
2020-11-24 04:58:26 +08:00
database , err := ash . openDatabase ( )
2020-05-06 02:35:32 +08:00
if err != nil {
2020-11-24 04:58:26 +08:00
return err
2020-11-24 04:03:58 +08:00
}
2020-05-06 02:35:32 +08:00
authorityConfig := caddypki . AuthorityConfig {
2021-02-27 10:27:58 +08:00
SignWithRoot : ash . SignWithRoot ,
2020-05-06 02:35:32 +08: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 } ,
2022-12-06 15:12:26 +08:00
DefaultTLSDur : & provisioner . Duration { Duration : time . Duration ( ash . Lifetime ) } ,
2020-05-06 02:35:32 +08:00
} ,
} ,
} ,
} ,
2020-11-24 04:58:26 +08:00
DB : database ,
2020-05-06 02:35:32 +08:00
}
auth , err := ca . NewAuthority ( authorityConfig )
if err != nil {
return err
}
2021-06-04 02:18:25 +08: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-06 02:35:32 +08:00
}
// create the router for the ACME endpoints
2021-06-04 02:18:25 +08:00
acmeRouterHandler := acmeAPI . NewHandler ( acmeAPI . HandlerOptions {
CA : auth ,
2021-06-22 01:56:41 +08:00
DB : acmeDB , // stores all the server state
2021-07-02 07:20:51 +08:00
DNS : ash . Host , // used for directory links
2021-06-22 01:56:41 +08:00
Prefix : strings . Trim ( ash . PathPrefix , "/" ) , // used for directory links
2021-06-04 02:18:25 +08:00
} )
// extract its http.Handler so we can use it directly
2020-05-06 02:35:32 +08: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-24 04:58:26 +08: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
}
2021-07-02 07:20:51 +08:00
const defaultPathPrefix = "/acme/"
2020-05-06 02:35:32 +08:00
2020-11-24 04:58:26 +08: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-06 02:35:32 +08:00
// Interface guards
var (
_ caddyhttp . MiddlewareHandler = ( * Handler ) ( nil )
_ caddy . Provisioner = ( * Handler ) ( nil )
)