2021-02-12 16:02:39 +00:00
|
|
|
package api
|
2021-02-12 11:03:08 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2021-03-05 11:40:42 +00:00
|
|
|
"net/url"
|
2021-02-12 11:03:08 +00:00
|
|
|
"strings"
|
|
|
|
|
2021-03-05 11:40:42 +00:00
|
|
|
"github.com/go-chi/chi"
|
2021-02-12 11:03:08 +00:00
|
|
|
"github.com/smallstep/certificates/api"
|
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
2021-02-12 16:02:39 +00:00
|
|
|
"github.com/smallstep/certificates/scep"
|
2021-03-06 22:24:49 +00:00
|
|
|
"go.mozilla.org/pkcs7"
|
2021-02-12 11:03:08 +00:00
|
|
|
|
|
|
|
microscep "github.com/micromdm/scep/scep"
|
|
|
|
)
|
|
|
|
|
2021-02-12 16:02:39 +00:00
|
|
|
const (
|
|
|
|
opnGetCACert = "GetCACert"
|
|
|
|
opnGetCACaps = "GetCACaps"
|
|
|
|
opnPKIOperation = "PKIOperation"
|
2021-02-25 23:32:21 +00:00
|
|
|
|
|
|
|
// TODO: add other (more optional) operations and handling
|
2021-02-12 16:02:39 +00:00
|
|
|
)
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
const maxPayloadSize = 2 << 20
|
|
|
|
|
|
|
|
type nextHTTP = func(http.ResponseWriter, *http.Request)
|
|
|
|
|
|
|
|
const (
|
|
|
|
certChainHeader = "application/x-x509-ca-ra-cert"
|
|
|
|
leafHeader = "application/x-x509-ca-cert"
|
|
|
|
pkiOpHeader = "application/x-pki-message"
|
|
|
|
)
|
|
|
|
|
2021-02-12 16:02:39 +00:00
|
|
|
// SCEPRequest is a SCEP server request.
|
|
|
|
type SCEPRequest struct {
|
|
|
|
Operation string
|
|
|
|
Message []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
// SCEPResponse is a SCEP server response.
|
|
|
|
type SCEPResponse struct {
|
2021-02-26 23:34:50 +00:00
|
|
|
Operation string
|
|
|
|
CACertNum int
|
|
|
|
Data []byte
|
|
|
|
Certificate *x509.Certificate
|
2021-02-12 16:02:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Handler is the SCEP request handler.
|
2021-02-12 11:03:08 +00:00
|
|
|
type Handler struct {
|
2021-02-12 16:02:39 +00:00
|
|
|
Auth scep.Interface
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-12 16:02:39 +00:00
|
|
|
// New returns a new SCEP API router.
|
|
|
|
func New(scepAuth scep.Interface) api.RouterHandler {
|
2021-02-12 11:03:08 +00:00
|
|
|
return &Handler{scepAuth}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Route traffic and implement the Router interface.
|
|
|
|
func (h *Handler) Route(r api.Router) {
|
2021-03-05 11:40:42 +00:00
|
|
|
getLink := h.Auth.GetLinkExplicit
|
|
|
|
r.MethodFunc(http.MethodGet, getLink("{provisionerID}", false, nil), h.lookupProvisioner(h.Get))
|
|
|
|
r.MethodFunc(http.MethodPost, getLink("{provisionerID}", false, nil), h.lookupProvisioner(h.Post))
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
// Get handles all SCEP GET requests
|
2021-02-12 11:03:08 +00:00
|
|
|
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
request, err := decodeSCEPRequest(r)
|
2021-02-12 11:03:08 +00:00
|
|
|
if err != nil {
|
2021-02-26 23:34:50 +00:00
|
|
|
writeError(w, fmt.Errorf("not a scep get request: %w", err))
|
|
|
|
return
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
ctx := r.Context()
|
|
|
|
var response SCEPResponse
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
switch request.Operation {
|
2021-02-12 11:03:08 +00:00
|
|
|
case opnGetCACert:
|
2021-02-26 23:34:50 +00:00
|
|
|
response, err = h.GetCACert(ctx)
|
2021-02-12 11:03:08 +00:00
|
|
|
case opnGetCACaps:
|
2021-02-26 23:34:50 +00:00
|
|
|
response, err = h.GetCACaps(ctx)
|
2021-02-12 11:03:08 +00:00
|
|
|
case opnPKIOperation:
|
2021-03-06 21:35:41 +00:00
|
|
|
// TODO: implement the GET for PKI operation? Default CACAPS doesn't specify this is in use, though
|
2021-02-12 11:03:08 +00:00
|
|
|
default:
|
2021-02-26 23:34:50 +00:00
|
|
|
err = fmt.Errorf("unknown operation: %s", request.Operation)
|
|
|
|
}
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
if err != nil {
|
|
|
|
writeError(w, fmt.Errorf("get request failed: %w", err))
|
|
|
|
return
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
2021-02-26 23:34:50 +00:00
|
|
|
|
|
|
|
writeSCEPResponse(w, response)
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
// Post handles all SCEP POST requests
|
2021-02-12 11:03:08 +00:00
|
|
|
func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
|
2021-02-25 23:32:21 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
request, err := decodeSCEPRequest(r)
|
2021-02-12 11:03:08 +00:00
|
|
|
if err != nil {
|
2021-02-26 23:34:50 +00:00
|
|
|
writeError(w, fmt.Errorf("not a scep post request: %w", err))
|
|
|
|
return
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
ctx := r.Context()
|
|
|
|
var response SCEPResponse
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
switch request.Operation {
|
2021-02-12 11:03:08 +00:00
|
|
|
case opnPKIOperation:
|
2021-02-26 23:34:50 +00:00
|
|
|
response, err = h.PKIOperation(ctx, request)
|
2021-02-12 11:03:08 +00:00
|
|
|
default:
|
2021-02-26 23:34:50 +00:00
|
|
|
err = fmt.Errorf("unknown operation: %s", request.Operation)
|
|
|
|
}
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
if err != nil {
|
|
|
|
writeError(w, fmt.Errorf("post request failed: %w", err))
|
|
|
|
return
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-03-06 23:30:37 +00:00
|
|
|
// TODO: fix cases in which we get here and there's no certificate (i.e. wrong password, waiting for cert, etc)
|
|
|
|
// We should generate an appropriate response and it should be signed
|
2021-02-26 23:34:50 +00:00
|
|
|
api.LogCertificate(w, response.Certificate)
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
writeSCEPResponse(w, response)
|
|
|
|
}
|
2021-02-12 11:03:08 +00:00
|
|
|
|
|
|
|
func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
|
|
|
|
|
|
|
|
defer r.Body.Close()
|
|
|
|
|
|
|
|
method := r.Method
|
|
|
|
query := r.URL.Query()
|
|
|
|
|
|
|
|
var operation string
|
|
|
|
if _, ok := query["operation"]; ok {
|
|
|
|
operation = query.Get("operation")
|
|
|
|
}
|
|
|
|
|
|
|
|
switch method {
|
|
|
|
case http.MethodGet:
|
|
|
|
switch operation {
|
|
|
|
case opnGetCACert, opnGetCACaps:
|
|
|
|
return SCEPRequest{
|
|
|
|
Operation: operation,
|
|
|
|
Message: []byte{},
|
|
|
|
}, nil
|
|
|
|
case opnPKIOperation:
|
|
|
|
var message string
|
|
|
|
if _, ok := query["message"]; ok {
|
|
|
|
message = query.Get("message")
|
|
|
|
}
|
2021-03-06 21:35:41 +00:00
|
|
|
// TODO: verify this; it seems like it should be StdEncoding instead of URLEncoding
|
2021-02-12 11:03:08 +00:00
|
|
|
decodedMessage, err := base64.URLEncoding.DecodeString(message)
|
|
|
|
if err != nil {
|
|
|
|
return SCEPRequest{}, err
|
|
|
|
}
|
|
|
|
return SCEPRequest{
|
|
|
|
Operation: operation,
|
|
|
|
Message: decodedMessage,
|
|
|
|
}, nil
|
|
|
|
default:
|
|
|
|
return SCEPRequest{}, fmt.Errorf("unsupported operation: %s", operation)
|
|
|
|
}
|
|
|
|
case http.MethodPost:
|
|
|
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, maxPayloadSize))
|
|
|
|
if err != nil {
|
|
|
|
return SCEPRequest{}, err
|
|
|
|
}
|
|
|
|
return SCEPRequest{
|
|
|
|
Operation: operation,
|
|
|
|
Message: body,
|
|
|
|
}, nil
|
|
|
|
default:
|
|
|
|
return SCEPRequest{}, fmt.Errorf("unsupported method: %s", method)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// lookupProvisioner loads the provisioner associated with the request.
|
|
|
|
// Responds 404 if the provisioner does not exist.
|
|
|
|
func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
2021-03-05 11:40:42 +00:00
|
|
|
name := chi.URLParam(r, "provisionerID")
|
|
|
|
provisionerID, err := url.PathUnescape(name)
|
|
|
|
if err != nil {
|
|
|
|
api.WriteError(w, fmt.Errorf("error url unescaping provisioner id '%s'", name))
|
|
|
|
return
|
|
|
|
}
|
2021-02-19 10:01:19 +00:00
|
|
|
|
|
|
|
p, err := h.Auth.LoadProvisionerByID("scep/" + provisionerID)
|
2021-02-12 11:03:08 +00:00
|
|
|
if err != nil {
|
2021-02-26 23:34:50 +00:00
|
|
|
writeError(w, err)
|
2021-02-12 11:03:08 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
provisioner, ok := p.(*provisioner.SCEP)
|
2021-02-12 11:03:08 +00:00
|
|
|
if !ok {
|
2021-02-26 23:34:50 +00:00
|
|
|
writeError(w, errors.New("provisioner must be of type SCEP"))
|
2021-02-12 11:03:08 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-12 16:02:39 +00:00
|
|
|
ctx := r.Context()
|
2021-02-26 23:34:50 +00:00
|
|
|
ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(provisioner))
|
2021-02-12 11:03:08 +00:00
|
|
|
next(w, r.WithContext(ctx))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
// GetCACert returns the CA certificates in a SCEP response
|
|
|
|
func (h *Handler) GetCACert(ctx context.Context) (SCEPResponse, error) {
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-12 16:02:39 +00:00
|
|
|
certs, err := h.Auth.GetCACertificates()
|
2021-02-12 11:03:08 +00:00
|
|
|
if err != nil {
|
2021-02-26 23:34:50 +00:00
|
|
|
return SCEPResponse{}, err
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(certs) == 0 {
|
2021-02-26 23:34:50 +00:00
|
|
|
return SCEPResponse{}, errors.New("missing CA cert")
|
|
|
|
}
|
|
|
|
|
2021-03-06 23:30:37 +00:00
|
|
|
response := SCEPResponse{
|
|
|
|
Operation: opnGetCACert,
|
|
|
|
CACertNum: len(certs),
|
|
|
|
}
|
2021-02-26 23:34:50 +00:00
|
|
|
|
|
|
|
if len(certs) == 1 {
|
|
|
|
response.Data = certs[0].Raw
|
2021-02-12 11:03:08 +00:00
|
|
|
} else {
|
|
|
|
data, err := microscep.DegenerateCertificates(certs)
|
2021-02-26 23:34:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return SCEPResponse{}, err
|
|
|
|
}
|
|
|
|
response.Data = data
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
return response, nil
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
// GetCACaps returns the CA capabilities in a SCEP response
|
|
|
|
func (h *Handler) GetCACaps(ctx context.Context) (SCEPResponse, error) {
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-03-06 23:50:00 +00:00
|
|
|
caps := h.Auth.GetCACaps(ctx)
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-03-06 23:50:00 +00:00
|
|
|
response := SCEPResponse{
|
|
|
|
Operation: opnGetCACaps,
|
|
|
|
Data: formatCapabilities(caps),
|
|
|
|
}
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
return response, nil
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
// PKIOperation performs PKI operations and returns a SCEP response
|
|
|
|
func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPResponse, error) {
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-03-06 22:24:49 +00:00
|
|
|
// parse the message using microscep implementation
|
2021-02-26 23:34:50 +00:00
|
|
|
microMsg, err := microscep.ParsePKIMessage(request.Message)
|
2021-02-12 11:03:08 +00:00
|
|
|
if err != nil {
|
2021-02-26 23:34:50 +00:00
|
|
|
return SCEPResponse{}, err
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-03-06 22:24:49 +00:00
|
|
|
p7, err := pkcs7.Parse(microMsg.Raw)
|
|
|
|
if err != nil {
|
|
|
|
return SCEPResponse{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy over properties to our internal PKIMessage
|
2021-02-26 17:07:50 +00:00
|
|
|
msg := &scep.PKIMessage{
|
|
|
|
TransactionID: microMsg.TransactionID,
|
|
|
|
MessageType: microMsg.MessageType,
|
|
|
|
SenderNonce: microMsg.SenderNonce,
|
|
|
|
Raw: microMsg.Raw,
|
2021-03-06 22:24:49 +00:00
|
|
|
P7: p7,
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 17:07:50 +00:00
|
|
|
if err := h.Auth.DecryptPKIEnvelope(ctx, msg); err != nil {
|
2021-02-26 23:34:50 +00:00
|
|
|
return SCEPResponse{}, err
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 17:07:50 +00:00
|
|
|
if msg.MessageType == microscep.PKCSReq {
|
2021-03-06 23:30:37 +00:00
|
|
|
|
|
|
|
challengeMatches, err := h.Auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword)
|
|
|
|
if err != nil {
|
|
|
|
return SCEPResponse{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !challengeMatches {
|
|
|
|
return SCEPResponse{}, errors.New("wrong password provided")
|
|
|
|
}
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 17:07:50 +00:00
|
|
|
csr := msg.CSRReqMessage.CSR
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 17:07:50 +00:00
|
|
|
certRep, err := h.Auth.SignCSR(ctx, csr, msg)
|
2021-02-12 11:03:08 +00:00
|
|
|
if err != nil {
|
2021-02-26 23:34:50 +00:00
|
|
|
return SCEPResponse{}, err
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-25 23:32:21 +00:00
|
|
|
// //cert := certRep.CertRepMessage.Certificate
|
|
|
|
// //name := certName(cert)
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-25 23:32:21 +00:00
|
|
|
// // 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
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-03-06 23:30:37 +00:00
|
|
|
response := SCEPResponse{
|
|
|
|
Operation: opnPKIOperation,
|
|
|
|
Data: certRep.Raw,
|
|
|
|
Certificate: certRep.Certificate,
|
|
|
|
}
|
2021-02-12 16:02:39 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
return response, nil
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func certName(cert *x509.Certificate) string {
|
|
|
|
if cert.Subject.CommonName != "" {
|
|
|
|
return cert.Subject.CommonName
|
|
|
|
}
|
|
|
|
return string(cert.Signature)
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatCapabilities(caps []string) []byte {
|
2021-02-19 11:06:24 +00:00
|
|
|
return []byte(strings.Join(caps, "\r\n"))
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// writeSCEPResponse writes a SCEP response back to the SCEP client.
|
2021-02-26 23:34:50 +00:00
|
|
|
func writeSCEPResponse(w http.ResponseWriter, response SCEPResponse) {
|
|
|
|
w.Header().Set("Content-Type", contentHeader(response))
|
|
|
|
_, err := w.Write(response.Data)
|
|
|
|
if err != nil {
|
|
|
|
writeError(w, fmt.Errorf("error when writing scep response: %w", err)) // This could end up as an error again
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
func writeError(w http.ResponseWriter, err error) {
|
2021-03-06 23:30:37 +00:00
|
|
|
// TODO: this probably needs to use SCEP specific errors (i.e. failInfo)
|
2021-02-26 23:34:50 +00:00
|
|
|
scepError := &scep.Error{
|
2021-03-05 11:40:42 +00:00
|
|
|
Message: err.Error(),
|
|
|
|
Status: http.StatusInternalServerError, // TODO: make this a param?
|
2021-02-12 11:03:08 +00:00
|
|
|
}
|
2021-02-26 23:34:50 +00:00
|
|
|
api.WriteError(w, scepError)
|
|
|
|
}
|
2021-02-12 11:03:08 +00:00
|
|
|
|
2021-02-26 23:34:50 +00:00
|
|
|
func contentHeader(r SCEPResponse) string {
|
|
|
|
switch r.Operation {
|
2021-02-12 11:03:08 +00:00
|
|
|
case opnGetCACert:
|
2021-02-26 23:34:50 +00:00
|
|
|
if r.CACertNum > 1 {
|
2021-02-12 11:03:08 +00:00
|
|
|
return certChainHeader
|
|
|
|
}
|
|
|
|
return leafHeader
|
|
|
|
case opnPKIOperation:
|
|
|
|
return pkiOpHeader
|
|
|
|
default:
|
|
|
|
return "text/plain"
|
|
|
|
}
|
|
|
|
}
|