mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-13 05:45:41 +08:00
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:
109
crypto/apple.go
Normal file
109
crypto/apple.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,3 +77,9 @@ type MDMPayloadContent struct {
|
||||
ServerURL string
|
||||
Topic string
|
||||
}
|
||||
|
||||
type ProfileServicePayload struct {
|
||||
URL string
|
||||
Challenge string `plist:",omitempty"`
|
||||
DeviceAttributes []string
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
107
enroll/transport_http.go
Normal 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
|
||||
}
|
||||
6
serve.go
6
serve.go
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user