Files
micromdm/server/server.go
2018-07-13 21:40:13 +00:00

457 lines
11 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
// TODO: refactor enroll service and remove the need to reference
// this on-disk cert. but it might be useful to keep the PEM
// around for anyone who will need to export the CA.
SCEPCACertPath string
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)
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.SCEPCACertPath,
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
}
caCert, err := depot.CreateOrLoadCA(key, 5, "MicroMDM", "US")
if err != nil {
return err
}
c.SCEPCACertPath = filepath.Join(c.ConfigPath, "SCEPCACert.pem")
err = crypto.WritePEMCertificateFile(caCert, c.SCEPCACertPath)
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
}