mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-13 14:05:42 +08:00
The SCEP CA certificate is not needed to establish trust between the client and MDM server. Thus including it in the enrollment profile is unnecessary. Separately, and since we don't need it, we can clean up the enrollment service a little by not reading the certificate from disk. Finally — just don't write it out to disk at all. Its usefulness on disk (for the sake of being in its own file) is debatable.
447 lines
10 KiB
Go
447 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/RobotsAndPencils/buford/push"
|
|
"github.com/boltdb/bolt"
|
|
"github.com/go-kit/kit/log"
|
|
"github.com/go-kit/kit/log/level"
|
|
"github.com/micromdm/dep"
|
|
boltdepot "github.com/micromdm/scep/depot/bolt"
|
|
scep "github.com/micromdm/scep/server"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/pkcs12"
|
|
|
|
"github.com/micromdm/micromdm/dep/depsync"
|
|
"github.com/micromdm/micromdm/mdm"
|
|
"github.com/micromdm/micromdm/mdm/enroll"
|
|
"github.com/micromdm/micromdm/pkg/crypto"
|
|
"github.com/micromdm/micromdm/platform/apns"
|
|
apnsbuiltin "github.com/micromdm/micromdm/platform/apns/builtin"
|
|
"github.com/micromdm/micromdm/platform/command"
|
|
"github.com/micromdm/micromdm/platform/config"
|
|
configbuiltin "github.com/micromdm/micromdm/platform/config/builtin"
|
|
"github.com/micromdm/micromdm/platform/profile"
|
|
profilebuiltin "github.com/micromdm/micromdm/platform/profile/builtin"
|
|
"github.com/micromdm/micromdm/platform/pubsub"
|
|
"github.com/micromdm/micromdm/platform/pubsub/inmem"
|
|
"github.com/micromdm/micromdm/platform/queue"
|
|
block "github.com/micromdm/micromdm/platform/remove"
|
|
blockbuiltin "github.com/micromdm/micromdm/platform/remove/builtin"
|
|
"github.com/micromdm/micromdm/workflow/webhook"
|
|
)
|
|
|
|
type Server struct {
|
|
ConfigPath string
|
|
Depsim string
|
|
PubClient pubsub.PublishSubscriber
|
|
DB *bolt.DB
|
|
PushCert pushServiceCert
|
|
ServerPublicURL string
|
|
SCEPChallenge string
|
|
APNSPrivateKeyPath string
|
|
APNSCertificatePath string
|
|
APNSPrivateKeyPass string
|
|
TLSCertPath string
|
|
SCEPDepot *boltdepot.Depot
|
|
ProfileDB profile.Store
|
|
ConfigDB config.Store
|
|
RemoveDB block.Store
|
|
CommandWebhookURL string
|
|
DEPClient dep.Client
|
|
|
|
PushService *push.Service // bufford push
|
|
APNSPushService apns.Service
|
|
CommandService command.Service
|
|
MDMService mdm.Service
|
|
EnrollService enroll.Service
|
|
SCEPService scep.Service
|
|
ConfigService config.Service
|
|
|
|
WebhooksHTTPClient *http.Client
|
|
}
|
|
|
|
func (c *Server) Setup(logger log.Logger) error {
|
|
if err := c.setupPubSub(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupBolt(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupRemoveService(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupConfigStore(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.loadPushCerts(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupSCEP(logger); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupPushService(logger); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupCommandService(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupWebhooks(logger); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupCommandQueue(logger); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupDepClient(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupProfileDB(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupEnrollmentService(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupProfileDB() error {
|
|
profileDB, err := profilebuiltin.NewDB(c.DB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.ProfileDB = profileDB
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupPubSub() error {
|
|
c.PubClient = inmem.NewPubSub()
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupWebhooks(logger log.Logger) error {
|
|
if c.CommandWebhookURL == "" {
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ww := webhook.New(c.CommandWebhookURL, c.PubClient, webhook.WithLogger(logger), webhook.WithHTTPClient(c.WebhooksHTTPClient))
|
|
go ww.Run(ctx)
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupRemoveService() error {
|
|
removeDB, err := blockbuiltin.NewDB(c.DB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.RemoveDB = removeDB
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupCommandService() error {
|
|
commandService, err := command.New(c.PubClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.CommandService = commandService
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupCommandQueue(logger log.Logger) error {
|
|
q, err := queue.NewQueue(c.DB, c.PubClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var mdmService mdm.Service
|
|
{
|
|
svc := mdm.NewService(c.PubClient, q)
|
|
mdmService = svc
|
|
mdmService = block.RemoveMiddleware(c.RemoveDB)(mdmService)
|
|
}
|
|
c.MDMService = mdmService
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupBolt() error {
|
|
dbPath := filepath.Join(c.ConfigPath, "micromdm.db")
|
|
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
|
if err != nil {
|
|
return errors.Wrap(err, "opening boltdb")
|
|
}
|
|
c.DB = db
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) loadPushCerts() error {
|
|
if c.APNSCertificatePath == "" && c.APNSPrivateKeyPass == "" && c.APNSPrivateKeyPath == "" {
|
|
// this is optional, config could also be provided with mdmctl
|
|
return nil
|
|
}
|
|
var err error
|
|
|
|
if c.APNSPrivateKeyPath == "" {
|
|
var pkcs12Data []byte
|
|
|
|
pkcs12Data, err = ioutil.ReadFile(c.APNSCertificatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.PushCert.PrivateKey, c.PushCert.Certificate, err =
|
|
pkcs12.Decode(pkcs12Data, c.APNSPrivateKeyPass)
|
|
return err
|
|
}
|
|
|
|
c.PushCert.Certificate, err = crypto.ReadPEMCertificateFile(c.APNSCertificatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var pemData []byte
|
|
pemData, err = ioutil.ReadFile(c.APNSPrivateKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pkeyBlock := new(bytes.Buffer)
|
|
pemBlock, _ := pem.Decode(pemData)
|
|
if pemBlock == nil {
|
|
return errors.New("invalid PEM data for privkey")
|
|
}
|
|
|
|
if x509.IsEncryptedPEMBlock(pemBlock) {
|
|
b, err := x509.DecryptPEMBlock(pemBlock, []byte(c.APNSPrivateKeyPass))
|
|
if err != nil {
|
|
return fmt.Errorf("decrypting DES private key %s", err)
|
|
}
|
|
pkeyBlock.Write(b)
|
|
} else {
|
|
pkeyBlock.Write(pemBlock.Bytes)
|
|
}
|
|
|
|
priv, err := x509.ParsePKCS1PrivateKey(pkeyBlock.Bytes())
|
|
if err != nil {
|
|
return fmt.Errorf("parsing pkcs1 private key: %s", err)
|
|
}
|
|
c.PushCert.PrivateKey = priv
|
|
|
|
return nil
|
|
}
|
|
|
|
type pushServiceCert struct {
|
|
*x509.Certificate
|
|
PrivateKey interface{}
|
|
}
|
|
|
|
func (c *Server) setupConfigStore() error {
|
|
db, err := configbuiltin.NewDB(c.DB, c.PubClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.ConfigDB = db
|
|
c.ConfigService = config.New(db)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupPushService(logger log.Logger) error {
|
|
var opts []apns.Option
|
|
{
|
|
cert, _ := c.ConfigDB.PushCertificate()
|
|
if c.PushCert.Certificate != nil && cert == nil {
|
|
cert = &tls.Certificate{
|
|
Certificate: [][]byte{c.PushCert.Certificate.Raw},
|
|
PrivateKey: c.PushCert.PrivateKey,
|
|
Leaf: c.PushCert.Certificate,
|
|
}
|
|
}
|
|
if cert == nil {
|
|
goto after
|
|
}
|
|
client, err := push.NewClient(*cert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
svc := push.NewService(client, push.Production)
|
|
opts = append(opts, apns.WithPushService(svc))
|
|
}
|
|
after:
|
|
|
|
db, err := apnsbuiltin.NewDB(c.DB, c.PubClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
service, err := apns.New(db, c.ConfigDB, c.PubClient, opts...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "starting micromdm push service")
|
|
}
|
|
c.APNSPushService = apns.LoggingMiddleware(
|
|
log.With(level.Info(logger), "component", "apns"),
|
|
)(service)
|
|
|
|
pushinfoWorker := apns.NewWorker(db, c.PubClient, logger)
|
|
go pushinfoWorker.Run(context.Background())
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) setupEnrollmentService() error {
|
|
var topicProvider enroll.TopicProvider
|
|
var err error
|
|
if c.PushCert.Certificate != nil {
|
|
pushTopic, err := crypto.TopicFromCert(c.PushCert.Certificate)
|
|
if err != nil {
|
|
return errors.Wrap(err, "get apns topic from certificate")
|
|
}
|
|
topicProvider = staticTopicProvider{topic: pushTopic}
|
|
} else {
|
|
topicProvider = c.ConfigDB
|
|
}
|
|
|
|
var SCEPCertificateSubject string
|
|
// TODO: clean up order of inputs. Maybe pass *SCEPConfig as an arg?
|
|
// but if you do, the packages are coupled, better not.
|
|
c.EnrollService, err = enroll.NewService(
|
|
topicProvider,
|
|
c.PubClient,
|
|
c.ServerPublicURL+"/scep",
|
|
c.SCEPChallenge,
|
|
c.ServerPublicURL,
|
|
c.TLSCertPath,
|
|
SCEPCertificateSubject,
|
|
c.ProfileDB,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// if the apns-cert flags are specified this provider will be used in the enroll service.
|
|
type staticTopicProvider struct{ topic string }
|
|
|
|
func (p staticTopicProvider) PushTopic() (string, error) {
|
|
return p.topic, nil
|
|
}
|
|
|
|
func (c *Server) setupDepClient() error {
|
|
// depsim config
|
|
depsim := c.Depsim
|
|
var conf *dep.Config
|
|
|
|
// try getting the oauth config from bolt
|
|
tokens, err := c.ConfigDB.DEPTokens()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(tokens) >= 1 {
|
|
conf = new(dep.Config)
|
|
conf.ConsumerSecret = tokens[0].ConsumerSecret
|
|
conf.ConsumerKey = tokens[0].ConsumerKey
|
|
conf.AccessSecret = tokens[0].AccessSecret
|
|
conf.AccessToken = tokens[0].AccessToken
|
|
// TODO: handle expiration
|
|
}
|
|
|
|
// override with depsim keys if specified on CLI
|
|
if depsim != "" {
|
|
conf = &dep.Config{
|
|
ConsumerKey: "CK_48dd68d198350f51258e885ce9a5c37ab7f98543c4a697323d75682a6c10a32501cb247e3db08105db868f73f2c972bdb6ae77112aea803b9219eb52689d42e6",
|
|
ConsumerSecret: "CS_34c7b2b531a600d99a0e4edcf4a78ded79b86ef318118c2f5bcfee1b011108c32d5302df801adbe29d446eb78f02b13144e323eb9aad51c79f01e50cb45c3a68",
|
|
AccessToken: "AT_927696831c59ba510cfe4ec1a69e5267c19881257d4bca2906a99d0785b785a6f6fdeb09774954fdd5e2d0ad952e3af52c6d8d2f21c924ba0caf4a031c158b89",
|
|
AccessSecret: "AS_c31afd7a09691d83548489336e8ff1cb11b82b6bca13f793344496a556b1f4972eaff4dde6deb5ac9cf076fdfa97ec97699c34d515947b9cf9ed31c99dded6ba",
|
|
}
|
|
}
|
|
|
|
if conf == nil {
|
|
return nil
|
|
}
|
|
|
|
depServerURL := "https://mdmenrollment.apple.com"
|
|
if depsim != "" {
|
|
depServerURL = depsim
|
|
}
|
|
client, err := dep.NewClient(conf, dep.ServerURL(depServerURL))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.DEPClient = client
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Server) CreateDEPSyncer(logger log.Logger) (depsync.Syncer, error) {
|
|
client := c.DEPClient
|
|
opts := []depsync.Option{
|
|
depsync.WithLogger(log.With(logger, "component", "depsync")),
|
|
}
|
|
if client != nil {
|
|
opts = append(opts, depsync.WithClient(client))
|
|
}
|
|
|
|
var syncer depsync.Syncer
|
|
syncer, err := depsync.New(c.PubClient, c.DB, logger, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return syncer, nil
|
|
}
|
|
|
|
func (c *Server) setupSCEP(logger log.Logger) error {
|
|
depot, err := boltdepot.NewBoltDepot(c.DB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key, err := depot.CreateOrLoadKey(2048)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = depot.CreateOrLoadCA(key, 5, "MicroMDM", "US")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opts := []scep.ServiceOption{
|
|
scep.ClientValidity(365),
|
|
scep.ChallengePassword(c.SCEPChallenge),
|
|
}
|
|
c.SCEPDepot = depot
|
|
c.SCEPService, err = scep.NewService(depot, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.SCEPService = scep.NewLoggingService(logger, c.SCEPService)
|
|
|
|
return nil
|
|
}
|