366 lines
10 KiB
Go
366 lines
10 KiB
Go
// Package serve implements the serve command for CFSSL's API.
|
|
package serve
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
rice "github.com/GeertJohan/go.rice"
|
|
"github.com/cloudflare/cfssl/api"
|
|
"github.com/cloudflare/cfssl/api/bundle"
|
|
"github.com/cloudflare/cfssl/api/certinfo"
|
|
"github.com/cloudflare/cfssl/api/crl"
|
|
"github.com/cloudflare/cfssl/api/gencrl"
|
|
"github.com/cloudflare/cfssl/api/generator"
|
|
"github.com/cloudflare/cfssl/api/info"
|
|
"github.com/cloudflare/cfssl/api/initca"
|
|
apiocsp "github.com/cloudflare/cfssl/api/ocsp"
|
|
"github.com/cloudflare/cfssl/api/revoke"
|
|
"github.com/cloudflare/cfssl/api/scan"
|
|
"github.com/cloudflare/cfssl/api/signhandler"
|
|
"github.com/cloudflare/cfssl/bundler"
|
|
"github.com/cloudflare/cfssl/certdb/dbconf"
|
|
certsql "github.com/cloudflare/cfssl/certdb/sql"
|
|
"github.com/cloudflare/cfssl/cli"
|
|
ocspsign "github.com/cloudflare/cfssl/cli/ocspsign"
|
|
"github.com/cloudflare/cfssl/cli/sign"
|
|
"github.com/cloudflare/cfssl/helpers"
|
|
"github.com/cloudflare/cfssl/log"
|
|
"github.com/cloudflare/cfssl/ocsp"
|
|
"github.com/cloudflare/cfssl/signer"
|
|
"github.com/cloudflare/cfssl/ubiquity"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// Usage text of 'cfssl serve'
|
|
var serverUsageText = `cfssl serve -- set up a HTTP server handles CF SSL requests
|
|
|
|
Usage of serve:
|
|
cfssl serve [-address address] [-ca cert] [-ca-bundle bundle] \
|
|
[-ca-key key] [-int-bundle bundle] [-int-dir dir] [-port port] \
|
|
[-metadata file] [-remote remote_host] [-config config] \
|
|
[-responder cert] [-responder-key key] [-tls-cert cert] [-tls-key key] \
|
|
[-mutual-tls-ca ca] [-mutual-tls-cn regex] \
|
|
[-tls-remote-ca ca] [-mutual-tls-client-cert cert] [-mutual-tls-client-key key] \
|
|
[-db-config db-config]
|
|
|
|
Flags:
|
|
`
|
|
|
|
// Flags used by 'cfssl serve'
|
|
var serverFlags = []string{"address", "port", "ca", "ca-key", "ca-bundle", "int-bundle", "int-dir", "metadata",
|
|
"remote", "config", "responder", "responder-key", "tls-key", "tls-cert", "mutual-tls-ca", "mutual-tls-cn",
|
|
"tls-remote-ca", "mutual-tls-client-cert", "mutual-tls-client-key", "db-config"}
|
|
|
|
var (
|
|
conf cli.Config
|
|
s signer.Signer
|
|
ocspSigner ocsp.Signer
|
|
db *sqlx.DB
|
|
)
|
|
|
|
// V1APIPrefix is the prefix of all CFSSL V1 API Endpoints.
|
|
var V1APIPrefix = "/api/v1/cfssl/"
|
|
|
|
// v1APIPath prepends the V1 API prefix to endpoints not beginning with "/"
|
|
func v1APIPath(path string) string {
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = V1APIPrefix + path
|
|
}
|
|
return (&url.URL{Path: path}).String()
|
|
}
|
|
|
|
// httpBox implements http.FileSystem which allows the use of Box with a http.FileServer.
|
|
// Atempting to Open an API endpoint will result in an error.
|
|
type httpBox struct {
|
|
*rice.Box
|
|
redirects map[string]string
|
|
}
|
|
|
|
func (hb *httpBox) findStaticBox() (err error) {
|
|
hb.Box, err = rice.FindBox("static")
|
|
return
|
|
}
|
|
|
|
// Open returns a File for non-API enpoints using the http.File interface.
|
|
func (hb *httpBox) Open(name string) (http.File, error) {
|
|
if strings.HasPrefix(name, V1APIPrefix) {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
if location, ok := hb.redirects[name]; ok {
|
|
return hb.Box.Open(location)
|
|
}
|
|
|
|
return hb.Box.Open(name)
|
|
}
|
|
|
|
// staticBox is the box containing all static assets.
|
|
var staticBox = &httpBox{
|
|
redirects: map[string]string{
|
|
"/scan": "/index.html",
|
|
"/bundle": "/index.html",
|
|
},
|
|
}
|
|
|
|
var errBadSigner = errors.New("signer not initialized")
|
|
var errNoCertDBConfigured = errors.New("cert db not configured (missing -db-config)")
|
|
|
|
var endpoints = map[string]func() (http.Handler, error){
|
|
"sign": func() (http.Handler, error) {
|
|
if s == nil {
|
|
return nil, errBadSigner
|
|
}
|
|
|
|
h, err := signhandler.NewHandlerFromSigner(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if conf.CABundleFile != "" && conf.IntBundleFile != "" {
|
|
sh := h.Handler.(*signhandler.Handler)
|
|
if err := sh.SetBundler(conf.CABundleFile, conf.IntBundleFile); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return h, nil
|
|
},
|
|
|
|
"authsign": func() (http.Handler, error) {
|
|
if s == nil {
|
|
return nil, errBadSigner
|
|
}
|
|
|
|
h, err := signhandler.NewAuthHandlerFromSigner(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if conf.CABundleFile != "" && conf.IntBundleFile != "" {
|
|
sh := h.(*api.HTTPHandler).Handler.(*signhandler.AuthHandler)
|
|
if err := sh.SetBundler(conf.CABundleFile, conf.IntBundleFile); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return h, nil
|
|
},
|
|
|
|
"info": func() (http.Handler, error) {
|
|
if s == nil {
|
|
return nil, errBadSigner
|
|
}
|
|
return info.NewHandler(s)
|
|
},
|
|
|
|
"crl": func() (http.Handler, error) {
|
|
if s == nil {
|
|
return nil, errBadSigner
|
|
}
|
|
|
|
if db == nil {
|
|
return nil, errNoCertDBConfigured
|
|
}
|
|
|
|
return crl.NewHandler(certsql.NewAccessor(db), conf.CAFile, conf.CAKeyFile)
|
|
},
|
|
|
|
"gencrl": func() (http.Handler, error) {
|
|
if s == nil {
|
|
return nil, errBadSigner
|
|
}
|
|
return gencrl.NewHandler(), nil
|
|
},
|
|
|
|
"newcert": func() (http.Handler, error) {
|
|
if s == nil {
|
|
return nil, errBadSigner
|
|
}
|
|
h := generator.NewCertGeneratorHandlerFromSigner(generator.CSRValidate, s)
|
|
if conf.CABundleFile != "" && conf.IntBundleFile != "" {
|
|
cg := h.(api.HTTPHandler).Handler.(*generator.CertGeneratorHandler)
|
|
if err := cg.SetBundler(conf.CABundleFile, conf.IntBundleFile); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return h, nil
|
|
},
|
|
|
|
"bundle": func() (http.Handler, error) {
|
|
return bundle.NewHandler(conf.CABundleFile, conf.IntBundleFile)
|
|
},
|
|
|
|
"newkey": func() (http.Handler, error) {
|
|
return generator.NewHandler(generator.CSRValidate)
|
|
},
|
|
|
|
"init_ca": func() (http.Handler, error) {
|
|
return initca.NewHandler(), nil
|
|
},
|
|
|
|
"scan": func() (http.Handler, error) {
|
|
return scan.NewHandler(conf.CABundleFile)
|
|
},
|
|
|
|
"scaninfo": func() (http.Handler, error) {
|
|
return scan.NewInfoHandler(), nil
|
|
},
|
|
|
|
"certinfo": func() (http.Handler, error) {
|
|
return certinfo.NewHandler(), nil
|
|
},
|
|
|
|
"ocspsign": func() (http.Handler, error) {
|
|
if ocspSigner == nil {
|
|
return nil, errBadSigner
|
|
}
|
|
return apiocsp.NewHandler(ocspSigner), nil
|
|
},
|
|
|
|
"revoke": func() (http.Handler, error) {
|
|
if db == nil {
|
|
return nil, errNoCertDBConfigured
|
|
}
|
|
return revoke.NewHandler(certsql.NewAccessor(db)), nil
|
|
},
|
|
|
|
"/": func() (http.Handler, error) {
|
|
if err := staticBox.findStaticBox(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return http.FileServer(staticBox), nil
|
|
},
|
|
}
|
|
|
|
// registerHandlers instantiates various handlers and associate them to corresponding endpoints.
|
|
func registerHandlers() {
|
|
for path, getHandler := range endpoints {
|
|
log.Debugf("getHandler for %s", path)
|
|
if handler, err := getHandler(); err != nil {
|
|
log.Warningf("endpoint '%s' is disabled: %v", path, err)
|
|
} else {
|
|
if path, handler, err = wrapHandler(path, handler, err); err != nil {
|
|
log.Warningf("endpoint '%s' is disabled by wrapper: %v", path, err)
|
|
} else {
|
|
log.Infof("endpoint '%s' is enabled", path)
|
|
http.Handle(path, handler)
|
|
}
|
|
}
|
|
}
|
|
log.Info("Handler set up complete.")
|
|
}
|
|
|
|
// serverMain is the command line entry point to the API server. It sets up a
|
|
// new HTTP server to handle sign, bundle, and validate requests.
|
|
func serverMain(args []string, c cli.Config) error {
|
|
conf = c
|
|
// serve doesn't support arguments.
|
|
if len(args) > 0 {
|
|
return errors.New("argument is provided but not defined; please refer to the usage by flag -h")
|
|
}
|
|
|
|
bundler.IntermediateStash = conf.IntDir
|
|
var err error
|
|
|
|
if err = ubiquity.LoadPlatforms(conf.Metadata); err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.DBConfigFile != "" {
|
|
db, err = dbconf.DBFromConfig(c.DBConfigFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
log.Info("Initializing signer")
|
|
|
|
if s, err = sign.SignerFromConfigAndDB(c, db); err != nil {
|
|
log.Warningf("couldn't initialize signer: %v", err)
|
|
}
|
|
|
|
if ocspSigner, err = ocspsign.SignerFromConfig(c); err != nil {
|
|
log.Warningf("couldn't initialize ocsp signer: %v", err)
|
|
}
|
|
|
|
registerHandlers()
|
|
|
|
addr := net.JoinHostPort(conf.Address, strconv.Itoa(conf.Port))
|
|
|
|
if conf.TLSCertFile == "" || conf.TLSKeyFile == "" {
|
|
log.Info("Now listening on ", addr)
|
|
return http.ListenAndServe(addr, nil)
|
|
}
|
|
if conf.MutualTLSCAFile != "" {
|
|
clientPool, err := helpers.LoadPEMCertPool(conf.MutualTLSCAFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load mutual TLS CA file: %s", err)
|
|
}
|
|
|
|
server := http.Server{
|
|
Addr: addr,
|
|
TLSConfig: &tls.Config{
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
ClientCAs: clientPool,
|
|
},
|
|
}
|
|
|
|
if conf.MutualTLSCNRegex != "" {
|
|
log.Debugf(`Requiring CN matches regex "%s" for client connections`, conf.MutualTLSCNRegex)
|
|
re, err := regexp.Compile(conf.MutualTLSCNRegex)
|
|
if err != nil {
|
|
return fmt.Errorf("malformed CN regex: %s", err)
|
|
}
|
|
server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r != nil && r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
|
if re.MatchString(r.TLS.PeerCertificates[0].Subject.CommonName) {
|
|
http.DefaultServeMux.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
log.Warningf(`Rejected client cert CN "%s" does not match regex %s`,
|
|
r.TLS.PeerCertificates[0].Subject.CommonName, conf.MutualTLSCNRegex)
|
|
}
|
|
http.Error(w, "Invalid CN", http.StatusForbidden)
|
|
})
|
|
}
|
|
log.Info("Now listening with mutual TLS on https://", addr)
|
|
return server.ListenAndServeTLS(conf.TLSCertFile, conf.TLSKeyFile)
|
|
}
|
|
log.Info("Now listening on https://", addr)
|
|
return http.ListenAndServeTLS(addr, conf.TLSCertFile, conf.TLSKeyFile, nil)
|
|
|
|
}
|
|
|
|
// Command assembles the definition of Command 'serve'
|
|
var Command = &cli.Command{UsageText: serverUsageText, Flags: serverFlags, Main: serverMain}
|
|
|
|
var wrapHandler = defaultWrapHandler
|
|
|
|
// The default wrapper simply returns the normal handler and prefixes the path appropriately
|
|
func defaultWrapHandler(path string, handler http.Handler, err error) (string, http.Handler, error) {
|
|
return v1APIPath(path), handler, err
|
|
}
|
|
|
|
// SetWrapHandler sets the wrap handler which is called for all endpoints
|
|
// A custom wrap handler may be provided in order to add arbitrary server-side pre or post processing
|
|
// of server-side HTTP handling of requests.
|
|
func SetWrapHandler(wh func(path string, handler http.Handler, err error) (string, http.Handler, error)) {
|
|
wrapHandler = wh
|
|
}
|
|
|
|
// SetEndpoint can be used to add additional routes/endpoints to the HTTP server, or to override an existing route/endpoint
|
|
func SetEndpoint(path string, getHandler func() (http.Handler, error)) {
|
|
endpoints[path] = getHandler
|
|
}
|