Files
micromdm/mdm/server.go

184 lines
4.9 KiB
Go

package mdm
import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
"github.com/micromdm/micromdm/pkg/crypto"
"github.com/micromdm/plist"
"github.com/pkg/errors"
"github.com/smallstep/pkcs7"
)
type Endpoints struct {
CheckinEndpoint endpoint.Endpoint
AcknowledgeEndpoint endpoint.Endpoint
}
func MakeServerEndpoints(s Service) Endpoints {
return Endpoints{
CheckinEndpoint: MakeCheckinEndpoint(s),
AcknowledgeEndpoint: MakeAcknowledgeEndpoint(s),
}
}
func RegisterHTTPHandlers(r *mux.Router, e Endpoints, v *crypto.PKCS7Verifier, logger log.Logger) {
options := []httptransport.ServerOption{
httptransport.ServerErrorEncoder(encodeError),
httptransport.ServerErrorLogger(logger),
httptransport.ServerBefore(httptransport.PopulateRequestContext),
httptransport.ServerBefore((verifier{PKCS7Verifier: v}).populateDeviceCertificateFromSignRequestHeader),
}
r.Methods(http.MethodPut).Path("/mdm/checkin").Handler(httptransport.NewServer(
e.CheckinEndpoint,
decodeCheckinRequest,
encodeResponse,
options...,
))
r.Methods(http.MethodPut).Path("/mdm/connect").Handler(httptransport.NewServer(
e.AcknowledgeEndpoint,
decodeAcknowledgeRequest,
encodeResponse,
options...,
))
}
type contextKey int
const (
ContextKeyDeviceCertificate contextKey = iota
ContextKeyDeviceCertificateVerifyError
)
func DeviceCertificateFromContext(ctx context.Context) (*x509.Certificate, error) {
cert := ctx.Value(ContextKeyDeviceCertificate).(*x509.Certificate)
err, _ := ctx.Value(ContextKeyDeviceCertificateVerifyError).(error)
return cert, err
}
type verifier struct {
*crypto.PKCS7Verifier
}
func (v verifier) populateDeviceCertificateFromSignRequestHeader(ctx context.Context, r *http.Request) context.Context {
bodyReader := r.Body
defer bodyReader.Close()
// We can't gracefully bubble up errors from this function,
// so we silently disregard them (terrible)
body, _ := ioutil.ReadAll(r.Body)
// Replace our body object with a fully buffered response
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
cert, err := v.verifySignature(r.Header.Get("Mdm-Signature"), body)
ctx = context.WithValue(ctx, ContextKeyDeviceCertificate, cert)
ctx = context.WithValue(ctx, ContextKeyDeviceCertificateVerifyError, err)
return ctx
}
// TODO: If we ever use Go client cert auth we can use
// r.TLS.PeerCertificates to return the client cert. Unnecessary
// now as default config is uses Mdm-Signature header method instead
// (for better compatilibity with proxies, etc.)
// func populateDeviceCertificateFromTLSPeerCertificates()
// Extract (raw) body bytes, parse property list
func mdmRequestBody(r *http.Request, s interface{}) ([]byte, error) {
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrap(err, "reading MDM acknowledge HTTP body")
}
err = plist.Unmarshal(body, s)
if err != nil {
return body, errors.Wrap(err, "unmarshal MDM acknowledge plist")
}
return body, nil
}
// Verify MDM header signature. Note: does NOT verify device certificate
func (v verifier) verifySignature(header string, body []byte) (*x509.Certificate, error) {
if header == "" {
return nil, errors.New("signature missing")
}
sig, err := base64.StdEncoding.DecodeString(header)
if err != nil {
return nil, errors.Wrap(err, "decode MDM SignMessage header")
}
p7, err := pkcs7.Parse(sig)
if err != nil {
return nil, errors.Wrap(err, "CMS parse decoded MDM SignMessage signature")
}
p7.Content = body
if err := v.Verify(p7); err != nil {
return nil, errors.Wrap(err, "CMS verify MDM Signed Message")
}
cert := p7.GetOnlySigner()
if cert == nil {
return nil, errors.New("invalid or missing CMS signer")
}
return cert, nil
}
// According to the MDM Check-in protocol, the server must respond with 200 OK
// to successful Check-in requests.
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
type failer interface {
Failed() error
}
if e, ok := response.(failer); ok && e.Failed() != nil {
return e.Failed()
}
w.WriteHeader(http.StatusOK)
type payloader interface {
Response() []byte
}
var err error
if r, ok := response.(payloader); ok {
_, err = w.Write(r.Response())
}
return errors.Wrap(err, "write acknowledge response")
}
func encodeError(ctx context.Context, err error, w http.ResponseWriter) {
err = errors.Cause(err)
type rejectUserAuthError interface {
error
UserAuthReject() bool
}
if e, ok := err.(rejectUserAuthError); ok && e.UserAuthReject() {
w.WriteHeader(http.StatusGone)
return
}
type checkoutErr interface {
error
Checkout() bool
}
if e, ok := err.(checkoutErr); ok && e.Checkout() {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusInternalServerError)
}