Improve SCEP API logic and error handling
This commit is contained in:
parent
30d3a26c20
commit
a191319da9
4 changed files with 145 additions and 103 deletions
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/smallstep/certificates/authority/mgmt"
|
"github.com/smallstep/certificates/authority/mgmt"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
"github.com/smallstep/certificates/logging"
|
"github.com/smallstep/certificates/logging"
|
||||||
|
"github.com/smallstep/certificates/scep"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteError writes to w a JSON representation of the given error.
|
// WriteError writes to w a JSON representation of the given error.
|
||||||
|
@ -22,6 +23,8 @@ func WriteError(w http.ResponseWriter, err error) {
|
||||||
case *mgmt.Error:
|
case *mgmt.Error:
|
||||||
mgmt.WriteError(w, k)
|
mgmt.WriteError(w, k)
|
||||||
return
|
return
|
||||||
|
case *scep.Error:
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
default:
|
default:
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
|
215
scep/api/api.go
215
scep/api/api.go
|
@ -11,7 +11,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
|
||||||
"github.com/smallstep/certificates/api"
|
"github.com/smallstep/certificates/api"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/scep"
|
"github.com/smallstep/certificates/scep"
|
||||||
|
@ -27,6 +26,30 @@ const (
|
||||||
// TODO: add other (more optional) operations and handling
|
// TODO: add other (more optional) operations and handling
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxPayloadSize = 2 << 20
|
||||||
|
|
||||||
|
type nextHTTP = func(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2
|
||||||
|
// TODO: move capabilities to Authority or Provisioner, so that they can be configured?
|
||||||
|
defaultCapabilities = []string{
|
||||||
|
"Renewal",
|
||||||
|
"SHA-1",
|
||||||
|
"SHA-256",
|
||||||
|
"AES",
|
||||||
|
"DES3",
|
||||||
|
"SCEPStandard",
|
||||||
|
"POSTPKIOperation",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certChainHeader = "application/x-x509-ca-ra-cert"
|
||||||
|
leafHeader = "application/x-x509-ca-cert"
|
||||||
|
pkiOpHeader = "application/x-pki-message"
|
||||||
|
)
|
||||||
|
|
||||||
// SCEPRequest is a SCEP server request.
|
// SCEPRequest is a SCEP server request.
|
||||||
type SCEPRequest struct {
|
type SCEPRequest struct {
|
||||||
Operation string
|
Operation string
|
||||||
|
@ -35,10 +58,10 @@ type SCEPRequest struct {
|
||||||
|
|
||||||
// SCEPResponse is a SCEP server response.
|
// SCEPResponse is a SCEP server response.
|
||||||
type SCEPResponse struct {
|
type SCEPResponse struct {
|
||||||
Operation string
|
Operation string
|
||||||
CACertNum int
|
CACertNum int
|
||||||
Data []byte
|
Data []byte
|
||||||
Err error
|
Certificate *x509.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is the SCEP request handler.
|
// Handler is the SCEP request handler.
|
||||||
|
@ -64,60 +87,65 @@ func (h *Handler) Route(r api.Router) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get handles all SCEP GET requests
|
||||||
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
scepRequest, err := decodeSCEPRequest(r)
|
request, err := decodeSCEPRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
writeError(w, fmt.Errorf("not a scep get request: %w", err))
|
||||||
fmt.Println("not a scep get request")
|
return
|
||||||
w.WriteHeader(500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scepResponse := SCEPResponse{Operation: scepRequest.Operation}
|
ctx := r.Context()
|
||||||
|
var response SCEPResponse
|
||||||
|
|
||||||
switch scepRequest.Operation {
|
switch request.Operation {
|
||||||
case opnGetCACert:
|
case opnGetCACert:
|
||||||
err := h.GetCACert(w, r, scepResponse)
|
response, err = h.GetCACert(ctx)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case opnGetCACaps:
|
case opnGetCACaps:
|
||||||
err := h.GetCACaps(w, r, scepResponse)
|
response, err = h.GetCACaps(ctx)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
case opnPKIOperation:
|
case opnPKIOperation:
|
||||||
|
// TODO: implement the GET for PKI operation
|
||||||
default:
|
default:
|
||||||
|
err = fmt.Errorf("unknown operation: %s", request.Operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, fmt.Errorf("get request failed: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSCEPResponse(w, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post handles all SCEP POST requests
|
||||||
func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
scepRequest, err := decodeSCEPRequest(r)
|
request, err := decodeSCEPRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
writeError(w, fmt.Errorf("not a scep post request: %w", err))
|
||||||
fmt.Println("not a scep post request")
|
return
|
||||||
w.WriteHeader(500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scepResponse := SCEPResponse{Operation: scepRequest.Operation}
|
ctx := r.Context()
|
||||||
|
var response SCEPResponse
|
||||||
|
|
||||||
switch scepRequest.Operation {
|
switch request.Operation {
|
||||||
case opnPKIOperation:
|
case opnPKIOperation:
|
||||||
err := h.PKIOperation(w, r, scepRequest, scepResponse)
|
response, err = h.PKIOperation(ctx, request)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
|
err = fmt.Errorf("unknown operation: %s", request.Operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
if err != nil {
|
||||||
|
writeError(w, fmt.Errorf("post request failed: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const maxPayloadSize = 2 << 20
|
api.LogCertificate(w, response.Certificate)
|
||||||
|
|
||||||
|
writeSCEPResponse(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
|
func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
|
||||||
|
|
||||||
|
@ -169,8 +197,6 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type nextHTTP = func(http.ResponseWriter, *http.Request)
|
|
||||||
|
|
||||||
// lookupProvisioner loads the provisioner associated with the request.
|
// lookupProvisioner loads the provisioner associated with the request.
|
||||||
// Responds 404 if the provisioner does not exist.
|
// Responds 404 if the provisioner does not exist.
|
||||||
func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
||||||
|
@ -189,67 +215,69 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
||||||
|
|
||||||
p, err := h.Auth.LoadProvisionerByID("scep/" + provisionerID)
|
p, err := h.Auth.LoadProvisionerByID("scep/" + provisionerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.WriteError(w, err)
|
writeError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scepProvisioner, ok := p.(*provisioner.SCEP)
|
provisioner, ok := p.(*provisioner.SCEP)
|
||||||
if !ok {
|
if !ok {
|
||||||
api.WriteError(w, errors.New("provisioner must be of type SCEP"))
|
writeError(w, errors.New("provisioner must be of type SCEP"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
ctx = context.WithValue(ctx, acme.ProvisionerContextKey, scep.Provisioner(scepProvisioner))
|
ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(provisioner))
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) GetCACert(w http.ResponseWriter, r *http.Request, scepResponse SCEPResponse) error {
|
// GetCACert returns the CA certificates in a SCEP response
|
||||||
|
func (h *Handler) GetCACert(ctx context.Context) (SCEPResponse, error) {
|
||||||
|
|
||||||
certs, err := h.Auth.GetCACertificates()
|
certs, err := h.Auth.GetCACertificates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return SCEPResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(certs) == 0 {
|
if len(certs) == 0 {
|
||||||
scepResponse.CACertNum = 0
|
return SCEPResponse{}, errors.New("missing CA cert")
|
||||||
scepResponse.Err = errors.New("missing CA Cert")
|
|
||||||
} else if len(certs) == 1 {
|
|
||||||
scepResponse.Data = certs[0].Raw
|
|
||||||
scepResponse.CACertNum = 1
|
|
||||||
} else {
|
|
||||||
data, err := microscep.DegenerateCertificates(certs)
|
|
||||||
scepResponse.CACertNum = len(certs)
|
|
||||||
scepResponse.Data = data
|
|
||||||
scepResponse.Err = err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return writeSCEPResponse(w, scepResponse)
|
response := SCEPResponse{Operation: opnGetCACert}
|
||||||
|
response.CACertNum = len(certs)
|
||||||
|
|
||||||
|
if len(certs) == 1 {
|
||||||
|
response.Data = certs[0].Raw
|
||||||
|
} else {
|
||||||
|
data, err := microscep.DegenerateCertificates(certs)
|
||||||
|
if err != nil {
|
||||||
|
return SCEPResponse{}, err
|
||||||
|
}
|
||||||
|
response.Data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) GetCACaps(w http.ResponseWriter, r *http.Request, scepResponse SCEPResponse) error {
|
// GetCACaps returns the CA capabilities in a SCEP response
|
||||||
|
func (h *Handler) GetCACaps(ctx context.Context) (SCEPResponse, error) {
|
||||||
|
|
||||||
//ctx := r.Context()
|
response := SCEPResponse{Operation: opnGetCACaps}
|
||||||
|
|
||||||
// _, err := ProvisionerFromContext(ctx)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: get the actual capabilities from provisioner config
|
// TODO: get the actual capabilities from provisioner config
|
||||||
scepResponse.Data = formatCapabilities(defaultCapabilities)
|
response.Data = formatCapabilities(defaultCapabilities)
|
||||||
|
|
||||||
return writeSCEPResponse(w, scepResponse)
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) PKIOperation(w http.ResponseWriter, r *http.Request, scepRequest SCEPRequest, scepResponse SCEPResponse) error {
|
// PKIOperation performs PKI operations and returns a SCEP response
|
||||||
|
func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPResponse, error) {
|
||||||
|
|
||||||
ctx := r.Context()
|
response := SCEPResponse{Operation: opnPKIOperation}
|
||||||
|
|
||||||
microMsg, err := microscep.ParsePKIMessage(scepRequest.Message)
|
microMsg, err := microscep.ParsePKIMessage(request.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return SCEPResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := &scep.PKIMessage{
|
msg := &scep.PKIMessage{
|
||||||
|
@ -260,7 +288,7 @@ func (h *Handler) PKIOperation(w http.ResponseWriter, r *http.Request, scepReque
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.Auth.DecryptPKIEnvelope(ctx, msg); err != nil {
|
if err := h.Auth.DecryptPKIEnvelope(ctx, msg); err != nil {
|
||||||
return err
|
return SCEPResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.MessageType == microscep.PKCSReq {
|
if msg.MessageType == microscep.PKCSReq {
|
||||||
|
@ -271,7 +299,7 @@ func (h *Handler) PKIOperation(w http.ResponseWriter, r *http.Request, scepReque
|
||||||
|
|
||||||
certRep, err := h.Auth.SignCSR(ctx, csr, msg)
|
certRep, err := h.Auth.SignCSR(ctx, csr, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return SCEPResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// //cert := certRep.CertRepMessage.Certificate
|
// //cert := certRep.CertRepMessage.Certificate
|
||||||
|
@ -280,11 +308,10 @@ func (h *Handler) PKIOperation(w http.ResponseWriter, r *http.Request, scepReque
|
||||||
// // TODO: check if CN already exists, if renewal is allowed and if existing should be revoked; fail if not
|
// // TODO: check if CN already exists, if renewal is allowed and if existing should be revoked; fail if not
|
||||||
// // TODO: store the new cert for CN locally; should go into the DB
|
// // TODO: store the new cert for CN locally; should go into the DB
|
||||||
|
|
||||||
scepResponse.Data = certRep.Raw
|
response.Data = certRep.Raw
|
||||||
|
response.Certificate = certRep.Certificate
|
||||||
|
|
||||||
api.LogCertificate(w, certRep.Certificate)
|
return response, nil
|
||||||
|
|
||||||
return writeSCEPResponse(w, scepResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func certName(cert *x509.Certificate) string {
|
func certName(cert *x509.Certificate) string {
|
||||||
|
@ -299,40 +326,26 @@ func formatCapabilities(caps []string) []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeSCEPResponse writes a SCEP response back to the SCEP client.
|
// writeSCEPResponse writes a SCEP response back to the SCEP client.
|
||||||
func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) error {
|
func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) {
|
||||||
if response.Err != nil {
|
w.Header().Set("Content-Type", contentHeader(response))
|
||||||
http.Error(w, response.Err.Error(), http.StatusInternalServerError)
|
_, err := w.Write(response.Data)
|
||||||
return nil
|
if err != nil {
|
||||||
|
writeError(w, fmt.Errorf("error when writing scep response: %w", err)) // This could end up as an error again
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", contentHeader(response.Operation, response.CACertNum))
|
|
||||||
w.Write(response.Data)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func writeError(w http.ResponseWriter, err error) {
|
||||||
// TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2
|
scepError := &scep.Error{
|
||||||
// TODO: move capabilities to Authority or Provisioner, so that they can be configured?
|
Err: fmt.Errorf("post request failed: %w", err),
|
||||||
defaultCapabilities = []string{
|
Status: http.StatusInternalServerError, // TODO: make this a param?
|
||||||
"Renewal",
|
|
||||||
"SHA-1",
|
|
||||||
"SHA-256",
|
|
||||||
"AES",
|
|
||||||
"DES3",
|
|
||||||
"SCEPStandard",
|
|
||||||
"POSTPKIOperation",
|
|
||||||
}
|
}
|
||||||
)
|
api.WriteError(w, scepError)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
func contentHeader(r SCEPResponse) string {
|
||||||
certChainHeader = "application/x-x509-ca-ra-cert"
|
switch r.Operation {
|
||||||
leafHeader = "application/x-x509-ca-cert"
|
|
||||||
pkiOpHeader = "application/x-pki-message"
|
|
||||||
)
|
|
||||||
|
|
||||||
func contentHeader(operation string, certNum int) string {
|
|
||||||
switch operation {
|
|
||||||
case opnGetCACert:
|
case opnGetCACert:
|
||||||
if certNum > 1 {
|
if r.CACertNum > 1 {
|
||||||
return certChainHeader
|
return certChainHeader
|
||||||
}
|
}
|
||||||
return leafHeader
|
return leafHeader
|
||||||
|
|
|
@ -3,14 +3,21 @@ package scep
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
// ContextKey is the key type for storing and searching for SCEP request
|
||||||
|
// essentials in the context of a request.
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ProvisionerContextKey provisioner key
|
||||||
|
ProvisionerContextKey = ContextKey("provisioner")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProvisionerFromContext searches the context for a SCEP provisioner.
|
// ProvisionerFromContext searches the context for a SCEP provisioner.
|
||||||
// Returns the provisioner or an error.
|
// Returns the provisioner or an error.
|
||||||
func ProvisionerFromContext(ctx context.Context) (Provisioner, error) {
|
func ProvisionerFromContext(ctx context.Context) (Provisioner, error) {
|
||||||
val := ctx.Value(acme.ProvisionerContextKey)
|
val := ctx.Value(ProvisionerContextKey)
|
||||||
if val == nil {
|
if val == nil {
|
||||||
return nil, errors.New("provisioner expected in request context")
|
return nil, errors.New("provisioner expected in request context")
|
||||||
}
|
}
|
||||||
|
|
19
scep/errors.go
Normal file
19
scep/errors.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package scep
|
||||||
|
|
||||||
|
// Error is an SCEP error type
|
||||||
|
type Error struct {
|
||||||
|
// Type ProbType
|
||||||
|
// Detail string
|
||||||
|
Err error
|
||||||
|
Status int
|
||||||
|
// Sub []*Error
|
||||||
|
// Identifier *Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
// if e.Err == nil {
|
||||||
|
// return e.Detail
|
||||||
|
// }
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
Loading…
Reference in a new issue