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 (
2023-05-04 01:07:22 +08:00
"context"
2020-05-06 02:35:32 +08:00
"fmt"
2023-05-04 01:07:22 +08:00
weakrand "math/rand"
"net"
2020-05-06 02:35:32 +08:00
"net/http"
"os"
"path/filepath"
2020-11-24 04:58:26 +08:00
"regexp"
2020-05-06 02:35:32 +08:00
"strings"
"time"
2023-10-24 09:02:11 +08:00
"github.com/go-chi/chi/v5"
2020-05-06 02:35:32 +08:00
"github.com/smallstep/certificates/acme"
2023-05-04 01:07:22 +08:00
"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"
2023-08-14 23:41:15 +08:00
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki"
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" `
2023-05-04 01:07:22 +08:00
// The addresses of DNS resolvers to use when looking up
// the TXT records for solving DNS challenges.
// It accepts [network addresses](/docs/conventions#network-addresses)
// with port range of only 1. If the host is an IP address,
// it will be dialed directly to resolve the upstream server.
// If the host is not an IP address, the addresses are resolved
// using the [name resolution convention](https://golang.org/pkg/net/#hdr-Name_Resolution)
// of the Go standard library. If the array contains more
// than 1 resolver address, one is chosen at random.
Resolvers [ ] string ` json:"resolvers,omitempty" `
2024-02-08 16:42:03 +08:00
// Specify the set of enabled ACME challenges. An empty or absent value
// means all challenges are enabled. Accepted values are:
// "http-01", "dns-01", "tls-alpn-01"
Challenges ACMEChallenges ` json:"challenges,omitempty" `
2023-05-04 01:07:22 +08:00
logger * zap . Logger
resolvers [ ] caddy . NetworkAddress
ctx caddy . Context
acmeDB acme . DB
acmeAuth * authority . Authority
acmeClient acme . Client
acmeLinker acme . Linker
2020-05-06 02:35:32 +08:00
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 {
2023-05-04 01:07:22 +08:00
ash . ctx = ctx
2022-09-17 06:55:30 +08:00
ash . logger = ctx . Logger ( )
2023-05-04 01:07:22 +08:00
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 )
}
2024-02-08 16:42:03 +08:00
if len ( ash . Challenges ) > 0 {
if err := ash . Challenges . validate ( ) ; err != nil {
return err
}
}
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 {
2024-02-08 16:42:03 +08:00
Name : ash . CA ,
Challenges : ash . Challenges . toSmallstepType ( ) ,
Type : provisioner . TypeACME . String ( ) ,
2020-05-06 02:35:32 +08:00
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
}
2023-05-04 01:07:22 +08:00
ash . acmeAuth , err = ca . NewAuthority ( authorityConfig )
2020-05-06 02:35:32 +08:00
if err != nil {
return err
}
2023-05-04 01:07:22 +08:00
ash . acmeDB , err = acmeNoSQL . New ( ash . acmeAuth . GetDatabase ( ) . ( nosql . DB ) )
if err != nil {
return fmt . Errorf ( "configuring ACME DB: %v" , err )
2020-05-06 02:35:32 +08:00
}
2023-05-04 01:07:22 +08:00
ash . acmeClient , err = ash . makeClient ( )
if err != nil {
return err
}
ash . acmeLinker = acme . NewLinker (
ash . Host ,
strings . Trim ( ash . PathPrefix , "/" ) ,
)
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 ) {
2023-05-04 01:07:22 +08:00
api . Route ( r )
2020-05-06 02:35:32 +08:00
} )
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 ) {
2023-05-04 01:07:22 +08:00
acmeCtx := acme . NewContext (
r . Context ( ) ,
ash . acmeDB ,
ash . acmeClient ,
ash . acmeLinker ,
nil ,
)
acmeCtx = authority . NewContext ( acmeCtx , ash . acmeAuth )
r = r . WithContext ( acmeCtx )
2020-05-06 02:35:32 +08:00
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" )
2023-08-08 03:40:31 +08:00
err := os . MkdirAll ( dbFolder , 0 o755 )
2020-11-24 04:58:26 +08:00
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
}
2023-05-04 01:07:22 +08:00
// makeClient creates an ACME client which will use a custom
// resolver instead of net.DefaultResolver.
func ( ash Handler ) makeClient ( ) ( acme . Client , error ) {
for _ , v := range ash . Resolvers {
addr , err := caddy . ParseNetworkAddressWithDefaults ( v , "udp" , 53 )
if err != nil {
return nil , err
}
if addr . PortRangeSize ( ) != 1 {
return nil , fmt . Errorf ( "resolver address must have exactly one address; cannot call %v" , addr )
}
ash . resolvers = append ( ash . resolvers , addr )
}
var resolver * net . Resolver
if len ( ash . resolvers ) != 0 {
dialer := & net . Dialer {
Timeout : 2 * time . Second ,
}
resolver = & net . Resolver {
PreferGo : true ,
Dial : func ( ctx context . Context , network , address string ) ( net . Conn , error ) {
//nolint:gosec
addr := ash . resolvers [ weakrand . Intn ( len ( ash . resolvers ) ) ]
return dialer . DialContext ( ctx , addr . Network , addr . JoinHostPort ( 0 ) )
} ,
}
} else {
resolver = net . DefaultResolver
}
return resolverClient {
Client : acme . NewClient ( ) ,
resolver : resolver ,
ctx : ash . ctx ,
} , nil
}
type resolverClient struct {
acme . Client
resolver * net . Resolver
ctx context . Context
}
func ( c resolverClient ) LookupTxt ( name string ) ( [ ] string , error ) {
return c . resolver . LookupTXT ( c . ctx , name )
}
2021-07-02 07:20:51 +08:00
const defaultPathPrefix = "/acme/"
2020-05-06 02:35:32 +08:00
2023-08-08 03:40:31 +08:00
var (
keyCleaner = regexp . MustCompile ( ` [^\w.-_] ` )
databasePool = caddy . NewUsagePool ( )
)
2020-11-24 04:58:26 +08:00
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 )
)