summaryrefslogtreecommitdiff
path: root/modules/caddypki
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2020-05-05 12:35:32 -0600
committerGitHub <noreply@github.com>2020-05-05 12:35:32 -0600
commit184e8e9f713bf39e82f4677452998bb003de6e6d (patch)
tree829aa87f9e05a4827638bf29da9c574c9a6249dd /modules/caddypki
parent1e8c9764df94c7b6549dc9f5be618cddc4573d1b (diff)
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
Diffstat (limited to 'modules/caddypki')
-rw-r--r--modules/caddypki/acmeserver/acmeserver.go165
-rw-r--r--modules/caddypki/ca.go63
-rw-r--r--modules/caddypki/command.go4
-rw-r--r--modules/caddypki/pki.go2
4 files changed, 230 insertions, 4 deletions
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 {