Implement OTA (#137)

* Refactor Enroll HTTP Handler factory
* Begin `crypto` package
* Implement OTA enroll profile endpoint
* Implement Handlers, Endpoints, and encdecs for OTA Phase 2 & 3
* Implement certificate verification against Apple Device CA
* Implement Phase 2 & 3 logic based on provided CMS cert
* Provide (existing) enrollment profile on Phase 3
This commit is contained in:
Jesse Peterson
2017-04-14 11:52:14 -07:00
committed by GitHub
parent b19dde19d7
commit c95956535b
7 changed files with 389 additions and 73 deletions

109
crypto/apple.go Normal file
View File

@@ -0,0 +1,109 @@
package crypto
import (
"crypto/x509"
"encoding/pem"
"fmt"
)
/*
These certificates are not currently used but they represent the chain
of certificates to verify a device's certificate in DEP & OTA requests.
const appleRootCAPEM = `-----BEGIN CERTIFICATE-----
MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0
MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw
bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+
+FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1
XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w
tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW
q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM
aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3
R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE
ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93
d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl
IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0
YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj
b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp
Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc
NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP
y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7
R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg
xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP
IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX
UKqK1drk/NAJBzewdXUh
-----END CERTIFICATE-----
`
const appleiPhoneCertificateAuthorityPEM = `-----BEGIN CERTIFICATE-----
MIID8zCCAtugAwIBAgIBFzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDcwNDEyMTc0
MzI4WhcNMjIwNDEyMTc0MzI4WjB5MQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw
bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
LTArBgNVBAMTJEFwcGxlIGlQaG9uZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMevvBHwLSeEFtGpLghuE+G
IXAoRWBcHMPICmRjiPv8ae74VPzpW7cGTgQvw2szr0RM6kuACbSH9lu0/WTds3Lg
E7P9F9m856jtwoxhwir57M6lXtZp62QLjQiPuKBQRgncGeTlsJRtu/eZmMTom0FO
1PFl4xtSetzoA9luHdoQVYakKVhJDOpH1xU0M/bAoERKcL4stSowN4wuFevR5GyX
OFVWsTUrWOpEoyaF7shmSuTPifA9Y60p3q26WrPcpaOapwlOgBY1ZaSFDWN7PmOK
2n1KRuyjORg0ucYoZRi8E2Ccf1esFMmJ7aG2h2hStoROuMiD7PmeGauzwQuGx58C
AwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQU5zQqLiLeOWBrtJTOd4NhLzGgfDUwHwYDVR0jBBgwFoAUK9BpR5R2Cf70
a40uQKb3R01/CF4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL3d3dy5hcHBsZS5j
b20vYXBwbGVjYS9yb290LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAHdHVe910TtcX
/IItDJmbXkJy8mnc1WteDQxrSz57FCXes5TooPoPgInyFz0AAqKRkb50V9yvmp+h
Cn0wvgAqzCFZ6/1JrG51GeiaegPRhvbn9rAOS0n6o7dButfR41ahfYOrl674UUom
wYVCEyaNA1RmEF5ghAUSMStrVMCgyEG8VB7nVK0TANJKx7vBiq+BCI7wRgq/J6a+
3M85OoBwGSMyo2tmXZ5NqEdJsntFtVEzp3RnCU62bG9I9yy5MwVEa0W+dEtvsoaR
tD4lKCWes8JRhvxP5a87qrtELAFJ4nSzNPpE7xTCEfItGRpRidMISkFsWFbemzrh
BVflYs/SDw==
-----END CERTIFICATE-----
`
*/
// TODO: This certificate expired 2014, but is required.
const appleiPhoneDeviceCAPEM = `-----BEGIN CERTIFICATE-----
MIIDaTCCAlGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB5MQswCQYDVQQGEwJVUzET
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkxLTArBgNVBAMTJEFwcGxlIGlQaG9uZSBDZXJ0aWZpY2F0aW9u
IEF1dGhvcml0eTAeFw0wNzA0MTYyMjU0NDZaFw0xNDA0MTYyMjU0NDZaMFoxCzAJ
BgNVBAYTAlVTMRMwEQYDVQQKEwpBcHBsZSBJbmMuMRUwEwYDVQQLEwxBcHBsZSBp
UGhvbmUxHzAdBgNVBAMTFkFwcGxlIGlQaG9uZSBEZXZpY2UgQ0EwgZ8wDQYJKoZI
hvcNAQEBBQADgY0AMIGJAoGBAPGUSsnquloYYK3Lok1NTlQZaRdZB2bLl+hmmkdf
Rq5nerVKc1SxywT2vTa4DFU4ioSDMVJl+TPhl3ecK0wmsCU/6TKqewh0lOzBSzgd
Z04IUpRai1mjXNeT9KD+VYW7TEaXXm6yd0UvZ1y8Cxi/WblshvcqdXbSGXH0KWO5
JQuvAgMBAAGjgZ4wgZswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w
HQYDVR0OBBYEFLL+ISNEhpVqedWBJo5zENinTI50MB8GA1UdIwQYMBaAFOc0Ki4i
3jlga7SUzneDYS8xoHw1MDgGA1UdHwQxMC8wLaAroCmGJ2h0dHA6Ly93d3cuYXBw
bGUuY29tL2FwcGxlY2EvaXBob25lLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAd13P
Z3pMViukVHe9WUg8Hum+0I/0kHKvjhwVd/IMwGlXyU7DhUYWdja2X/zqj7W24Aq5
7dEKm3fqqxK5XCFVGY5HI0cRsdENyTP7lxSiiTRYj2mlPedheCn+k6T5y0U4Xr40
FXwWb2nWqCF1AgIudhgvVbxlvqcxUm8Zz7yDeJ0JFovXQhyO5fLUHRLCQFssAbf8
B4i8rYYsBUhYTspVJcxVpIIltkYpdIRSIARA49HNvKK4hzjzMS/OhKQpVKw+OCEZ
xptCVeN2pjbdt9uzi175oVo/u6B2ArKAW17u6XEHIdDMOe7cb33peVI6TD15W4MI
pyQPbp8orlXe+tA8JA==
-----END CERTIFICATE-----
`
// VerifyFromAppleDeviceCA verifies a certificate was signed by Apple's iPhone Device CA.
// TODO: We want to have more intensive verification (like the whole provided chain).
// TODO: Implement some sort of cache so we don't need to parse PEM & DER every invocation.
func VerifyFromAppleDeviceCA(c *x509.Certificate) error {
block, _ := pem.Decode([]byte(appleiPhoneDeviceCAPEM))
if block == nil || block.Type != "CERTIFICATE" {
panic("appleiPhoneDeviceCAPEM: invalid PEM block")
}
parent, err := x509.ParseCertificate(block.Bytes)
if err != nil {
panic(fmt.Sprintf("appleiPhoneDeviceCAPEM: err parsing: %s", err))
}
// Note we CheckSignatureFrom() as we cannot Verify the certificate chain
// (known expired intermediate)
return c.CheckSignatureFrom(parent)
}

View File

@@ -5,18 +5,43 @@ import (
"errors"
"fmt"
"github.com/micromdm/micromdm/crypto"
boltdepot "github.com/micromdm/scep/depot/bolt"
"github.com/fullsailor/pkcs7"
"github.com/go-kit/kit/endpoint"
"github.com/micromdm/mdm"
)
type Endpoints struct {
GetEnrollEndpoint endpoint.Endpoint
GetEnrollEndpoint endpoint.Endpoint
OTAEnrollEndpoint endpoint.Endpoint
OTAPhase2Phase3Endpoint endpoint.Endpoint
}
type depEnrollmentRequest struct {
mdm.DEPEnrollmentRequest
}
// TODO: may overlap at some point with mdm.DEPEnrollmentRequest
type otaEnrollmentRequest struct {
Challenge string `plist:"CHALLENGE"`
Product string `plist:"PRODUCT"`
Serial string `plist:"SERIAL"`
UDID string `plist:"UDID"`
Version string `plist:"VERSION"` // build no.
IMSI string `plist:"IMSI"`
IMEI string `plist:"IMEI,omitempty"`
MEID string `plist:"MEID,omitempty"`
ICCID string `plist:"ICCID"`
MACAddressEN0 string `plist:"MAC_ADDRESS_EN0"`
DeviceName string `plist:"DEVICE_NAME"`
NotOnConsole bool
UserID string // GUID of User
UserLongName string
UserShortName string
}
type mdmEnrollRequest struct{}
type mdmEnrollResponse struct {
@@ -24,9 +49,21 @@ type mdmEnrollResponse struct {
Err error `plist:"error,omitempty"`
}
func MakeServerEndpoints(s Service) Endpoints {
type mdmOTAPhase2Phase3Request struct {
otaEnrollmentRequest otaEnrollmentRequest
p7 *pkcs7.PKCS7
}
type mdmOTAEnrollResponse struct {
Payload
Err error `plist:"error,omitempty"`
}
func MakeServerEndpoints(s Service, scepDepot *boltdepot.Depot) Endpoints {
return Endpoints{
GetEnrollEndpoint: MakeGetEnrollEndpoint(s),
GetEnrollEndpoint: MakeGetEnrollEndpoint(s),
OTAEnrollEndpoint: MakeOTAEnrollEndpoint(s),
OTAPhase2Phase3Endpoint: MakeOTAPhase2Phase3Endpoint(s, scepDepot),
}
}
@@ -45,3 +82,61 @@ func MakeGetEnrollEndpoint(s Service) endpoint.Endpoint {
}
}
}
func MakeOTAEnrollEndpoint(s Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
payload, err := s.OTAEnroll(ctx)
return mdmOTAEnrollResponse{payload, err}, nil
}
}
func MakeOTAPhase2Phase3Endpoint(s Service, scepDepot *boltdepot.Depot) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(mdmOTAPhase2Phase3Request)
if req.p7 == nil || req.p7.GetOnlySigner() == nil {
return nil, errors.New("invalid signer/signer not provided")
}
// TODO: currently only verifying the signing certificate but ought to
// verify the whole provided chain. Note this will be difficult to do
// given the inconsist certificate chain returned by macOS in OTA mode,
// macOS in DEP mode, and iOS in either mode. See:
// https://openradar.appspot.com/radar?id=4957320861712384
if err := crypto.VerifyFromAppleDeviceCA(req.p7.GetOnlySigner()); err == nil {
// signing certificate is signed by the Apple Device CA. this means
// we don't yet have a SCEP identity and thus are in Phase 2 of the
// OTA enrollment
profile, err := s.OTAPhase2(ctx)
return mdmEnrollResponse{profile, err}, nil
}
caChain, _, err := scepDepot.CA(nil)
if err != nil {
return nil, err
}
if len(caChain) < 1 {
return nil, errors.New("invalid SCEP CA chain")
}
if req.p7.GetOnlySigner().CheckSignatureFrom(caChain[0]) == nil {
// signing certificate is signed by our SCEP CA. this means we
// we are in Phase 3 of OTA enrollment (as we already have a
// identified certificate)
// TODO: possibly deliver a different enrollment profile based
// on device certificates
// TODO: we can encrypt the enrollment (or any profile) at this
// point: we have a device identity that we can encrypt to that
// device's private key that it can decrypt
// TODO: the SCEP CA checking ought to be more robust
// see: https://github.com/micromdm/scep/issues/32
profile, err := s.Enroll(ctx)
// profile, err := s.OTAPhase3(ctx)
return mdmEnrollResponse{profile, err}, nil
}
return mdmEnrollResponse{Profile{}, errors.New("unauthorized client")}, nil
}
}

