mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add TestMDMClient to simulate MDM clients in osquery-perf (#11672)
#11528 osquery-perf simulated hosts enroll and are identified as manually enrolled. (Enrolling as DEP requires more work, e.g. a new mocked Apple DEP endpoint). Given that these are simulated MDM clients, they cannot be woken up with push notifications. Instead, these check for new commands to execute every 10 seconds (which is not realistic, but could serve as a good loadtesting exercise). I will now start setting up the loadtest environment with MDM enabled and configured to test this. - ~[ ] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information.~ - ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md)~ - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - [X] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - ~For Orbit and Fleet Desktop changes:~ - ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux.~ - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
This commit is contained in:
parent
2c13f16db7
commit
bb3b21b574
9 changed files with 992 additions and 391 deletions
|
|
@ -52,6 +52,7 @@ import (
|
|||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/kolide/kit/version"
|
||||
"github.com/micromdm/nanomdm/cryptoutil"
|
||||
"github.com/micromdm/nanomdm/push"
|
||||
"github.com/micromdm/nanomdm/push/buford"
|
||||
nanomdm_pushsvc "github.com/micromdm/nanomdm/push/service"
|
||||
scep_depot "github.com/micromdm/scep/v2/depot"
|
||||
|
|
@ -166,7 +167,7 @@ the way that the Fleet server works.
|
|||
if config.MysqlReadReplica.Address != "" {
|
||||
opts = append(opts, mysql.Replica(&config.MysqlReadReplica))
|
||||
}
|
||||
if dev && os.Getenv("FLEET_ENABLE_DEV_SQL_INTERCEPTOR") != "" {
|
||||
if dev && os.Getenv("FLEET_DEV_ENABLE_SQL_INTERCEPTOR") != "" {
|
||||
opts = append(opts, mysql.WithInterceptor(&devSQLInterceptor{
|
||||
logger: kitlog.With(logger, "component", "sql-interceptor"),
|
||||
}))
|
||||
|
|
@ -455,7 +456,7 @@ the way that the Fleet server works.
|
|||
appleAPNsKeyPEM []byte
|
||||
depStorage *mysql.NanoDEPStorage
|
||||
mdmStorage *mysql.NanoMDMStorage
|
||||
mdmPushService *nanomdm_pushsvc.PushService
|
||||
mdmPushService push.Pusher
|
||||
mdmCheckinAndCommandService *service.MDMAppleCheckinAndCommandService
|
||||
mdmPushCertTopic string
|
||||
)
|
||||
|
|
@ -534,7 +535,11 @@ the way that the Fleet server works.
|
|||
}
|
||||
nanoMDMLogger := service.NewNanoMDMLogger(kitlog.With(logger, "component", "apple-mdm-push"))
|
||||
pushProviderFactory := buford.NewPushProviderFactory()
|
||||
mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger)
|
||||
if os.Getenv("FLEET_DEV_MDM_APPLE_DISABLE_PUSH") == "1" {
|
||||
mdmPushService = nopPusher{}
|
||||
} else {
|
||||
mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger)
|
||||
}
|
||||
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService)
|
||||
mdmCheckinAndCommandService = service.NewMDMAppleCheckinAndCommandService(ds, commander, logger)
|
||||
appCfg.MDM.EnabledAndConfigured = true
|
||||
|
|
@ -1110,3 +1115,13 @@ func (m *debugMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
m.fleetAuthenticatedHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// nopPusher is a no-op push.Pusher.
|
||||
type nopPusher struct{}
|
||||
|
||||
var _ push.Pusher = nopPusher{}
|
||||
|
||||
// Push implements push.Pusher.
|
||||
func (n nopPusher) Push(context.Context, []string) (map[string]*push.Response, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import (
|
|||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -60,12 +62,15 @@ func init() {
|
|||
}
|
||||
|
||||
type Stats struct {
|
||||
errors int
|
||||
enrollments int
|
||||
orbitenrollments int
|
||||
distributedwrites int
|
||||
orbitErrors int
|
||||
desktopErrors int
|
||||
errors int
|
||||
osqueryEnrollments int
|
||||
orbitEnrollments int
|
||||
mdmEnrollments int
|
||||
distributedWrites int
|
||||
mdmCommandsReceived int
|
||||
orbitErrors int
|
||||
mdmErrors int
|
||||
desktopErrors int
|
||||
|
||||
l sync.Mutex
|
||||
}
|
||||
|
|
@ -79,19 +84,31 @@ func (s *Stats) IncrementErrors(errors int) {
|
|||
func (s *Stats) IncrementEnrollments() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
s.enrollments++
|
||||
s.osqueryEnrollments++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementOrbitEnrollments() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
s.orbitenrollments++
|
||||
s.orbitEnrollments++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementMDMEnrollments() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
s.mdmEnrollments++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementDistributedWrites() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
s.distributedwrites++
|
||||
s.distributedWrites++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementMDMCommandsReceived() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
s.mdmCommandsReceived++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementOrbitErrors() {
|
||||
|
|
@ -100,6 +117,12 @@ func (s *Stats) IncrementOrbitErrors() {
|
|||
s.orbitErrors++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementMDMErrors() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
s.mdmErrors++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementDesktopErrors() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
|
@ -111,14 +134,17 @@ func (s *Stats) Log() {
|
|||
defer s.l.Unlock()
|
||||
|
||||
fmt.Printf(
|
||||
"%s :: error rate: %.2f \t enrollments: %d \t orbit enrollments: %d \t writes: %d\n \t orbit errors: %d \t desktop errors: %d",
|
||||
time.Now().String(),
|
||||
float64(s.errors)/float64(s.enrollments),
|
||||
s.enrollments,
|
||||
s.orbitenrollments,
|
||||
s.distributedwrites,
|
||||
"%s :: error rate: %.2f, osquery enrolls: %d, orbit enrolls: %d, mdm enrolls: %d, distributed/writes: %d, mdm commands received: %d, orbit errors: %d, desktop errors: %d, mdm errors: %d\n",
|
||||
time.Now().Format("2006-01-02T15:04:05Z"),
|
||||
float64(s.errors)/float64(s.osqueryEnrollments),
|
||||
s.osqueryEnrollments,
|
||||
s.orbitEnrollments,
|
||||
s.mdmEnrollments,
|
||||
s.distributedWrites,
|
||||
s.mdmCommandsReceived,
|
||||
s.orbitErrors,
|
||||
s.desktopErrors,
|
||||
s.mdmErrors,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -208,13 +234,23 @@ type agent struct {
|
|||
|
||||
scheduledQueries []string
|
||||
|
||||
// mdmClient simulates a device running the MDM protocol (client side).
|
||||
mdmClient *mdmtest.TestMDMClient
|
||||
// isEnrolledToMDM is true when the mdmDevice has enrolled.
|
||||
isEnrolledToMDM bool
|
||||
// isEnrolledToMDMMu protects isEnrolledToMDM.
|
||||
isEnrolledToMDMMu sync.Mutex
|
||||
|
||||
//
|
||||
// The following are exported to be used by the templates.
|
||||
//
|
||||
|
||||
EnrollSecret string
|
||||
UUID string
|
||||
SerialNumber string
|
||||
ConfigInterval time.Duration
|
||||
QueryInterval time.Duration
|
||||
MDMCheckInInterval time.Duration
|
||||
DiskEncryptionEnabled bool
|
||||
}
|
||||
|
||||
|
|
@ -236,12 +272,17 @@ type softwareEntityCount struct {
|
|||
|
||||
func newAgent(
|
||||
agentIndex int,
|
||||
serverAddress, enrollSecret string, templates *template.Template,
|
||||
configInterval, queryInterval time.Duration, softwareCount softwareEntityCount, userCount entityCount,
|
||||
serverAddress, enrollSecret string,
|
||||
templates *template.Template,
|
||||
configInterval, queryInterval, mdmCheckInInterval time.Duration,
|
||||
softwareCount softwareEntityCount,
|
||||
userCount entityCount,
|
||||
policyPassProb float64,
|
||||
orbitProb float64,
|
||||
munkiIssueProb float64, munkiIssueCount int,
|
||||
emptySerialProb float64,
|
||||
mdmProb float64,
|
||||
mdmSCEPChallenge string,
|
||||
) *agent {
|
||||
var deviceAuthToken *string
|
||||
if rand.Float64() <= orbitProb {
|
||||
|
|
@ -251,9 +292,21 @@ func newAgent(
|
|||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
serial := randSerial()
|
||||
serialNumber := mdmtest.RandSerialNumber()
|
||||
if rand.Float64() <= emptySerialProb {
|
||||
serial = ""
|
||||
serialNumber = ""
|
||||
}
|
||||
uuid := strings.ToUpper(uuid.New().String())
|
||||
var mdmClient *mdmtest.TestMDMClient
|
||||
if rand.Float64() <= mdmProb {
|
||||
mdmClient = mdmtest.NewTestMDMClientDirect(mdmtest.EnrollInfo{
|
||||
SCEPChallenge: mdmSCEPChallenge,
|
||||
SCEPURL: serverAddress + apple_mdm.SCEPPath,
|
||||
MDMURL: serverAddress + apple_mdm.MDMPath,
|
||||
})
|
||||
// Have the osquery agent match the MDM device serial number and UUID.
|
||||
serialNumber = mdmClient.SerialNumber
|
||||
uuid = mdmClient.UUID
|
||||
}
|
||||
return &agent{
|
||||
agentIndex: agentIndex,
|
||||
|
|
@ -271,11 +324,14 @@ func newAgent(
|
|||
deviceAuthToken: deviceAuthToken,
|
||||
os: strings.TrimRight(templates.Name(), ".tmpl"),
|
||||
|
||||
EnrollSecret: enrollSecret,
|
||||
ConfigInterval: configInterval,
|
||||
QueryInterval: queryInterval,
|
||||
UUID: strings.ToUpper(uuid.New().String()),
|
||||
SerialNumber: serial,
|
||||
EnrollSecret: enrollSecret,
|
||||
ConfigInterval: configInterval,
|
||||
QueryInterval: queryInterval,
|
||||
MDMCheckInInterval: mdmCheckInInterval,
|
||||
UUID: uuid,
|
||||
SerialNumber: serialNumber,
|
||||
|
||||
mdmClient: mdmClient,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +372,17 @@ func (a *agent) runLoop(i int, onlyAlreadyEnrolled bool) {
|
|||
go a.runOrbitLoop()
|
||||
}
|
||||
|
||||
if a.mdmClient != nil {
|
||||
if err := a.mdmClient.Enroll(); err != nil {
|
||||
log.Printf("MDM enroll failed: %s\n", err)
|
||||
a.stats.IncrementMDMErrors()
|
||||
return
|
||||
}
|
||||
a.setMDMEnrolled()
|
||||
a.stats.IncrementMDMEnrollments()
|
||||
go a.runMDMLoop()
|
||||
}
|
||||
|
||||
configTicker := time.Tick(a.ConfigInterval)
|
||||
liveQueryTicker := time.Tick(a.QueryInterval)
|
||||
for {
|
||||
|
|
@ -459,6 +526,29 @@ func (a *agent) runOrbitLoop() {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *agent) runMDMLoop() {
|
||||
mdmCheckInTicker := time.Tick(a.MDMCheckInInterval)
|
||||
|
||||
for range mdmCheckInTicker {
|
||||
mdmCommandPayload, err := a.mdmClient.Idle()
|
||||
if err != nil {
|
||||
log.Printf("MDM Idle request failed: %s\n", err)
|
||||
a.stats.IncrementMDMErrors()
|
||||
continue
|
||||
}
|
||||
INNER_FOR_LOOP:
|
||||
for mdmCommandPayload != nil {
|
||||
a.stats.IncrementMDMCommandsReceived()
|
||||
mdmCommandPayload, err = a.mdmClient.Acknowledge(mdmCommandPayload.CommandUUID)
|
||||
if err != nil {
|
||||
log.Printf("MDM Acknowledge request failed: %s\n", err)
|
||||
a.stats.IncrementMDMErrors()
|
||||
break INNER_FOR_LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) waitingDo(req *fasthttp.Request, res *fasthttp.Response) {
|
||||
err := a.fastClient.Do(req, res)
|
||||
for err != nil || res.StatusCode() != http.StatusOK {
|
||||
|
|
@ -888,21 +978,38 @@ var possibleMDMServerURLs = []string{
|
|||
"https://example.com/2",
|
||||
}
|
||||
|
||||
// mdmMac returns the results for the `mdm` table query.
|
||||
//
|
||||
// If the host is enrolled via MDM it will return installed_from_dep as false
|
||||
// (which means the host will be identified as manually enrolled).
|
||||
//
|
||||
// NOTE: To support proper DEP simulation in a loadtest environment
|
||||
// we may need to implement a mocked Apple DEP endpoint.
|
||||
func (a *agent) mdmMac() []map[string]string {
|
||||
enrolled := "true"
|
||||
if rand.Intn(2) == 1 {
|
||||
enrolled = "false"
|
||||
if !a.mdmEnrolled() {
|
||||
return []map[string]string{
|
||||
{"enrolled": "false", "server_url": "", "installed_from_dep": "false"},
|
||||
}
|
||||
}
|
||||
installedFromDep := "true"
|
||||
if rand.Intn(2) == 1 {
|
||||
installedFromDep = "false"
|
||||
}
|
||||
ix := rand.Intn(len(possibleMDMServerURLs))
|
||||
return []map[string]string{
|
||||
{"enrolled": enrolled, "server_url": possibleMDMServerURLs[ix], "installed_from_dep": installedFromDep},
|
||||
{"enrolled": "true", "server_url": a.mdmClient.EnrollInfo.MDMURL, "installed_from_dep": "false"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) mdmEnrolled() bool {
|
||||
a.isEnrolledToMDMMu.Lock()
|
||||
defer a.isEnrolledToMDMMu.Unlock()
|
||||
|
||||
return a.isEnrolledToMDM
|
||||
}
|
||||
|
||||
func (a *agent) setMDMEnrolled() {
|
||||
a.isEnrolledToMDMMu.Lock()
|
||||
defer a.isEnrolledToMDMMu.Unlock()
|
||||
|
||||
a.isEnrolledToMDM = true
|
||||
}
|
||||
|
||||
func (a *agent) mdmWindows() []map[string]string {
|
||||
autopilot := rand.Intn(2) == 1
|
||||
ix := rand.Intn(len(possibleMDMServerURLs))
|
||||
|
|
@ -1216,6 +1323,7 @@ func main() {
|
|||
startPeriod = flag.Duration("start_period", 10*time.Second, "Duration to spread start of hosts over")
|
||||
configInterval = flag.Duration("config_interval", 1*time.Minute, "Interval for config requests")
|
||||
queryInterval = flag.Duration("query_interval", 10*time.Second, "Interval for live query requests")
|
||||
mdmCheckInInterval = flag.Duration("mdm_check_in_interval", 10*time.Second, "Interval for performing MDM check ins")
|
||||
onlyAlreadyEnrolled = flag.Bool("only_already_enrolled", false, "Only start agents that are already enrolled")
|
||||
nodeKeyFile = flag.String("node_key_file", "", "File with node keys to use")
|
||||
|
||||
|
|
@ -1237,7 +1345,10 @@ func main() {
|
|||
munkiIssueProb = flag.Float64("munki_issue_prob", 0.5, "Probability of a host having munki issues (note that ~50% of hosts have munki installed) [0, 1]")
|
||||
munkiIssueCount = flag.Int("munki_issue_count", 10, "Number of munki issues reported by hosts identified to have munki issues")
|
||||
osTemplates = flag.String("os_templates", "mac10.14.6", fmt.Sprintf("Comma separated list of host OS templates to use (any of %v, with or without the .tmpl extension)", allowedTemplateNames))
|
||||
emptySerialProb = flag.Float64("empty_serial_prob", 0.1, "Probability of a host having no serial number [0, 1]")
|
||||
emptySerialProb = flag.Float64("empty_serial_prob", 0.0, "Probability of a host having no serial number [0, 1]")
|
||||
|
||||
mdmProb = flag.Float64("mdm_prob", 0.1, "Probability of a host enrolling via MDM (for macOS) [0, 1]")
|
||||
mdmSCEPChallenge = flag.String("mdm_scep_challenge", "", "SCEP challenge to use when running MDM enroll")
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
|
|
@ -1287,7 +1398,13 @@ func main() {
|
|||
|
||||
for i := 0; i < *hostCount; i++ {
|
||||
tmpl := tmpls[i%len(tmpls)]
|
||||
a := newAgent(i+1, *serverURL, *enrollSecret, tmpl, *configInterval, *queryInterval,
|
||||
a := newAgent(i+1,
|
||||
*serverURL,
|
||||
*enrollSecret,
|
||||
tmpl,
|
||||
*configInterval,
|
||||
*queryInterval,
|
||||
*mdmCheckInInterval,
|
||||
softwareEntityCount{
|
||||
entityCount: entityCount{
|
||||
common: *commonSoftwareCount,
|
||||
|
|
@ -1309,6 +1426,8 @@ func main() {
|
|||
*munkiIssueProb,
|
||||
*munkiIssueCount,
|
||||
*emptySerialProb,
|
||||
*mdmProb,
|
||||
*mdmSCEPChallenge,
|
||||
)
|
||||
a.stats = stats
|
||||
a.nodeKeyManager = nodeKeyManager
|
||||
|
|
@ -1319,15 +1438,3 @@ func main() {
|
|||
fmt.Println("Agents running. Kill with C-c.")
|
||||
<-make(chan struct{})
|
||||
}
|
||||
|
||||
// numbers plus capital letters without I, L, O for readability
|
||||
const serialLetters = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"
|
||||
|
||||
func randSerial() string {
|
||||
b := make([]byte, 12)
|
||||
for i := range b {
|
||||
//nolint:gosec // not used for crypto, only to generate random serial for testing
|
||||
b[i] = serialLetters[rand.Intn(len(serialLetters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
|
|
|||
625
pkg/mdm/mdmtest/mdmtest.go
Normal file
625
pkg/mdm/mdmtest/mdmtest.go
Normal file
|
|
@ -0,0 +1,625 @@
|
|||
// Package mdmtest contains types and methods useful for testing MDM servers.
|
||||
package mdmtest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
mrand "math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/go-kit/kit/log"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
"github.com/google/uuid"
|
||||
"github.com/groob/plist"
|
||||
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
||||
"github.com/micromdm/nanomdm/mdm"
|
||||
"github.com/micromdm/scep/v2/cryptoutil/x509util"
|
||||
"github.com/micromdm/scep/v2/scep"
|
||||
scepserver "github.com/micromdm/scep/v2/server"
|
||||
"go.mozilla.org/pkcs7"
|
||||
)
|
||||
|
||||
// TestMDMClient simulates an MDM client.
|
||||
type TestMDMClient struct {
|
||||
// UUID is a random fake unique ID of the simulated device.
|
||||
UUID string
|
||||
// SerialNumber is a random fake serial number of the simulated device.
|
||||
SerialNumber string
|
||||
// Model is the model of the simulated device.
|
||||
Model string
|
||||
|
||||
// EnrollInfo holds the information necessary to enroll to an MDM server.
|
||||
EnrollInfo EnrollInfo
|
||||
|
||||
// fleetServerURL is the URL of the Fleet server, used to fetch the enrollment profile.
|
||||
fleetServerURL string
|
||||
|
||||
// debug enables debug logging of request/responses.
|
||||
debug bool
|
||||
|
||||
// fetchEnrollmentProfileFromDesktop indicates whether this simulated device
|
||||
// will fetch the enrollment profile from Fleet as if it were a device running
|
||||
// Fleet Desktop.
|
||||
fetchEnrollmentProfileFromDesktop bool
|
||||
// desktopURLToken is the Fleet Desktop token used to fetch the enrollment profile
|
||||
// from Fleet as if it were a device running Fleet Desktop.
|
||||
desktopURLToken string
|
||||
|
||||
// fetchEnrollmentProfileFromDEP indicates whether this simulated device will fetch
|
||||
// the enrollment profile from Fleet as if it were a device running the DEP flow.
|
||||
fetchEnrollmentProfileFromDEP bool
|
||||
// desktopURLToken is the token used to fetch the enrollment profile
|
||||
// from Fleet as if it were a device running the DEP flow.
|
||||
depURLToken string
|
||||
|
||||
// scepCert contains the SCEP client certificate generated during the
|
||||
// SCEP enrollment process.
|
||||
scepCert *x509.Certificate
|
||||
// scepKey contains the SCEP client private key generated during the
|
||||
// SCEP enrollment process.
|
||||
scepKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// TestMDMClientOption allows configuring a TestMDMClient.
|
||||
type TestMDMClientOption func(*TestMDMClient)
|
||||
|
||||
// TestMDMClientDebug configures the TestMDMClient to run in debug mode.
|
||||
func TestMDMClientDebug() TestMDMClientOption {
|
||||
return func(c *TestMDMClient) {
|
||||
c.debug = true
|
||||
}
|
||||
}
|
||||
|
||||
// EnrollInfo contains the necessary information to enroll to an MDM server.
|
||||
type EnrollInfo struct {
|
||||
// SCEPChallenge is the SCEP challenge to present to the SCEP server when enrolling.
|
||||
SCEPChallenge string
|
||||
// SCEPURL is the URL of the SCEP server.
|
||||
SCEPURL string
|
||||
// MDMURL is the URL of the MDM server.
|
||||
MDMURL string
|
||||
}
|
||||
|
||||
// NewTestMDMClientDesktopManual will create a simulated device that will fetch
|
||||
// enrollment profile from Fleet as if it were a device running Fleet Desktop.
|
||||
func NewTestMDMClientDesktopManual(serverURL string, desktopURLToken string, opts ...TestMDMClientOption) *TestMDMClient {
|
||||
c := TestMDMClient{
|
||||
UUID: strings.ToUpper(uuid.New().String()),
|
||||
SerialNumber: RandSerialNumber(),
|
||||
Model: "MacBookPro16,1",
|
||||
|
||||
fetchEnrollmentProfileFromDesktop: true,
|
||||
desktopURLToken: desktopURLToken,
|
||||
|
||||
fleetServerURL: serverURL,
|
||||
}
|
||||
for _, fn := range opts {
|
||||
fn(&c)
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
// NewTestMDMClientDEP will create a simulated device that will fetch
|
||||
// enrollment profile from Fleet as if it were a device running the DEP flow.
|
||||
func NewTestMDMClientDEP(serverURL string, depURLToken string, opts ...TestMDMClientOption) *TestMDMClient {
|
||||
c := TestMDMClient{
|
||||
UUID: strings.ToUpper(uuid.New().String()),
|
||||
SerialNumber: RandSerialNumber(),
|
||||
Model: "MacBookPro16,1",
|
||||
|
||||
fetchEnrollmentProfileFromDEP: true,
|
||||
depURLToken: depURLToken,
|
||||
|
||||
fleetServerURL: serverURL,
|
||||
}
|
||||
for _, fn := range opts {
|
||||
fn(&c)
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
// NewTestMDMClientDEP will create a simulated device that will not fetch the enrollment
|
||||
// profile from Fleet. The enrollment information is to be provided in the enrollInfo.
|
||||
func NewTestMDMClientDirect(enrollInfo EnrollInfo, opts ...TestMDMClientOption) *TestMDMClient {
|
||||
c := TestMDMClient{
|
||||
UUID: strings.ToUpper(uuid.New().String()),
|
||||
SerialNumber: RandSerialNumber(),
|
||||
Model: "MacBookPro16,1",
|
||||
|
||||
EnrollInfo: enrollInfo,
|
||||
}
|
||||
for _, fn := range opts {
|
||||
fn(&c)
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
// Enroll runs the MDM enroll protocol on the simulated device.
|
||||
func (c *TestMDMClient) Enroll() error {
|
||||
switch {
|
||||
case c.fetchEnrollmentProfileFromDesktop:
|
||||
if err := c.fetchEnrollmentProfileFromDesktopURL(); err != nil {
|
||||
return fmt.Errorf("get enrollment profile from desktop URL: %w", err)
|
||||
}
|
||||
case c.fetchEnrollmentProfileFromDEP:
|
||||
if err := c.fetchEnrollmentProfileFromDEPURL(); err != nil {
|
||||
return fmt.Errorf("get enrollment profile from DEP URL: %w", err)
|
||||
}
|
||||
default:
|
||||
if c.EnrollInfo.SCEPURL == "" || c.EnrollInfo.MDMURL == "" || c.EnrollInfo.SCEPChallenge == "" {
|
||||
return fmt.Errorf("missing info needed to perform enrollment: %+v", c.EnrollInfo)
|
||||
}
|
||||
}
|
||||
if err := c.SCEPEnroll(); err != nil {
|
||||
return fmt.Errorf("scep enroll: %w", err)
|
||||
}
|
||||
if err := c.Authenticate(); err != nil {
|
||||
return fmt.Errorf("authenticate: %w", err)
|
||||
}
|
||||
if err := c.TokenUpdate(); err != nil {
|
||||
return fmt.Errorf("token update: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TestMDMClient) fetchEnrollmentProfileFromDesktopURL() error {
|
||||
return c.fetchEnrollmentProfile(
|
||||
"/api/latest/fleet/device/" + c.desktopURLToken + "/mdm/apple/manual_enrollment_profile",
|
||||
)
|
||||
}
|
||||
|
||||
func (c *TestMDMClient) fetchEnrollmentProfileFromDEPURL() error {
|
||||
return c.fetchEnrollmentProfile(
|
||||
apple_mdm.EnrollPath + "?token=" + c.depURLToken,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *TestMDMClient) fetchEnrollmentProfile(path string) error {
|
||||
request, err := http.NewRequest("GET", c.fleetServerURL+path, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
// #nosec (this client is used for testing only)
|
||||
cc := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}))
|
||||
response, err := cc.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("request error: %d, %s", response.StatusCode, response.Status)
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return fmt.Errorf("close body: %w", err)
|
||||
}
|
||||
enrollInfo, err := ParseEnrollmentProfile(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse enrollment profile: %w", err)
|
||||
}
|
||||
c.EnrollInfo = *enrollInfo
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SCEPEnroll runs the SCEP enroll protocol for the simulated device.
|
||||
func (c *TestMDMClient) SCEPEnroll() error {
|
||||
ctx := context.Background()
|
||||
|
||||
var logger log.Logger
|
||||
if c.debug {
|
||||
logger = kitlog.NewJSONLogger(os.Stdout)
|
||||
} else {
|
||||
logger = kitlog.NewNopLogger()
|
||||
}
|
||||
client, err := newSCEPClient(c.EnrollInfo.SCEPURL, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scep client: %w", err)
|
||||
}
|
||||
|
||||
// (1). Get the CA certificate from the SCEP server.
|
||||
resp, _, err := client.GetCACert(ctx, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get CA cert: %w", err)
|
||||
}
|
||||
caCert, err := x509.ParseCertificates(resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse CA cert: %w", err)
|
||||
}
|
||||
|
||||
// (2). Generate RSA key pair.
|
||||
devicePrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate RSA private key: %w", err)
|
||||
}
|
||||
|
||||
// (3). Generate CSR.
|
||||
cn := fmt.Sprintf("fleet-testdevice-%s", c.UUID)
|
||||
csrTemplate := x509util.CertificateRequest{
|
||||
CertificateRequest: x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: []string{"fleet-organization"},
|
||||
},
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
},
|
||||
ChallengePassword: c.EnrollInfo.SCEPChallenge,
|
||||
}
|
||||
csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, devicePrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create CSR: %w", err)
|
||||
}
|
||||
csr, err := x509.ParseCertificateRequest(csrDerBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse CSR: %w", err)
|
||||
}
|
||||
|
||||
// (4). SCEP requires a certificate for client authentication. We generate a new one
|
||||
// that uses the same CommonName and Key that we are trying to have signed.
|
||||
//
|
||||
// From RFC-8894:
|
||||
// If the client does not have an appropriate existing certificate, then a locally generated
|
||||
// self-signed certificate MUST be used. The keyUsage extension in the certificate MUST indicate that
|
||||
// it is valid for digitalSignature and keyEncipherment (if available). The self-signed certificate
|
||||
// SHOULD use the same subject name and key as in the PKCS #10 request.
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(365 * 24 * time.Hour)
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
certSerialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate cert serial number: %w", err)
|
||||
}
|
||||
deviceCertificateTemplate := x509.Certificate{
|
||||
SerialNumber: certSerialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: csr.Subject.Organization,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
deviceCertificateDerBytes, err := x509.CreateCertificate(
|
||||
rand.Reader,
|
||||
&deviceCertificateTemplate,
|
||||
&deviceCertificateTemplate,
|
||||
&devicePrivateKey.PublicKey,
|
||||
devicePrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create device certificate: %w", err)
|
||||
}
|
||||
deviceCertificateForRequest, err := x509.ParseCertificate(deviceCertificateDerBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse device certificate: %w", err)
|
||||
}
|
||||
|
||||
// (5). Send the PKCSReq message to the SCEP server.
|
||||
pkiMsgReq := &scep.PKIMessage{
|
||||
MessageType: scep.PKCSReq,
|
||||
Recipients: caCert,
|
||||
SignerKey: devicePrivateKey,
|
||||
SignerCert: deviceCertificateForRequest,
|
||||
CSRReqMessage: &scep.CSRReqMessage{
|
||||
ChallengePassword: c.EnrollInfo.SCEPChallenge,
|
||||
},
|
||||
}
|
||||
msg, err := scep.NewCSRRequest(csr, pkiMsgReq, scep.WithLogger(logger))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create CSR request: %w", err)
|
||||
}
|
||||
respBytes, err := client.PKIOperation(ctx, msg.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("do CSR request: %w", err)
|
||||
}
|
||||
pkiMsgResp, err := scep.ParsePKIMessage(respBytes, scep.WithLogger(logger), scep.WithCACerts(msg.Recipients))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse PKIMessage response: %w", err)
|
||||
}
|
||||
if pkiMsgResp.PKIStatus != scep.SUCCESS {
|
||||
return fmt.Errorf("PKIMessage CSR request failed with code: %s, fail info: %s", pkiMsgResp.PKIStatus, pkiMsgResp.FailInfo)
|
||||
}
|
||||
if err := pkiMsgResp.DecryptPKIEnvelope(deviceCertificateForRequest, devicePrivateKey); err != nil {
|
||||
return fmt.Errorf("decrypt PKI envelope: %w", err)
|
||||
}
|
||||
|
||||
// (6). Finally, set the signed certificate returned from the server as the device certificate and key.
|
||||
c.scepCert = pkiMsgResp.CertRepMessage.Certificate
|
||||
c.scepKey = devicePrivateKey
|
||||
|
||||
if c.debug {
|
||||
fmt.Println("SCEP enrollment successful")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate sends the Authenticate message to the MDM server (Check In protocol).
|
||||
func (c *TestMDMClient) Authenticate() error {
|
||||
payload := map[string]any{
|
||||
"MessageType": "Authenticate",
|
||||
"UDID": c.UUID,
|
||||
"Model": c.Model,
|
||||
"DeviceName": "testdevice" + c.SerialNumber,
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
"EnrollmentID": "testenrollmentid-" + c.UUID,
|
||||
"SerialNumber": c.SerialNumber,
|
||||
}
|
||||
_, err := c.request("application/x-apple-aspen-mdm-checkin", payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// TokenUpdate sends the TokenUpdate message to the MDM server (Check In protocol).
|
||||
func (c *TestMDMClient) TokenUpdate() error {
|
||||
payload := map[string]any{
|
||||
"MessageType": "TokenUpdate",
|
||||
"UDID": c.UUID,
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
"EnrollmentID": "testenrollmentid-" + c.UUID,
|
||||
"NotOnConsole": "false",
|
||||
"PushMagic": "pushmagic" + c.SerialNumber,
|
||||
"Token": []byte("token" + c.SerialNumber),
|
||||
}
|
||||
_, err := c.request("application/x-apple-aspen-mdm-checkin", payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// Checkout sends the CheckOut message to the MDM server.
|
||||
func (c *TestMDMClient) Checkout() error {
|
||||
payload := map[string]any{
|
||||
"MessageType": "CheckOut",
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
"UDID": c.UUID,
|
||||
"EnrollmentID": "testenrollmentid-" + c.UUID,
|
||||
}
|
||||
_, err := c.request("application/x-apple-aspen-mdm-checkin", payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// Idle sends an Idle message to the MDM server.
|
||||
//
|
||||
// Devices send an Idle status to signal the server that they're ready to
|
||||
// receive commands. The server can signal back with either a command to run
|
||||
// or an empty (nil, nil) response body to end the communication
|
||||
// (i.e. no commands to run).
|
||||
func (c *TestMDMClient) Idle() (*micromdm.CommandPayload, error) {
|
||||
payload := map[string]any{
|
||||
"Status": "Idle",
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
"UDID": c.UUID,
|
||||
"EnrollmentID": "testenrollmentid-" + c.UUID,
|
||||
}
|
||||
return c.sendAndDecodeCommandResponse(payload)
|
||||
}
|
||||
|
||||
// Acknowledge sends an Acknowledge message to the MDM server.
|
||||
// The cmdUUID is the UUID of the command to reference.
|
||||
//
|
||||
// The server can signal back with either a command to run
|
||||
// or an empty (nil, nil) response body to end the communication
|
||||
// (i.e. no commands to run).
|
||||
func (c *TestMDMClient) Acknowledge(cmdUUID string) (*micromdm.CommandPayload, error) {
|
||||
payload := map[string]any{
|
||||
"Status": "Acknowledged",
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
"UDID": c.UUID,
|
||||
"EnrollmentID": "testenrollmentid-" + c.UUID,
|
||||
"CommandUUID": cmdUUID,
|
||||
}
|
||||
return c.sendAndDecodeCommandResponse(payload)
|
||||
}
|
||||
|
||||
// Err sends an Error message to the MDM server.
|
||||
// The cmdUUID is the UUID of the command to reference.
|
||||
//
|
||||
// The server can signal back with either a command to run
|
||||
// or an empty (nil, nil) response body to end the communication
|
||||
// (i.e. no commands to run).
|
||||
func (c *TestMDMClient) Err(cmdUUID string, errChain []mdm.ErrorChain) (*micromdm.CommandPayload, error) {
|
||||
payload := map[string]any{
|
||||
"Status": "Error",
|
||||
"Topic": "com.apple.mgmt.External." + c.UUID,
|
||||
"UDID": c.UUID,
|
||||
"EnrollmentID": "testenrollmentid-" + c.UUID,
|
||||
"CommandUUID": cmdUUID,
|
||||
"ErrorChain": errChain,
|
||||
}
|
||||
return c.sendAndDecodeCommandResponse(payload)
|
||||
}
|
||||
|
||||
func (c *TestMDMClient) sendAndDecodeCommandResponse(payload map[string]any) (*micromdm.CommandPayload, error) {
|
||||
res, err := c.request("", payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request error: %w", err)
|
||||
}
|
||||
if res.ContentLength == 0 {
|
||||
if c.debug {
|
||||
fmt.Printf("response: no commands returned\n")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
raw, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response body: %w", err)
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Printf("response: %s\n", raw)
|
||||
}
|
||||
if err = res.Body.Close(); err != nil {
|
||||
return nil, fmt.Errorf("close response body: %w", err)
|
||||
}
|
||||
cmd, err := mdm.DecodeCommand(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode command: %w", err)
|
||||
}
|
||||
var p micromdm.CommandPayload
|
||||
err = plist.Unmarshal(cmd.Raw, &p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal command payload: %w", err)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (c *TestMDMClient) request(contentType string, payload map[string]any) (*http.Response, error) {
|
||||
body, err := plist.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal payload: %w", err)
|
||||
}
|
||||
|
||||
signedData, err := pkcs7.NewSignedData(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create signed data: %w", err)
|
||||
}
|
||||
err = signedData.AddSigner(c.scepCert, c.scepKey, pkcs7.SignerInfoConfig{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add signer: %w", err)
|
||||
}
|
||||
sig, err := signedData.Finish()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finish signing: %w", err)
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Printf("request: %s\n", body)
|
||||
}
|
||||
request, err := http.NewRequest("POST", c.EnrollInfo.MDMURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", contentType)
|
||||
request.Header.Set("Mdm-Signature", base64.StdEncoding.EncodeToString(sig))
|
||||
// #nosec (this client is used for testing only)
|
||||
cc := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}))
|
||||
response, err := cc.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request error: %d, %s", response.StatusCode, response.Status)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseEnrollmentProfile parses the enrollment profile and returns the parsed information as EnrollInfo.
|
||||
func ParseEnrollmentProfile(mobileConfig []byte) (*EnrollInfo, error) {
|
||||
var enrollmentProfile struct {
|
||||
PayloadContent []map[string]interface{} `plist:"PayloadContent"`
|
||||
}
|
||||
if err := plist.Unmarshal(mobileConfig, &enrollmentProfile); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal enrollment profile: %w", err)
|
||||
}
|
||||
payloadContent := enrollmentProfile.PayloadContent[0]["PayloadContent"].(map[string]interface{})
|
||||
|
||||
scepChallenge, ok := payloadContent["Challenge"].(string)
|
||||
if !ok || scepChallenge == "" {
|
||||
return nil, errors.New("SCEP Challenge field not found")
|
||||
}
|
||||
scepURL, ok := payloadContent["URL"].(string)
|
||||
if !ok || scepURL == "" {
|
||||
return nil, errors.New("SCEP URL field not found")
|
||||
}
|
||||
mdmURL, ok := enrollmentProfile.PayloadContent[1]["ServerURL"].(string)
|
||||
if !ok || mdmURL == "" {
|
||||
return nil, errors.New("MDM ServerURL field not found")
|
||||
}
|
||||
// Check the server sent a proper APNS topic.
|
||||
if apnsTopic, ok := enrollmentProfile.PayloadContent[1]["Topic"].(string); !ok || apnsTopic == "" {
|
||||
return nil, errors.New("MDM Topic field not found")
|
||||
}
|
||||
return &EnrollInfo{
|
||||
SCEPChallenge: scepChallenge,
|
||||
SCEPURL: scepURL,
|
||||
MDMURL: mdmURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// numbers plus capital letters without I, L, O for readability
|
||||
const serialLetters = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"
|
||||
|
||||
// RandSerialNumber returns a fake random serial number.
|
||||
func RandSerialNumber() string {
|
||||
b := make([]byte, 12)
|
||||
for i := range b {
|
||||
//nolint:gosec // not used for crypto, only to generate random serial for testing
|
||||
b[i] = serialLetters[mrand.Intn(len(serialLetters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type scepClient interface {
|
||||
scepserver.Service
|
||||
Supports(cap string) bool
|
||||
}
|
||||
|
||||
func newSCEPClient(
|
||||
serverURL string,
|
||||
logger log.Logger,
|
||||
) (scepClient, error) {
|
||||
endpoints, err := makeClientSCEPEndpoints(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints.GetEndpoint = scepserver.EndpointLoggingMiddleware(logger)(endpoints.GetEndpoint)
|
||||
endpoints.PostEndpoint = scepserver.EndpointLoggingMiddleware(logger)(endpoints.PostEndpoint)
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// makeClientSCEPClientEndpoints returns an Endpoints struct where each endpoint invokes
|
||||
// the corresponding method on the remote instance, via a transport/http.Client.
|
||||
func makeClientSCEPEndpoints(instance string) (*scepserver.Endpoints, error) {
|
||||
if !strings.HasPrefix(instance, "http") {
|
||||
instance = "http://" + instance
|
||||
}
|
||||
tgt, err := url.Parse(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// #nosec (this client is used for testing only)
|
||||
c := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}))
|
||||
options := []httptransport.ClientOption{
|
||||
httptransport.SetClient(c),
|
||||
}
|
||||
|
||||
return &scepserver.Endpoints{
|
||||
GetEndpoint: httptransport.NewClient(
|
||||
"GET",
|
||||
tgt,
|
||||
scepserver.EncodeSCEPRequest,
|
||||
scepserver.DecodeSCEPResponse,
|
||||
options...).Endpoint(),
|
||||
PostEndpoint: httptransport.NewClient(
|
||||
"POST",
|
||||
tgt,
|
||||
scepserver.EncodeSCEPRequest,
|
||||
scepserver.DecodeSCEPResponse,
|
||||
options...).Endpoint(),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -648,7 +648,7 @@ type Service interface {
|
|||
|
||||
// EnqueueMDMAppleCommand enqueues a command for execution on the given
|
||||
// devices. Note that a deviceID is the same as a host's UUID.
|
||||
EnqueueMDMAppleCommand(ctx context.Context, rawBase64Cmd string, deviceIDs []string, noPush bool) (status int, result *CommandEnqueueResult, err error)
|
||||
EnqueueMDMAppleCommand(ctx context.Context, rawBase64Cmd string, deviceIDs []string) (status int, result *CommandEnqueueResult, err error)
|
||||
|
||||
// EnqueueMDMAppleCommandRemoveEnrollmentProfile enqueues a command to remove the
|
||||
// profile used for Fleet MDM enrollment from the specified device.
|
||||
|
|
|
|||
|
|
@ -59,10 +59,6 @@ func ResolveAppleMDMURL(serverURL string) (string, error) {
|
|||
return resolveURL(serverURL, MDMPath)
|
||||
}
|
||||
|
||||
func ResolveAppleEnrollMDMURL(serverURL string) (string, error) {
|
||||
return resolveURL(serverURL, EnrollPath)
|
||||
}
|
||||
|
||||
func ResolveAppleSCEPURL(serverURL string) (string, error) {
|
||||
return resolveURL(serverURL, SCEPPath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -906,7 +906,7 @@ func (r enqueueMDMAppleCommandResponse) Status() int { return r.status }
|
|||
|
||||
func enqueueMDMAppleCommandEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*enqueueMDMAppleCommandRequest)
|
||||
status, result, err := svc.EnqueueMDMAppleCommand(ctx, req.Command, req.DeviceIDs, false)
|
||||
status, result, err := svc.EnqueueMDMAppleCommand(ctx, req.Command, req.DeviceIDs)
|
||||
if err != nil {
|
||||
return enqueueMDMAppleCommandResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -920,7 +920,6 @@ func (svc *Service) EnqueueMDMAppleCommand(
|
|||
ctx context.Context,
|
||||
rawBase64Cmd string,
|
||||
deviceIDs []string,
|
||||
noPush bool,
|
||||
) (status int, result *fleet.CommandEnqueueResult, err error) {
|
||||
premiumCommands := map[string]bool{
|
||||
"EraseDevice": true,
|
||||
|
|
@ -2548,5 +2547,6 @@ func ReconcileProfiles(
|
|||
if err := ds.BulkUpsertMDMAppleHostProfiles(ctx, failed); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "reverting status of failed profiles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ func TestAppleMDMAuthorization(t *testing.T) {
|
|||
for _, c := range enqueueCmdCases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
ctx = test.UserContext(ctx, c.user)
|
||||
_, _, err = svc.EnqueueMDMAppleCommand(ctx, rawB64FreeCmd, c.uuids, false)
|
||||
_, _, err = svc.EnqueueMDMAppleCommand(ctx, rawB64FreeCmd, c.uuids)
|
||||
checkAuthErr(t, err, c.shoudFailWithAuth)
|
||||
})
|
||||
}
|
||||
|
|
@ -324,7 +324,7 @@ func TestAppleMDMAuthorization(t *testing.T) {
|
|||
<string>uuid</string>
|
||||
</dict>
|
||||
</plist>`, "DeviceLock")))
|
||||
_, _, err = svc.EnqueueMDMAppleCommand(ctx, rawB64PremiumCmd, []string{"host1"}, false)
|
||||
_, _, err = svc.EnqueueMDMAppleCommand(ctx, rawB64PremiumCmd, []string{"host1"})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fleet.ErrMissingLicense.Error())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,17 +3,12 @@ package service
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
mathrand "math/rand"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -29,6 +24,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
||||
|
|
@ -40,7 +36,6 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/service/schedule"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/google/uuid"
|
||||
"github.com/groob/plist"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
|
@ -52,9 +47,6 @@ import (
|
|||
"github.com/micromdm/nanomdm/mdm"
|
||||
"github.com/micromdm/nanomdm/push"
|
||||
nanomdm_pushsvc "github.com/micromdm/nanomdm/push/service"
|
||||
scepclient "github.com/micromdm/scep/v2/client"
|
||||
"github.com/micromdm/scep/v2/cryptoutil/x509util"
|
||||
"github.com/micromdm/scep/v2/scep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
|
@ -160,6 +152,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
}
|
||||
},
|
||||
},
|
||||
APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589",
|
||||
}
|
||||
users, server := RunServerForTestsWithDS(s.T(), s.ds, &config)
|
||||
s.server = server
|
||||
|
|
@ -180,6 +173,12 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
}))
|
||||
s.T().Setenv("TEST_FLEETDM_API_URL", fleetdmSrv.URL)
|
||||
|
||||
appConf, err = s.ds.AppConfig(context.Background())
|
||||
require.NoError(s.T(), err)
|
||||
appConf.ServerSettings.ServerURL = server.URL
|
||||
err = s.ds.SaveAppConfig(context.Background(), appConf)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
s.T().Cleanup(fleetdmSrv.Close)
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +250,7 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
|
|||
require.NoError(t, getAppleBMResp.Err)
|
||||
require.Equal(t, "abc", getAppleBMResp.AppleID)
|
||||
require.Equal(t, "test_org", getAppleBMResp.OrgName)
|
||||
require.Equal(t, "https://example.org/mdm/apple/mdm", getAppleBMResp.MDMServerURL)
|
||||
require.Equal(t, s.server.URL+"/mdm/apple/mdm", getAppleBMResp.MDMServerURL)
|
||||
require.Empty(t, getAppleBMResp.DefaultTeam)
|
||||
|
||||
// create a new team
|
||||
|
|
@ -274,7 +273,7 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
|
|||
require.NoError(t, getAppleBMResp.Err)
|
||||
require.Equal(t, "abc", getAppleBMResp.AppleID)
|
||||
require.Equal(t, "test_org", getAppleBMResp.OrgName)
|
||||
require.Equal(t, "https://example.org/mdm/apple/mdm", getAppleBMResp.MDMServerURL)
|
||||
require.Equal(t, s.server.URL+"/mdm/apple/mdm", getAppleBMResp.MDMServerURL)
|
||||
require.Equal(t, tm.Name, getAppleBMResp.DefaultTeam)
|
||||
}
|
||||
|
||||
|
|
@ -304,7 +303,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
var fleetdProfile bytes.Buffer
|
||||
params := mobileconfig.FleetdProfileOptions{
|
||||
EnrollSecret: t.Name(),
|
||||
ServerURL: "https://example.org",
|
||||
ServerURL: s.server.URL,
|
||||
PayloadType: mobileconfig.FleetdConfigPayloadIdentifier,
|
||||
}
|
||||
err = mobileconfig.FleetdProfileTemplate.Execute(&fleetdProfile, params)
|
||||
|
|
@ -332,6 +331,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
|
||||
// create a non-macOS host
|
||||
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 1,
|
||||
OsqueryHostID: ptr.String("non-macos-host"),
|
||||
NodeKey: ptr.String("non-macos-host"),
|
||||
UUID: uuid.New().String(),
|
||||
|
|
@ -342,6 +342,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
|
||||
// create a host that's not enrolled into MDM
|
||||
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 2,
|
||||
OsqueryHostID: ptr.String("not-mdm-enrolled"),
|
||||
NodeKey: ptr.String("not-mdm-enrolled"),
|
||||
UUID: uuid.New().String(),
|
||||
|
|
@ -350,26 +351,14 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create and enroll a host in MDM
|
||||
d := newDevice(s)
|
||||
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now().Add(-1 * time.Minute),
|
||||
OsqueryHostID: ptr.String(t.Name()),
|
||||
NodeKey: ptr.String(t.Name()),
|
||||
UUID: d.uuid,
|
||||
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
|
||||
Platform: "darwin",
|
||||
HardwareSerial: d.serial,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
d.mdmEnroll(s)
|
||||
// Create a host and then enroll to MDM.
|
||||
host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
triggerSchedule := func() {
|
||||
ch := make(chan bool)
|
||||
s.onScheduleDone = func() { close(ch) }
|
||||
ch := make(chan struct{})
|
||||
s.onScheduleDone = func() {
|
||||
close(ch)
|
||||
}
|
||||
_, err := s.profileSchedule.Trigger()
|
||||
require.NoError(t, err)
|
||||
<-ch
|
||||
|
|
@ -377,7 +366,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
|
||||
s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
|
||||
require.Len(t, pushes, 1)
|
||||
require.Equal(t, pushes[0].PushMagic, "pushmagic"+d.serial)
|
||||
require.Equal(t, pushes[0].PushMagic, "pushmagic"+mdmDevice.SerialNumber)
|
||||
res := map[string]*push.Response{
|
||||
pushes[0].Token.String(): {
|
||||
Id: uuid.New().String(),
|
||||
|
|
@ -396,10 +385,11 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
// on the first run, cmd will be nil and we need to
|
||||
// ping the server via idle
|
||||
if cmd == nil {
|
||||
cmd = d.idle()
|
||||
cmd, err = mdmDevice.Idle()
|
||||
} else {
|
||||
cmd = d.acknowledge(cmd.CommandUUID)
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// if after idle or acknowledge cmd is still nil, it
|
||||
// means there aren't any commands left to run
|
||||
|
|
@ -422,7 +412,10 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
// trigger a profile sync
|
||||
triggerSchedule()
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
installs, removes := checkNextPayloads()
|
||||
|
||||
// verify that we received all profiles
|
||||
require.ElementsMatch(t, wantGlobalProfiles, installs)
|
||||
require.Empty(t, removes)
|
||||
|
|
@ -483,6 +476,33 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
|
|||
require.Equal(t, uint(0), noTeamSummaryResp.Verifying)
|
||||
}
|
||||
|
||||
func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestMDMClient) {
|
||||
desktopToken := uuid.New().String()
|
||||
mdmDevice := mdmtest.NewTestMDMClientDesktopManual(fleetServerURL, desktopToken)
|
||||
fleetHost, err := ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now().Add(-1 * time.Minute),
|
||||
OsqueryHostID: ptr.String(t.Name() + uuid.New().String()),
|
||||
NodeKey: ptr.String(t.Name() + uuid.New().String()),
|
||||
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
|
||||
Platform: "darwin",
|
||||
|
||||
UUID: mdmDevice.UUID,
|
||||
HardwareSerial: mdmDevice.SerialNumber,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateDeviceAuthToken(context.Background(), fleetHost.ID, desktopToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
return fleetHost, mdmDevice
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
|
||||
t := s.T()
|
||||
devices := []godep.Device{
|
||||
|
|
@ -553,25 +573,29 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
|
|||
s.DoJSON("GET", "/api/latest/fleet/hosts?mdm_enrollment_status=pending", nil, http.StatusOK, &listHostsRes)
|
||||
require.Len(t, listHostsRes.Hosts, 2)
|
||||
|
||||
d := newDevice(s)
|
||||
s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
|
||||
return map[string]*push.Response{}, nil
|
||||
}
|
||||
|
||||
// enroll one of the hosts
|
||||
d.serial = devices[0].SerialNumber
|
||||
d.mdmEnroll(s)
|
||||
// Enroll one of the hosts
|
||||
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
|
||||
mdmDevice := mdmtest.NewTestMDMClientDEP(s.server.URL, depURLToken)
|
||||
mdmDevice.SerialNumber = devices[0].SerialNumber
|
||||
err = mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
// make sure the host gets a request to install fleetd
|
||||
var fleetdCmd *micromdm.CommandPayload
|
||||
cmd := d.idle()
|
||||
cmd, err := mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
if cmd.Command.RequestType == "InstallEnterpriseApplication" &&
|
||||
cmd.Command.InstallEnterpriseApplication.ManifestURL != nil &&
|
||||
strings.Contains(*cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) {
|
||||
fleetdCmd = cmd
|
||||
}
|
||||
cmd = d.acknowledge(cmd.CommandUUID)
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NotNil(t, fleetdCmd)
|
||||
require.NotNil(t, fleetdCmd.Command)
|
||||
|
|
@ -603,6 +627,15 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
|
|||
require.True(t, found)
|
||||
}
|
||||
|
||||
func loadEnrollmentProfileDEPToken(t *testing.T, ds *mysql.Datastore) string {
|
||||
var token string
|
||||
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(context.Background(), q, &token,
|
||||
`SELECT token FROM mdm_apple_enrollment_profiles`)
|
||||
})
|
||||
return token
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestDeviceMDMManualEnroll() {
|
||||
t := s.T()
|
||||
|
||||
|
|
@ -620,8 +653,17 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
t := s.T()
|
||||
|
||||
// Enroll two devices into MDM
|
||||
deviceA := newMDMEnrolledDevice(s)
|
||||
deviceB := newMDMEnrolledDevice(s)
|
||||
mdmEnrollInfo := mdmtest.EnrollInfo{
|
||||
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
||||
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
||||
}
|
||||
mdmDeviceA := mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)
|
||||
err := mdmDeviceA.Enroll()
|
||||
require.NoError(t, err)
|
||||
mdmDeviceB := mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)
|
||||
err = mdmDeviceB.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find the ID of Fleet's MDM solution
|
||||
var mdmID uint
|
||||
|
|
@ -636,14 +678,14 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
require.Len(t, listHostsRes.Hosts, 2)
|
||||
require.EqualValues(
|
||||
t,
|
||||
[]string{deviceA.uuid, deviceB.uuid},
|
||||
[]string{mdmDeviceA.UUID, mdmDeviceB.UUID},
|
||||
[]string{listHostsRes.Hosts[0].UUID, listHostsRes.Hosts[1].UUID},
|
||||
)
|
||||
|
||||
var targetHostID uint
|
||||
var lastEnroll time.Time
|
||||
for _, host := range listHostsRes.Hosts {
|
||||
if host.UUID == deviceA.uuid {
|
||||
if host.UUID == mdmDeviceA.UUID {
|
||||
targetHostID = host.ID
|
||||
lastEnroll = host.LastEnrolledAt
|
||||
break
|
||||
|
|
@ -664,8 +706,8 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
}
|
||||
}
|
||||
require.Len(t, details, 2)
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, deviceA.serial, deviceA.model, deviceA.serial), string(*details[len(details)-2]))
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, deviceB.serial, deviceB.model, deviceB.serial), string(*details[len(details)-1]))
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*details[len(details)-2]))
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), string(*details[len(details)-1]))
|
||||
|
||||
// set an enroll secret
|
||||
var applyResp applyEnrollSecretSpecResponse
|
||||
|
|
@ -678,7 +720,7 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
// simulate a matching host enrolling via osquery
|
||||
j, err := json.Marshal(&enrollAgentRequest{
|
||||
EnrollSecret: t.Name(),
|
||||
HostIdentifier: deviceA.uuid,
|
||||
HostIdentifier: mdmDeviceA.UUID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
var enrollResp enrollAgentResponse
|
||||
|
|
@ -699,7 +741,8 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
require.Greater(t, getHostResp.Host.LastEnrolledAt, lastEnroll)
|
||||
|
||||
// Unenroll a device
|
||||
deviceA.checkout()
|
||||
err = mdmDeviceA.Checkout()
|
||||
require.NoError(t, err)
|
||||
|
||||
// An activity is created
|
||||
activities = listActivitiesResponse{}
|
||||
|
|
@ -712,21 +755,30 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|||
require.Nil(t, activity.ActorID)
|
||||
require.Nil(t, activity.ActorFullName)
|
||||
details = append(details, activity.Details)
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, deviceA.serial, deviceA.model, deviceA.serial), string(*activity.Details))
|
||||
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*activity.Details))
|
||||
}
|
||||
}
|
||||
require.True(t, found)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestDeviceMultipleAuthMessages() {
|
||||
d := newMDMEnrolledDevice(s)
|
||||
t := s.T()
|
||||
|
||||
mdmDevice := mdmtest.NewTestMDMClientDirect(mdmtest.EnrollInfo{
|
||||
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
||||
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
||||
})
|
||||
err := mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
listHostsRes := listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
||||
require.Len(s.T(), listHostsRes.Hosts, 1)
|
||||
|
||||
// send the auth message again, we still have only one host
|
||||
d.authenticate()
|
||||
err = mdmDevice.Authenticate()
|
||||
require.NoError(t, err)
|
||||
listHostsRes = listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
||||
require.Len(s.T(), listHostsRes.Hosts, 1)
|
||||
|
|
@ -777,8 +829,15 @@ func (s *integrationMDMTestSuite) TestAppleMDMCSRRequest() {
|
|||
|
||||
func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
|
||||
t := s.T()
|
||||
// enroll into mdm
|
||||
d := newMDMEnrolledDevice(s)
|
||||
|
||||
// Enroll a device into MDM.
|
||||
mdmDevice := mdmtest.NewTestMDMClientDirect(mdmtest.EnrollInfo{
|
||||
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
||||
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
||||
})
|
||||
err := mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
// set an enroll secret
|
||||
var applyResp applyEnrollSecretSpecResponse
|
||||
|
|
@ -791,7 +850,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
|
|||
// simulate a matching host enrolling via osquery
|
||||
j, err := json.Marshal(&enrollAgentRequest{
|
||||
EnrollSecret: t.Name(),
|
||||
HostIdentifier: d.uuid,
|
||||
HostIdentifier: mdmDevice.UUID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
var enrollResp enrollAgentResponse
|
||||
|
|
@ -852,13 +911,16 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
|
|||
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusInternalServerError)
|
||||
|
||||
// try again, but this time the host is online and answers
|
||||
var checkoutErr error
|
||||
s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
|
||||
res, err := mockSuccessfulPush(pushes)
|
||||
d.checkout()
|
||||
checkoutErr = mdmDevice.Checkout()
|
||||
return res, err
|
||||
}
|
||||
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusOK)
|
||||
|
||||
require.NoError(t, checkoutErr)
|
||||
|
||||
// profiles are removed and the host is no longer enrolled
|
||||
hostResp = getHostResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp)
|
||||
|
|
@ -1984,14 +2046,18 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() {
|
|||
// always receive the "no team" profiles on mdm enrollment since it would
|
||||
// not be part of any team yet (team assignment is done when it enrolls
|
||||
// with orbit).
|
||||
d := newDevice(s)
|
||||
mdmDevice := mdmtest.NewTestMDMClientDirect(mdmtest.EnrollInfo{
|
||||
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
||||
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
||||
})
|
||||
|
||||
// enroll the device with orbit
|
||||
var resp EnrollOrbitResponse
|
||||
s.DoJSON("POST", "/api/fleet/orbit/enroll", EnrollOrbitRequest{
|
||||
EnrollSecret: secret,
|
||||
HardwareUUID: d.uuid, // will not match any existing host
|
||||
HardwareSerial: d.serial,
|
||||
HardwareUUID: mdmDevice.UUID, // will not match any existing host
|
||||
HardwareSerial: mdmDevice.SerialNumber,
|
||||
}, http.StatusOK, &resp)
|
||||
require.NotEmpty(t, resp.OrbitNodeKey)
|
||||
orbitNodeKey := resp.OrbitNodeKey
|
||||
|
|
@ -1999,7 +2065,8 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() {
|
|||
require.NoError(t, err)
|
||||
h.OrbitNodeKey = &orbitNodeKey
|
||||
|
||||
d.mdmEnroll(s)
|
||||
err = mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
@ -2467,8 +2534,17 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
|
|||
ctx := context.Background()
|
||||
t := s.T()
|
||||
|
||||
// Create host enrolled via osquery, but not enrolled in MDM.
|
||||
unenrolledHost := createHostAndDeviceToken(t, s.ds, "unused")
|
||||
enrolledHost := newMDMEnrolledDevice(s)
|
||||
|
||||
// Create device enrolled in MDM but not enrolled via osquery.
|
||||
mdmDevice := mdmtest.NewTestMDMClientDirect(mdmtest.EnrollInfo{
|
||||
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
||||
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
||||
})
|
||||
err := mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
base64Cmd := func(rawCmd string) string {
|
||||
return base64.RawStdEncoding.EncodeToString([]byte(rawCmd))
|
||||
|
|
@ -2522,7 +2598,7 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
|
|||
res = s.Do("POST", "/api/latest/fleet/mdm/apple/enqueue",
|
||||
enqueueMDMAppleCommandRequest{
|
||||
Command: base64Cmd(string(mobileconfigForTest("test config profile", uuid.New().String()))),
|
||||
DeviceIDs: []string{enrolledHost.uuid},
|
||||
DeviceIDs: []string{mdmDevice.UUID},
|
||||
}, http.StatusUnsupportedMediaType)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "unable to decode plist command")
|
||||
|
|
@ -2534,7 +2610,7 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
|
|||
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/enqueue",
|
||||
enqueueMDMAppleCommandRequest{
|
||||
Command: base64Cmd(rawCmd),
|
||||
DeviceIDs: []string{enrolledHost.uuid},
|
||||
DeviceIDs: []string{mdmDevice.UUID},
|
||||
}, http.StatusOK, &resp)
|
||||
require.NotEmpty(t, resp.CommandUUID)
|
||||
require.Contains(t, rawCmd, resp.CommandUUID)
|
||||
|
|
@ -2546,8 +2622,8 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
|
|||
require.Len(t, cmdResResp.Results, 0)
|
||||
|
||||
// simulate a result and call again
|
||||
err := s.mdmStorage.StoreCommandReport(&mdm.Request{
|
||||
EnrollID: &mdm.EnrollID{ID: enrolledHost.uuid},
|
||||
err = s.mdmStorage.StoreCommandReport(&mdm.Request{
|
||||
EnrollID: &mdm.EnrollID{ID: mdmDevice.UUID},
|
||||
Context: ctx,
|
||||
}, &mdm.CommandResults{
|
||||
CommandUUID: uuid2,
|
||||
|
|
@ -2557,7 +2633,7 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err := s.ds.HostByIdentifier(ctx, enrolledHost.uuid)
|
||||
h, err := s.ds.HostByIdentifier(ctx, mdmDevice.UUID)
|
||||
require.NoError(t, err)
|
||||
h.Hostname = "test-host"
|
||||
err = s.ds.UpdateHost(ctx, h)
|
||||
|
|
@ -2568,7 +2644,7 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
|
|||
require.NotZero(t, cmdResResp.Results[0].UpdatedAt)
|
||||
cmdResResp.Results[0].UpdatedAt = time.Time{}
|
||||
require.Equal(t, &fleet.MDMAppleCommandResult{
|
||||
DeviceID: enrolledHost.uuid,
|
||||
DeviceID: mdmDevice.UUID,
|
||||
CommandUUID: uuid2,
|
||||
Status: "Acknowledged",
|
||||
RequestType: "ProfileList",
|
||||
|
|
@ -2582,7 +2658,7 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
|
|||
require.NotZero(t, listCmdResp.Results[0].UpdatedAt)
|
||||
listCmdResp.Results[0].UpdatedAt = time.Time{}
|
||||
require.Equal(t, &fleet.MDMAppleCommand{
|
||||
DeviceID: enrolledHost.uuid,
|
||||
DeviceID: mdmDevice.UUID,
|
||||
CommandUUID: uuid2,
|
||||
Status: "Acknowledged",
|
||||
RequestType: "ProfileList",
|
||||
|
|
@ -2700,7 +2776,7 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() {
|
|||
|
||||
type deviceWithResponse struct {
|
||||
bootstrapResponse string
|
||||
device *device
|
||||
device *mdmtest.TestMDMClient
|
||||
}
|
||||
|
||||
// Note: The responses specified here are not a 1:1 mapping of the possible responses specified
|
||||
|
|
@ -2714,42 +2790,47 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() {
|
|||
// - Error means that the device will enroll and fail to install the bp
|
||||
// - Offline means that the device will enroll but won't acknowledge nor fail the bp request
|
||||
// - Pending means that the device won't enroll at all
|
||||
mdmEnrollInfo := mdmtest.EnrollInfo{
|
||||
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
||||
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
||||
}
|
||||
noTeamDevices := []deviceWithResponse{
|
||||
{"Acknowledge", newDevice(s)},
|
||||
{"Acknowledge", newDevice(s)},
|
||||
{"Acknowledge", newDevice(s)},
|
||||
{"Error", newDevice(s)},
|
||||
{"Offline", newDevice(s)},
|
||||
{"Offline", newDevice(s)},
|
||||
{"Pending", newDevice(s)},
|
||||
{"Pending", newDevice(s)},
|
||||
{"Acknowledge", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Acknowledge", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Acknowledge", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Error", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Offline", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Offline", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Pending", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Pending", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
}
|
||||
|
||||
teamDevices := []deviceWithResponse{
|
||||
{"Acknowledge", newDevice(s)},
|
||||
{"Acknowledge", newDevice(s)},
|
||||
{"Error", newDevice(s)},
|
||||
{"Error", newDevice(s)},
|
||||
{"Error", newDevice(s)},
|
||||
{"Offline", newDevice(s)},
|
||||
{"Pending", newDevice(s)},
|
||||
{"Acknowledge", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Acknowledge", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Error", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Error", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Error", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Offline", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
{"Pending", mdmtest.NewTestMDMClientDirect(mdmEnrollInfo)},
|
||||
}
|
||||
|
||||
expectedSerialsByTeamAndStatus := make(map[uint]map[fleet.MDMBootstrapPackageStatus][]string)
|
||||
expectedSerialsByTeamAndStatus[0] = map[fleet.MDMBootstrapPackageStatus][]string{
|
||||
fleet.MDMBootstrapPackageInstalled: {noTeamDevices[0].device.serial, noTeamDevices[1].device.serial, noTeamDevices[2].device.serial},
|
||||
fleet.MDMBootstrapPackageFailed: {noTeamDevices[3].device.serial},
|
||||
fleet.MDMBootstrapPackagePending: {noTeamDevices[4].device.serial, noTeamDevices[5].device.serial, noTeamDevices[6].device.serial, noTeamDevices[7].device.serial},
|
||||
fleet.MDMBootstrapPackageInstalled: {noTeamDevices[0].device.SerialNumber, noTeamDevices[1].device.SerialNumber, noTeamDevices[2].device.SerialNumber},
|
||||
fleet.MDMBootstrapPackageFailed: {noTeamDevices[3].device.SerialNumber},
|
||||
fleet.MDMBootstrapPackagePending: {noTeamDevices[4].device.SerialNumber, noTeamDevices[5].device.SerialNumber, noTeamDevices[6].device.SerialNumber, noTeamDevices[7].device.SerialNumber},
|
||||
}
|
||||
expectedSerialsByTeamAndStatus[team.ID] = map[fleet.MDMBootstrapPackageStatus][]string{
|
||||
fleet.MDMBootstrapPackageInstalled: {teamDevices[0].device.serial, teamDevices[1].device.serial},
|
||||
fleet.MDMBootstrapPackageFailed: {teamDevices[2].device.serial, teamDevices[3].device.serial, teamDevices[4].device.serial},
|
||||
fleet.MDMBootstrapPackagePending: {teamDevices[5].device.serial, teamDevices[6].device.serial},
|
||||
fleet.MDMBootstrapPackageInstalled: {teamDevices[0].device.SerialNumber, teamDevices[1].device.SerialNumber},
|
||||
fleet.MDMBootstrapPackageFailed: {teamDevices[2].device.SerialNumber, teamDevices[3].device.SerialNumber, teamDevices[4].device.SerialNumber},
|
||||
fleet.MDMBootstrapPackagePending: {teamDevices[5].device.SerialNumber, teamDevices[6].device.SerialNumber},
|
||||
}
|
||||
|
||||
// for good measure, add a couple of manually enrolled hosts
|
||||
_ = newMDMEnrolledDevice(s)
|
||||
_ = newMDMEnrolledDevice(s)
|
||||
createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
// create a non-macOS host
|
||||
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
|
|
@ -2789,7 +2870,7 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() {
|
|||
case "/devices/sync":
|
||||
depResp := []godep.Device{}
|
||||
for _, gd := range mockRespDevices {
|
||||
depResp = append(depResp, godep.Device{SerialNumber: gd.device.serial})
|
||||
depResp = append(depResp, godep.Device{SerialNumber: gd.device.SerialNumber})
|
||||
}
|
||||
err := encoder.Encode(godep.DeviceResponse{Devices: depResp})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -2834,24 +2915,28 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() {
|
|||
|
||||
// devices send their responses
|
||||
enrollAndCheckBootstrapPackage := func(d *deviceWithResponse, bp *fleet.MDMAppleBootstrapPackage) {
|
||||
d.device.mdmEnroll(s)
|
||||
cmd := d.device.idle()
|
||||
err := d.device.Enroll()
|
||||
require.NoError(t, err)
|
||||
cmd, err := d.device.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
// if the command is to install the bootstrap package
|
||||
if manifest := cmd.Command.InstallEnterpriseApplication.Manifest; manifest != nil {
|
||||
require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
|
||||
require.Equal(t, "software-package", (*manifest).ManifestItems[0].Assets[0].Kind)
|
||||
wantURL, err := bp.URL("https://example.org")
|
||||
wantURL, err := bp.URL(s.server.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wantURL, (*manifest).ManifestItems[0].Assets[0].URL)
|
||||
|
||||
// respond to the command accordingly
|
||||
switch d.bootstrapResponse {
|
||||
case "Acknowledge":
|
||||
cmd = d.device.acknowledge(cmd.CommandUUID)
|
||||
cmd, err = d.device.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
continue
|
||||
case "Error":
|
||||
cmd = d.device.err(cmd.CommandUUID, mockErrorChain)
|
||||
cmd, err = d.device.Err(cmd.CommandUUID, mockErrorChain)
|
||||
require.NoError(t, err)
|
||||
continue
|
||||
case "Offline":
|
||||
// host is offline, can't process any more commands
|
||||
|
|
@ -2859,7 +2944,8 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() {
|
|||
continue
|
||||
}
|
||||
}
|
||||
cmd = d.device.acknowledge(cmd.CommandUUID)
|
||||
cmd, err = d.device.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3639,240 +3725,6 @@ func (s *integrationMDMTestSuite) uploadEULA(
|
|||
}
|
||||
}
|
||||
|
||||
type device struct {
|
||||
uuid string
|
||||
serial string
|
||||
model string
|
||||
|
||||
s *integrationMDMTestSuite
|
||||
scepCert *x509.Certificate
|
||||
scepKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func newDevice(s *integrationMDMTestSuite) *device {
|
||||
return &device{
|
||||
uuid: strings.ToUpper(uuid.New().String()),
|
||||
serial: randSerial(),
|
||||
model: "MacBookPro16,1",
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
|
||||
func newMDMEnrolledDevice(s *integrationMDMTestSuite) *device {
|
||||
d := newDevice(s)
|
||||
d.mdmEnroll(s)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *device) mdmEnroll(s *integrationMDMTestSuite) {
|
||||
d.scepEnroll()
|
||||
d.authenticate()
|
||||
d.tokenUpdate()
|
||||
}
|
||||
|
||||
func (d *device) authenticate() {
|
||||
payload := map[string]any{
|
||||
"MessageType": "Authenticate",
|
||||
"UDID": d.uuid,
|
||||
"Model": d.model,
|
||||
"DeviceName": "testdevice" + d.serial,
|
||||
"Topic": "com.apple.mgmt.External." + d.uuid,
|
||||
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
||||
"SerialNumber": d.serial,
|
||||
}
|
||||
d.request("application/x-apple-aspen-mdm-checkin", payload)
|
||||
}
|
||||
|
||||
func (d *device) tokenUpdate() {
|
||||
payload := map[string]any{
|
||||
"MessageType": "TokenUpdate",
|
||||
"UDID": d.uuid,
|
||||
"Topic": "com.apple.mgmt.External." + d.uuid,
|
||||
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
||||
"NotOnConsole": "false",
|
||||
"PushMagic": "pushmagic" + d.serial,
|
||||
"Token": []byte("token" + d.serial),
|
||||
}
|
||||
d.request("application/x-apple-aspen-mdm-checkin", payload)
|
||||
}
|
||||
|
||||
func (d *device) checkout() {
|
||||
payload := map[string]any{
|
||||
"MessageType": "CheckOut",
|
||||
"Topic": "com.apple.mgmt.External." + d.uuid,
|
||||
"UDID": d.uuid,
|
||||
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
||||
}
|
||||
d.request("application/x-apple-aspen-mdm-checkin", payload)
|
||||
}
|
||||
|
||||
// Devices send an Idle status to signal the server that they're ready to
|
||||
// receive commands.
|
||||
// The server can signal back with either a command to run
|
||||
// or an empty response body to end the communication.
|
||||
func (d *device) idle() *micromdm.CommandPayload {
|
||||
payload := map[string]any{
|
||||
"Status": "Idle",
|
||||
"Topic": "com.apple.mgmt.External." + d.uuid,
|
||||
"UDID": d.uuid,
|
||||
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
||||
}
|
||||
return d.sendAndDecodeCommandResponse(payload)
|
||||
}
|
||||
|
||||
func (d *device) acknowledge(cmdUUID string) *micromdm.CommandPayload {
|
||||
payload := map[string]any{
|
||||
"Status": "Acknowledged",
|
||||
"Topic": "com.apple.mgmt.External." + d.uuid,
|
||||
"UDID": d.uuid,
|
||||
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
||||
"CommandUUID": cmdUUID,
|
||||
}
|
||||
return d.sendAndDecodeCommandResponse(payload)
|
||||
}
|
||||
|
||||
func (d *device) err(cmdUUID string, errChain []mdm.ErrorChain) *micromdm.CommandPayload {
|
||||
payload := map[string]any{
|
||||
"Status": "Error",
|
||||
"Topic": "com.apple.mgmt.External." + d.uuid,
|
||||
"UDID": d.uuid,
|
||||
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
||||
"CommandUUID": cmdUUID,
|
||||
"ErrorChain": errChain,
|
||||
}
|
||||
return d.sendAndDecodeCommandResponse(payload)
|
||||
}
|
||||
|
||||
func (d *device) sendAndDecodeCommandResponse(payload map[string]any) *micromdm.CommandPayload {
|
||||
res := d.request("", payload)
|
||||
if res.ContentLength == 0 {
|
||||
return nil
|
||||
}
|
||||
raw, err := io.ReadAll(res.Body)
|
||||
require.NoError(d.s.T(), err)
|
||||
cmd, err := mdm.DecodeCommand(raw)
|
||||
require.NoError(d.s.T(), err)
|
||||
|
||||
var p micromdm.CommandPayload
|
||||
err = plist.Unmarshal(cmd.Raw, &p)
|
||||
require.NoError(d.s.T(), err)
|
||||
return &p
|
||||
}
|
||||
|
||||
func (d *device) request(reqType string, payload map[string]any) *http.Response {
|
||||
body, err := plist.Marshal(payload)
|
||||
require.NoError(d.s.T(), err)
|
||||
|
||||
signedData, err := pkcs7.NewSignedData(body)
|
||||
require.NoError(d.s.T(), err)
|
||||
err = signedData.AddSigner(d.scepCert, d.scepKey, pkcs7.SignerInfoConfig{})
|
||||
require.NoError(d.s.T(), err)
|
||||
sig, err := signedData.Finish()
|
||||
require.NoError(d.s.T(), err)
|
||||
|
||||
return d.s.DoRawWithHeaders(
|
||||
"POST",
|
||||
"/mdm/apple/mdm",
|
||||
body,
|
||||
200,
|
||||
map[string]string{
|
||||
"Content-Type": reqType,
|
||||
"Mdm-Signature": base64.StdEncoding.EncodeToString(sig),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (d *device) scepEnroll() {
|
||||
t := d.s.T()
|
||||
ctx := context.Background()
|
||||
logger := kitlog.NewJSONLogger(os.Stdout)
|
||||
logger = level.NewFilter(logger, level.AllowDebug())
|
||||
client, err := scepclient.New(d.s.server.URL+apple_mdm.SCEPPath, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, _, err := client.GetCACert(ctx, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
certs, err := x509.ParseCertificates(resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
csrTemplate := x509util.CertificateRequest{
|
||||
CertificateRequest: x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "fleet-test",
|
||||
},
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
},
|
||||
ChallengePassword: d.s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
}
|
||||
csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, key)
|
||||
require.NoError(t, err)
|
||||
csr, err := x509.ParseCertificateRequest(csrDerBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(time.Hour)
|
||||
|
||||
certTemplate := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "SCEP SIGNER",
|
||||
Organization: csr.Subject.Organization,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
certDerBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
|
||||
require.NoError(t, err)
|
||||
cert, err := x509.ParseCertificate(certDerBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
tmpl := &scep.PKIMessage{
|
||||
MessageType: scep.PKCSReq,
|
||||
Recipients: certs,
|
||||
SignerKey: key,
|
||||
SignerCert: cert,
|
||||
CSRReqMessage: &scep.CSRReqMessage{
|
||||
ChallengePassword: d.s.fleetCfg.MDM.AppleSCEPChallenge,
|
||||
},
|
||||
}
|
||||
|
||||
msg, err := scep.NewCSRRequest(csr, tmpl, scep.WithLogger(logger))
|
||||
require.NoError(t, err)
|
||||
|
||||
respBytes, err := client.PKIOperation(ctx, msg.Raw)
|
||||
require.NoError(t, err)
|
||||
|
||||
respMsg, err := scep.ParsePKIMessage(respBytes, scep.WithLogger(logger), scep.WithCACerts(msg.Recipients))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, scep.SUCCESS, respMsg.PKIStatus)
|
||||
|
||||
err = respMsg.DecryptPKIEnvelope(cert, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
d.scepCert = respMsg.CertRepMessage.Certificate
|
||||
d.scepKey = key
|
||||
}
|
||||
|
||||
// numbers plus capital letters without I, L, O for readability
|
||||
const serialLetters = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"
|
||||
|
||||
func randSerial() string {
|
||||
b := make([]byte, 12)
|
||||
for i := range b {
|
||||
//nolint:gosec // not used for crypto, only to generate random serial for testing
|
||||
b[i] = serialLetters[mathrand.Intn(len(serialLetters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var testBMToken = &nanodep_client.OAuth1Tokens{
|
||||
ConsumerKey: "test_consumer",
|
||||
ConsumerSecret: "test_secret",
|
||||
|
|
|
|||
|
|
@ -123,6 +123,11 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
}
|
||||
}
|
||||
|
||||
mdmPushCertTopic := ""
|
||||
if len(opts) > 0 && opts[0].APNSTopic != "" {
|
||||
mdmPushCertTopic = opts[0].APNSTopic
|
||||
}
|
||||
|
||||
svc, err := NewService(
|
||||
ctx,
|
||||
ds,
|
||||
|
|
@ -143,7 +148,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
depStorage,
|
||||
mdmStorage,
|
||||
mdmPusher,
|
||||
"",
|
||||
mdmPushCertTopic,
|
||||
cronSchedulesService,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -262,6 +267,7 @@ type TestServerOpts struct {
|
|||
HTTPServerConfig *http.Server
|
||||
StartCronSchedules []TestNewScheduleFunc
|
||||
UseMailService bool
|
||||
APNSTopic string
|
||||
}
|
||||
|
||||
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue