From 5b09e7df3d09c10f0e22f87f71ebb4f997e5e199 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Fri, 25 Aug 2023 22:00:28 +0300 Subject: [PATCH] pki: rough draft for generating CSR through API --- modules/caddypki/adminapi.go | 74 ++++++++++++++++++++++++++++++++++-- modules/caddypki/ca.go | 40 +++++++++++++++++++ 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/modules/caddypki/adminapi.go b/modules/caddypki/adminapi.go index 435af349a..720a5dee9 100644 --- a/modules/caddypki/adminapi.go +++ b/modules/caddypki/adminapi.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "net/http" + "regexp" "strings" "go.uber.org/zap" @@ -29,6 +30,12 @@ func init() { caddy.RegisterModule(adminAPI{}) } +var ( + caInfoPathPattern = regexp.MustCompile(`^ca/[^/]+$`) + getCertPathPattern = regexp.MustCompile(`^ca/[^/]+/certificates$`) + produceCSRPathPattern = regexp.MustCompile(`^ca/[^/]+/csr$`) +) + // adminAPI is a module that serves PKI endpoints to retrieve // information about the CAs being managed by Caddy. type adminAPI struct { @@ -71,12 +78,13 @@ func (a *adminAPI) Routes() []caddy.AdminRoute { // handleAPIEndpoints routes API requests within adminPKIEndpointBase. func (a *adminAPI) handleAPIEndpoints(w http.ResponseWriter, r *http.Request) error { uri := strings.TrimPrefix(r.URL.Path, "/pki/") - parts := strings.Split(uri, "/") switch { - case len(parts) == 2 && parts[0] == "ca" && parts[1] != "": + case caInfoPathPattern.MatchString(uri): return a.handleCAInfo(w, r) - case len(parts) == 3 && parts[0] == "ca" && parts[1] != "" && parts[2] == "certificates": + case getCertPathPattern.MatchString(uri): return a.handleCACerts(w, r) + case produceCSRPathPattern.MatchString(uri): + return a.handleCSRGeneration(w, r) } return caddy.APIError{ HTTPStatus: http.StatusNotFound, @@ -168,6 +176,66 @@ func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error { return nil } +type csrRequest struct { + // SANs is a list of subject alternative names for the certificate. + SANs []string `json:"sans"` +} + +func (a *adminAPI) handleCSRGeneration(w http.ResponseWriter, r *http.Request) error { + if r.Method != http.MethodPost { + return caddy.APIError{ + HTTPStatus: http.StatusMethodNotAllowed, + Err: fmt.Errorf("method not allowed: %v", r.Method), + } + } + + ca, err := a.getCAFromAPIRequestPath(r) + if err != nil { + return caddy.APIError{ + HTTPStatus: http.StatusBadRequest, + Err: err, + } + } + + // Decode the CSR request from the request body + var csrReq csrRequest + if err := json.NewDecoder(r.Body).Decode(&csrReq); err != nil { + return caddy.APIError{ + HTTPStatus: http.StatusBadRequest, + Err: fmt.Errorf("failed to decode CSR request: %v", err), + } + } + // Generate the CSR + csr, err := ca.generateCSR(csrReq.SANs) + if err != nil { + return caddy.APIError{ + HTTPStatus: http.StatusInternalServerError, + Err: fmt.Errorf("failed to generate CSR: %v", err), + } + } + if r.Header.Get("Accept") != "application/pkcs10" { + return caddy.APIError{ + HTTPStatus: http.StatusNotAcceptable, + Err: fmt.Errorf("only accept application/pkcs10"), + } + } + bs, err := pemEncode("CERTIFICATE REQUEST", csr.Raw) + if err != nil { + return caddy.APIError{ + HTTPStatus: http.StatusInternalServerError, + Err: fmt.Errorf("failed to encode CSR to PEM: %v", err), + } + } + w.Header().Set("Content-Type", "application/pkcs10") + if _, err := w.Write(bs); err != nil { + return caddy.APIError{ + HTTPStatus: http.StatusInternalServerError, + Err: fmt.Errorf("failed to write CSR response: %v", err), + } + } + return nil +} + func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) { // Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/) id := strings.Split(r.URL.Path, "/")[3] diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index 6c48da6f9..15339cedb 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -29,6 +29,8 @@ import ( "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/db" "github.com/smallstep/truststore" + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/x509util" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" @@ -394,6 +396,10 @@ func (ca CA) storageKeyIntermediateKey() string { return path.Join(ca.storageKeyCAPrefix(), "intermediate.key") } +func (ca CA) storageKeyCSRKey() string { + return path.Join(ca.storageKeyCAPrefix(), "csr.key") +} + func (ca CA) newReplacer() *caddy.Replacer { repl := caddy.NewReplacer() repl.Set("pki.ca.name", ca.Name) @@ -421,6 +427,40 @@ func (ca CA) installRoot() error { ) } +func (ca CA) generateCSR(sans []string) (csr *x509.CertificateRequest, err error) { + var signer crypto.Signer + csrKeyPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyCSRKey()) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("loading csr key: %v", err) + } + + signer, err = keyutil.GenerateDefaultSigner() + if err != nil { + return nil, err + } + csrKeyPEM, err = certmagic.PEMEncodePrivateKey(signer) + if err != nil { + return nil, fmt.Errorf("encoding csr key: %v", err) + } + if err := ca.storage.Store(ca.ctx, ca.storageKeyCSRKey(), csrKeyPEM); err != nil { + return nil, fmt.Errorf("saving csr key: %v", err) + } + } + if signer == nil { + signer, err = certmagic.PEMDecodePrivateKey(csrKeyPEM) + if err != nil { + return nil, fmt.Errorf("decoding root key: %v", err) + } + } + + csr, err = x509util.CreateCertificateRequest("", sans, signer) + if err != nil { + return nil, err + } + return csr, nil +} + // AuthorityConfig is used to help a CA configure // the underlying signing authority. type AuthorityConfig struct {