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
105 changes: 102 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,96 @@ func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error {
return nil
}

type csrRequest struct {
// Custom name assigned to the CSR key. If empty, UUID is generated and assigned.
ID string `json:"id,omitempty"`

// Customization knobs of the generated/loaded key, if desired.
// If empty, sane defaults will be managed internally without exposing their details
// to the user. At the moment, the default parameters are:
// {
// "type": "EC",
// "curve": "P-256"
// }
Key *struct {
// The key type to be used for signing the CSR. The possible types are:
// EC, RSA, and OKP.
Type string `json:"type"`

// The curve to use with key types EC and OKP.
// If the Type is OKP, then acceptable curves are: Ed25519, or X25519
// If the Type is EC, then acceptable curves are: P-256, P-384, or P-521
Curve string `json:"curve,omitempty"`

// Only used with RSA keys and accepts minimum of 2048.
Size int `json:"size,omitempty"`
} `json:"key,omitempty"`
// SANs is a list of subject alternative names for the certificate.
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
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),
}
}
csrReq.ID = strings.TrimSpace(csrReq.ID)
if len(csrReq.ID) == 0 {
csrReq.ID = uuid.New().String()
}

// 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),
}
}
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")
w.Header().Set("content-disposition", fmt.Sprintf(`attachment; filename="%s"`, 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
47 changes: 47 additions & 0 deletions modules/caddypki/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -394,6 +396,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 +427,47 @@ 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 nil, fmt.Errorf("loading csr key '%s': %v", csrReq.ID, err)
}
if csrReq.Key == nil {
signer, err = keyutil.GenerateDefaultSigner()
if err != nil {
return nil, err
}
} else {
signer, err = keyutil.GenerateSigner(csrReq.Key.Type, csrReq.Key.Curve, csrReq.Key.Size)
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(csrReq.ID), 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 csr key: %v", err)
}
}

csr, err = x509util.CreateCertificateRequest("", csrReq.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 {
Expand Down