Add new data types and table for Apple MDM config profiles (#9758)

This commit is contained in:
gillespi314 2023-02-08 18:36:20 -06:00 committed by GitHub
parent 7cd581866a
commit aca2449566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 311 additions and 3 deletions

2
go.mod
View File

@ -49,6 +49,7 @@ require (
github.com/groob/plist v0.0.0-20220217120414-63fa881b19a5
github.com/hashicorp/go-multierror v1.1.1
github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95
github.com/hillu/go-ntdll v0.0.0-20220801201350-0d23f057ef1f
github.com/igm/sockjs-go/v3 v3.0.0
github.com/jinzhu/copier v0.3.5
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5
@ -225,7 +226,6 @@ require (
github.com/hashicorp/go-version v1.2.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hillu/go-ntdll v0.0.0-20220801201350-0d23f057ef1f // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/iancoleman/orderedmap v0.2.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect

View File

@ -0,0 +1,37 @@
package tables
import (
"database/sql"
"github.com/pkg/errors"
)
func init() {
MigrationClient.AddMigration(Up_20230206163608, Down_20230206163608)
}
func Up_20230206163608(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE mdm_apple_configuration_profiles (
profile_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
team_id INT(10) UNSIGNED NOT NULL DEFAULT 0,
-- team_id is zero for configuration profiles that are not associated with any team
identifier VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
mobileconfig BLOB NOT NULL,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (profile_id),
UNIQUE KEY idx_mdm_apple_config_prof_team_identifier (team_id, identifier),
UNIQUE KEY idx_mdm_apple_config_prof_team_name (team_id, name)
);`)
if err != nil {
return errors.Wrapf(err, "create table")
}
return nil
}
func Down_20230206163608(tx *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,50 @@
package tables
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUp_20230206163608(t *testing.T) {
db := applyUpToPrev(t)
applyNext(t, db)
stmt := `
INSERT INTO
mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig)
VALUES (?, ?, ?, ?)`
mcBytes := []byte(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array/>
<key>PayloadDisplayName</key>
<string>TestPayloadName</string>
<key>PayloadIdentifier</key>
<string>TestPayloadIdentifier</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>TestPayloadUUID</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
`)
_, err := db.Exec(stmt, 0, "TestPayloadIdentifier", "TestPayloadName", mcBytes)
require.NoError(t, err)
var (
identifier string
mobileconfig []byte
)
err = db.QueryRow(`SELECT identifier, mobileconfig FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?`, "TestPayloadName", 0).Scan(&identifier, &mobileconfig)
require.NoError(t, err)
require.Equal(t, "TestPayloadIdentifier", identifier)
require.Equal(t, mcBytes, mobileconfig)
}

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,17 @@
package fleet
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/micromdm/nanodep/godep"
"github.com/micromdm/nanomdm/mdm"
"go.mozilla.org/pkcs7"
"howett.net/plist"
)
// MDMAppleEnrollmentType is the type for Apple MDM enrollments.
@ -192,3 +198,84 @@ func (e MDMAppleCommandTimeoutError) Error() string {
func (e MDMAppleCommandTimeoutError) StatusCode() int {
return http.StatusGatewayTimeout
}
// Mobileconfig is the byte slice corresponding to an XML property list (i.e. plist) representation
// of an Apple MDM configuration profile in Fleet.
//
// Configuration profiles are used to configure Apple devices. See also
// https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles.
type Mobileconfig []byte
// ParseConfigProfile attempts to parse the Mobileconfig byte slice as a Fleet MDMAppleConfigProfile.
//
// The byte slice must be XML or PKCS7 parseable. Fleet also requires that it contains both
// a PayloadIdentifier and a PayloadDisplayName and that it has PayloadType set to "Configuration".
//
// Adapted from https://github.com/micromdm/micromdm/blob/main/platform/profile/profile.go
func (mc *Mobileconfig) ParseConfigProfile() (*MDMAppleConfigProfile, error) {
mcBytes := *mc
if !bytes.HasPrefix(mcBytes, []byte("<?xml")) {
p7, err := pkcs7.Parse(mcBytes)
if err != nil {
return nil, fmt.Errorf("mobileconfig is not XML nor PKCS7 parseable: %w", err)
}
err = p7.Verify()
if err != nil {
return nil, err
}
mcBytes = Mobileconfig(p7.Content)
}
var parsed struct {
PayloadIdentifier string
PayloadDisplayName string
PayloadType string
}
_, err := plist.Unmarshal(mcBytes, &parsed)
if err != nil {
return nil, err
}
if parsed.PayloadType != "Configuration" {
return nil, fmt.Errorf("invalid PayloadType: %s", parsed.PayloadType)
}
if parsed.PayloadIdentifier == "" {
return nil, errors.New("empty PayloadIdentifier in profile")
}
if parsed.PayloadDisplayName == "" {
return nil, errors.New("empty PayloadDisplayName in profile")
}
return &MDMAppleConfigProfile{
Identifier: parsed.PayloadIdentifier,
Name: parsed.PayloadDisplayName,
Mobileconfig: mc,
}, nil
}
// MDMAppleConfigProfile represents an Apple MDM configuration profile in Fleet.
// Configuration profiles are used to configure Apple devices .
// See also https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles.
type MDMAppleConfigProfile struct {
// ProfileID is the unique id of the configuration profile in Fleet
ProfileID uint `db:"profile_id"`
// TeamID is the id of the team with which the configuration is associated. A team id of zero
// represents a configuration profile that is not associated with any team.
TeamID uint `db:"team_id"`
// Identifier corresponds to the payload identifier of the associated mobileconfig payload.
// Fleet requires that Identifier must be unique in combination with the Name and TeamID.
Identifier string `db:"identifier"`
// Name corresponds to the payload display name of the associated mobileconfig payload.
// Fleet requires that Name must be unique in combination with the Identifier and TeamID.
Name string `db:"name"`
// Mobileconfig is the byte slice corresponding to the XML property list (i.e. plist)
// representation of the configuration profile. It must be XML or PKCS7 parseable.
Mobileconfig *Mobileconfig `db:"mobileconfig"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (cp *MDMAppleConfigProfile) Validate() error {
// TODO(sarah): Additional validations for PayloadContent (e.g., screening out FileVault payloads)
// should be handled here
return nil
}

View File

@ -0,0 +1,119 @@
package fleet
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"fmt"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"go.mozilla.org/pkcs7"
"github.com/micromdm/scep/v2/depot"
)
func TestMDMAppleConfigProfile(t *testing.T) {
cases := []struct {
testName string
mobileconfig Mobileconfig
shouldFail bool
}{
{
testName: "TestParseConfigProfileOK",
mobileconfig: mobileconfigForTest("ValidName", "ValidIdentifier", uuid.NewString()),
shouldFail: false,
},
{
testName: "TestParseConfigProfileNoIdentifier",
mobileconfig: mobileconfigForTest("ValidName", "", uuid.NewString()),
shouldFail: true,
},
{
testName: "TestParseConfigProfileNoName",
mobileconfig: mobileconfigForTest("", "ValidIdentifier", uuid.NewString()),
shouldFail: true,
},
{
testName: "TestParseConfigProfileNoNameNoIdentifier",
mobileconfig: mobileconfigForTest("", "", uuid.NewString()),
shouldFail: true,
},
{
testName: "TestParseConfigProfileInvalidEncoding",
mobileconfig: func() []byte {
b, err := json.Marshal(MDMAppleConfigProfile{Name: "ValidName", Identifier: "ValidIdentifier"})
require.NoError(t, err)
return b
}(),
shouldFail: true,
},
{
testName: "TestParseConfigProfilePKCS7Encoding",
mobileconfig: func() []byte {
// generate certificate for signed data test
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
crtBytes, err := depot.NewCACert().SelfSign(rand.Reader, key.Public(), key)
require.NoError(t, err)
crt, err := x509.ParseCertificate(crtBytes)
require.NoError(t, err)
// encode mobileconfig as PKCS7 signed data
signedData, err := pkcs7.NewSignedData(mobileconfigForTest("ValidName", "ValidIdentifier", uuid.NewString()))
require.NoError(t, err)
err = signedData.AddSigner(crt, key, pkcs7.SignerInfoConfig{})
require.NoError(t, err)
signedBytes, err := signedData.Finish()
require.NoError(t, err)
p7, err := pkcs7.Parse(signedBytes)
require.NoError(t, err)
require.NoError(t, p7.Verify())
return signedBytes
}(),
shouldFail: false,
},
}
for _, c := range cases {
t.Run(c.testName, func(t *testing.T) {
mc := c.mobileconfig
cp := new(MDMAppleConfigProfile)
cp.Mobileconfig = &mc
parsed, err := cp.Mobileconfig.ParseConfigProfile()
if c.shouldFail {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, "ValidName", parsed.Name)
require.Equal(t, "ValidIdentifier", parsed.Identifier)
}
})
}
}
func mobileconfigForTest(name string, identifier string, uuid string) Mobileconfig {
return []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array/>
<key>PayloadDisplayName</key>
<string>%s</string>
<key>PayloadIdentifier</key>
<string>%s</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>%s</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
`, name, identifier, uuid))
}