View File

@@ -77,3 +77,9 @@ type MDMPayloadContent struct {
ServerURL string
Topic string
}
type ProfileServicePayload struct {
URL string
Challenge string `plist:",omitempty"`
DeviceAttributes []string
}

View File

@@ -1,6 +1,7 @@
package enroll
import (
"crypto/x509"
"golang.org/x/net/context"
"io/ioutil"
"strings"
@@ -8,6 +9,9 @@ import (
type Service interface {
Enroll(ctx context.Context) (Profile, error)
OTAEnroll(ctx context.Context) (Payload, error)
OTAPhase2(ctx context.Context) (Profile, error)
OTAPhase3(ctx context.Context) (Profile, error)
}
func NewService(pushTopic, caCertPath, scepURL, scepChallenge, url, tlsCertPath, scepSubject string) (Service, error) {
@@ -145,3 +149,64 @@ func (svc service) Enroll(ctx context.Context) (Profile, error) {
return *profile, nil
}
// OTAEnroll returns an Over-the-Air "Profile Service" Payload for enrollment.
func (svc service) OTAEnroll(ctx context.Context) (Payload, error) {
payload := NewPayload("Profile Service")
payload.PayloadIdentifier = "com.github.micromdm.ota.profile-service"
payload.PayloadDisplayName = "MicroMDM Profile Service"
payload.PayloadDescription = "Profile Service enrollment"
payload.PayloadOrganization = "MicroMDM"
payload.PayloadContent = ProfileServicePayload{
URL: svc.URL + "/ota/phase23",
Challenge: "",
DeviceAttributes: []string{"UDID", "VERSION", "PRODUCT", "SERIAL", "MEID", "IMEI"},
}
// yes, this is a bare Payload, not a Profile
return *payload, nil
}
// OTAPhase2 returns a SCEP Profile for use in phase 2 of Over-the-Air enrollment.
func (svc service) OTAPhase2(ctx context.Context) (Profile, error) {
profile := NewProfile()
profile.PayloadIdentifier = "com.github.micromdm.micromdm.ota-scep"
profile.PayloadOrganization = "MicroMDM"
profile.PayloadDisplayName = "Enrollment Profile"
profile.PayloadDescription = "The server may alter your settings"
profile.PayloadScope = "System"
scepContent := SCEPPayloadContent{
URL: svc.SCEPURL,
Keysize: 1024,
KeyType: "RSA",
KeyUsage: int(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment),
Name: "Device Management Identity Certificate",
Subject: svc.SCEPSubject,
}
if svc.SCEPChallenge != "" {
scepContent.Challenge = svc.SCEPChallenge
}
scepPayload := NewPayload("com.apple.security.scep")
scepPayload.PayloadDescription = "Configures SCEP"
scepPayload.PayloadDisplayName = "SCEP"
scepPayload.PayloadIdentifier = "com.github.micromdm.scep"
scepPayload.PayloadOrganization = "MicroMDM"
scepPayload.PayloadContent = scepContent
scepPayload.PayloadScope = "System"
profile.PayloadContent = append(profile.PayloadContent, *scepPayload)
return *profile, nil
}
// OTAPhase3 returns a Profile for use in phase 3 of Over-the-Air profile enrollment.
// This would typically be the final or end profile of the Over-the-Air
// enrollment process. In our case this would probably be a device-specifc
// MDM enrollment payload.
// TODO: Not implemented.
func (svc service) OTAPhase3(ctx context.Context) (Profile, error) {
return Profile{}, nil
}

View File

@@ -1,68 +0,0 @@
package enroll
import (
"context"
"errors"
"io/ioutil"
"net/http"
"github.com/fullsailor/pkcs7"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
"github.com/groob/plist"
)
// ServiceHandler returns an HTTP Handler for the enroll service
func ServiceHandler(ctx context.Context, svc Service, logger log.Logger) http.Handler {
r := mux.NewRouter()
e := MakeServerEndpoints(svc)
opts := []httptransport.ServerOption{
httptransport.ServerErrorLogger(logger),
}
r.Methods("GET", "POST").Path("/mdm/enroll").Handler(httptransport.NewServer(
e.GetEnrollEndpoint,
decodeMDMEnrollRequest,
encodeResponse,
opts...,
))
return r
}
func decodeMDMEnrollRequest(_ context.Context, r *http.Request) (interface{}, error) {
switch r.Method {
case "GET":
return mdmEnrollRequest{}, nil
case "POST":
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
p7, err := pkcs7.Parse(data)
if err != nil {
return nil, err
}
// TODO: We should verify but not currently possible. Apple
// does no provide a cert for the CA.
var request depEnrollmentRequest
if err := plist.Unmarshal(p7.Content, &request); err != nil {
return nil, err
}
return request, nil
default:
return nil, errors.New("unknown enrollment method")
}
}
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
resp := response.(mdmEnrollResponse)
w.Header().Set("Content-Type", "application/x-apple-aspen-config")
if err := plist.NewEncoder(w).Encode(resp); err != nil {
return err
}
return nil
}

107
enroll/transport_http.go Normal file
View File

@@ -0,0 +1,107 @@
package enroll
import (
"context"
"errors"
"io/ioutil"
"net/http"
"github.com/fullsailor/pkcs7"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/groob/plist"
)
type HTTPHandlers struct {
EnrollHandler http.Handler
OTAEnrollHandler http.Handler
// In Apple's Over-the-Air design Phases 2 and 3 happen over the same URL.
// The differentiator is which certificate signed the CMS POST body.
OTAPhase2Phase3Handler http.Handler
}
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
h := HTTPHandlers{
EnrollHandler: httptransport.NewServer(
endpoints.GetEnrollEndpoint,
decodeMDMEnrollRequest,
encodeResponse,
opts...,
),
OTAEnrollHandler: httptransport.NewServer(
endpoints.OTAEnrollEndpoint,
nilRequest,
encodeResponse,
opts...,
),
OTAPhase2Phase3Handler: httptransport.NewServer(
endpoints.OTAPhase2Phase3Endpoint,
decodeOTAPhase2Phase3Request,
encodeResponse,
opts...,
),
}
return h
}
func nilRequest(_ context.Context, _ *http.Request) (interface{}, error) {
return nil, nil
}
func decodeMDMEnrollRequest(_ context.Context, r *http.Request) (interface{}, error) {
switch r.Method {
case "GET":
return mdmEnrollRequest{}, nil
case "POST":
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
p7, err := pkcs7.Parse(data)
if err != nil {
return nil, err
}
// TODO: We should verify but not currently possible. Apple
// does no provide a cert for the CA.
var request depEnrollmentRequest
if err := plist.Unmarshal(p7.Content, &request); err != nil {
return nil, err
}
return request, nil
default:
return nil, errors.New("unknown enrollment method")
}
}
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", "application/x-apple-aspen-config")
enc := plist.NewEncoder(w)
enc.Indent(" ")
if err := enc.Encode(response); err != nil {
return err
}
return nil
}
func decodeOTAPhase2Phase3Request(_ context.Context, r *http.Request) (interface{}, error) {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
p7, err := pkcs7.Parse(data)
if err != nil {
return nil, err
}
err = p7.Verify()
if err != nil {
return nil, err
}
var request otaEnrollmentRequest
err = plist.Unmarshal(p7.Content, &request)
if err != nil {
return nil, err
}
return mdmOTAPhase2Phase3Request{request, p7}, nil
}

