Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pki: generating CSR through API #5783

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
76 changes: 73 additions & 3 deletions modules/caddypki/adminapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"

"github.com/google/uuid"
"go.uber.org/zap"

"github.com/caddyserver/caddy/v2"
Expand All @@ -29,6 +31,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 {
Expand Down Expand Up @@ -71,12 +79,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,
Expand Down Expand Up @@ -168,6 +177,67 @@ func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error {
return nil
}

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),
}
}
csrReq.ID = strings.TrimSpace(csrReq.ID)
if len(csrReq.ID) == 0 {
csrReq.ID = uuid.New().String()
}
if err := csrReq.validate(); err != nil {
return caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("invalid CSR request: %v", err),
}
}
// Generate the CSR
csr, err := ca.generateCSR(csrReq)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("failed to generate CSR: %v", err),
}
}
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")
w.Header().Set("content-disposition", fmt.Sprintf(`attachment; filename="%s.csr"`, csrReq.ID))

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/<ca>)
id := strings.Split(r.URL.Path, "/")[3]
Expand Down
70 changes: 70 additions & 0 deletions modules/caddypki/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package caddypki

import (
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"errors"
"fmt"
Expand All @@ -29,6 +31,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"
Expand Down Expand Up @@ -394,6 +398,10 @@ func (ca CA) storageKeyIntermediateKey() string {
return path.Join(ca.storageKeyCAPrefix(), "intermediate.key")
}

func (ca CA) storageKeyCSRKey(id string) string {
return path.Join(ca.storageKeyCAPrefix(), id+".csr.key")
}

func (ca CA) newReplacer() *caddy.Replacer {
repl := caddy.NewReplacer()
repl.Set("pki.ca.name", ca.Name)
Expand Down Expand Up @@ -421,6 +429,68 @@ func (ca CA) installRoot() error {
)
}

func (ca CA) generateCSR(csrReq csrRequest) (csr *x509.CertificateRequest, err error) {
var signer crypto.Signer
csrKeyPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyCSRKey(csrReq.ID))
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return csr, fmt.Errorf("loading csr key '%s': %v", csrReq.ID, err)
}
if csrReq.Key == nil {
signer, err = keyutil.GenerateDefaultSigner()
if err != nil {
return csr, err
}
} else {
signer, err = keyutil.GenerateSigner(csrReq.Key.Type.String(), csrReq.Key.Curve.String(), csrReq.Key.Size)
if err != nil {
return csr, err
}
}

csrKeyPEM, err = certmagic.PEMEncodePrivateKey(signer)
if err != nil {
return csr, fmt.Errorf("encoding csr key: %v", err)
}
if err := ca.storage.Store(ca.ctx, ca.storageKeyCSRKey(csrReq.ID), csrKeyPEM); err != nil {
return csr, fmt.Errorf("saving csr key: %v", err)
}
}
if signer == nil {
signer, err = certmagic.PEMDecodePrivateKey(csrKeyPEM)
if err != nil {
return csr, fmt.Errorf("decoding csr key: %v", err)
}
}

var subject pkix.Name
if csrReq.Request != nil && csrReq.Request.Subject != nil {
subject = pkix.Name{
Country: csrReq.Request.Subject.Country,
Organization: csrReq.Request.Subject.Organization,
OrganizationalUnit: csrReq.Request.Subject.OrganizationalUnit,
Locality: csrReq.Request.Subject.Locality,
Province: csrReq.Request.Subject.Province,
StreetAddress: csrReq.Request.Subject.StreetAddress,
PostalCode: csrReq.Request.Subject.PostalCode,
CommonName: csrReq.Request.Subject.CommonName,
}
}
dnsNames, ips, emails, uris := x509util.SplitSANs(csrReq.Request.SANs)

csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Subject: subject,
DNSNames: dnsNames,
IPAddresses: ips,
EmailAddresses: emails,
URIs: uris,
}, signer)
if err != nil {
return csr, err
}
return x509.ParseCertificateRequest(csrBytes)
}

// AuthorityConfig is used to help a CA configure
// the underlying signing authority.
type AuthorityConfig struct {
Expand Down