View File

@@ -226,11 +226,13 @@ func serve(args []string) error {
pushHandlers := nanopush.MakeHTTPHandlers(ctx, pushEndpoints, checkinOpts...)
scepHandler := scep.ServiceHandler(ctx, sm.scepService, httpLogger)
enrollHandler := enroll.ServiceHandler(ctx, sm.enrollService, httpLogger)
enrollHandlers := enroll.MakeHTTPHandlers(ctx, enroll.MakeServerEndpoints(sm.enrollService, sm.scepDepot), httptransport.ServerErrorLogger(httpLogger))
r := mux.NewRouter()
r.Handle("/mdm/checkin", mdmAuthSignMessageMiddleware(sm.scepDepot, checkinHandlers.CheckinHandler)).Methods("PUT")
r.Handle("/mdm/connect", mdmAuthSignMessageMiddleware(sm.scepDepot, connectHandlers.ConnectHandler)).Methods("PUT")
r.Handle("/mdm/enroll", enrollHandler).Methods("GET", "POST")
r.Handle("/mdm/enroll", enrollHandlers.EnrollHandler).Methods("GET", "POST")
r.Handle("/ota/enroll", enrollHandlers.OTAEnrollHandler)
r.Handle("/ota/phase23", enrollHandlers.OTAPhase2Phase3Handler).Methods("POST")
r.Handle("/scep", scepHandler)
r.Handle("/push/{udid}", pushHandlers.PushHandler)
r.Handle("/v1/commands", commandHandlers.NewCommandHandler).Methods("POST")