From ab583d66e6063d68d3cdba9506801f6b831e0cbc Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Wed, 5 Apr 2023 10:35:38 -0300 Subject: [PATCH] Add a tool to generate manifests for Apple MDM (#10959) Related to #9459 this will allow us to host a `fleetd` metadata alongside the installer that can be used by Apple's MDM `InstallEnterpriseApplication` --- server/mdm/apple/appmanifest/appmanifest.go | 39 ++++++++++++++ .../mdm/apple/appmanifest/appmanifest_test.go | 52 +++++++++++++++++++ tools/mdm/apple/appmanifest/README.md | 36 +++++++++++++ tools/mdm/apple/appmanifest/main.go | 43 +++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 server/mdm/apple/appmanifest/appmanifest.go create mode 100644 server/mdm/apple/appmanifest/appmanifest_test.go create mode 100644 tools/mdm/apple/appmanifest/README.md create mode 100644 tools/mdm/apple/appmanifest/main.go diff --git a/server/mdm/apple/appmanifest/appmanifest.go b/server/mdm/apple/appmanifest/appmanifest.go new file mode 100644 index 0000000000..4401a9b6f9 --- /dev/null +++ b/server/mdm/apple/appmanifest/appmanifest.go @@ -0,0 +1,39 @@ +// package appmanifest provides utilities for managing app manifest files +// used by MDM InstallApplication commands. +// +// It's heavily based on the micromdm/mdm/appmanifest package but it uses +// SHA256 as the hashing algorithm instead of MD5. +package appmanifest + +import ( + "crypto/sha256" + "fmt" + "io" + + "github.com/micromdm/micromdm/mdm/appmanifest" +) + +// Create builds an AppManifest using SHA256 checksums and the provided URL +func Create(file io.Reader, url string) (*appmanifest.Manifest, error) { + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return nil, err + } + + sum := fmt.Sprintf("%x", hash.Sum(nil)) + + ast := appmanifest.Asset{ + Kind: "software-package", + SHA256Size: int64(hash.Size()), + SHA256s: []string{sum}, + URL: url, + } + + return &appmanifest.Manifest{ + ManifestItems: []appmanifest.Item{ + { + Assets: []appmanifest.Asset{ast}, + }, + }, + }, nil +} diff --git a/server/mdm/apple/appmanifest/appmanifest_test.go b/server/mdm/apple/appmanifest/appmanifest_test.go new file mode 100644 index 0000000000..a1e013f175 --- /dev/null +++ b/server/mdm/apple/appmanifest/appmanifest_test.go @@ -0,0 +1,52 @@ +package appmanifest + +import ( + "errors" + "io" + "strings" + "testing" + + "github.com/micromdm/micromdm/mdm/appmanifest" + "github.com/stretchr/testify/require" +) + +var errTest = errors.New("test error") + +type alwaysFailReader struct{} + +func (alwaysFailReader) Read(p []byte) (n int, err error) { + return 0, errTest +} + +func TestCreate(t *testing.T) { + url := "https://test.example.com" + + cases := []struct { + in io.Reader + out *appmanifest.Manifest + err error + }{ + { + in: strings.NewReader("foo"), + out: &appmanifest.Manifest{ + ManifestItems: []appmanifest.Item{{Assets: []appmanifest.Asset{{ + Kind: "software-package", + SHA256Size: 32, + SHA256s: []string{"2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + URL: "https://test.example.com", + }}}}}, + err: nil, + }, + { + in: alwaysFailReader{}, + out: nil, + err: errTest, + }, + } + + for _, c := range cases { + m, err := Create(c.in, url) + require.Equal(t, c.out, m) + require.Equal(t, c.err, err) + } +} diff --git a/tools/mdm/apple/appmanifest/README.md b/tools/mdm/apple/appmanifest/README.md new file mode 100644 index 0000000000..6c5a45e550 --- /dev/null +++ b/tools/mdm/apple/appmanifest/README.md @@ -0,0 +1,36 @@ +## appmanifest + +`appmanifest` is a tool that outputs to stdout a valid XML manifest that can be used by the MDM `InstallEnterpriseApplication` command to install a package. + +``` +$ go run tools/mdm/apple/appmanifest/main.go --help +Usage of appmanifest: + -pkg-file string + Path to a .pkg file + -pkg-url string + URL where the package will be served +``` + +### Example workflow + +1. Create a fleetd installer + +``` +fleetctl package --type=pkg --fleet-desktop +``` + +2. Sign the installer so it can be installed via MDM + +``` +productsign --sign "Developer ID Installer: $DEVID_INFO" fleet-osquery.pkg fleetd-base.pkg +``` + +3. Run `appmanifest` + +``` +$ go run tools/mdm/apple/appmanifest/main.go \ + -pkg-file fleetd-base.pkg \ + -pkg-url $YOUR_URL > fleetd-base-manifest.plist +``` + +4. Upload `fleetd-base.pkg` to `$YOUR_URL` and `fleetd-base-manifest.plist` to a publicly accessible location. diff --git a/tools/mdm/apple/appmanifest/main.go b/tools/mdm/apple/appmanifest/main.go new file mode 100644 index 0000000000..c151a54796 --- /dev/null +++ b/tools/mdm/apple/appmanifest/main.go @@ -0,0 +1,43 @@ +// Command appmanifest takes a .pkg file and outputs an XML manifest for it +package main + +import ( + "bytes" + "flag" + "fmt" + "log" + "os" + + "github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest" + "github.com/groob/plist" +) + +func main() { + pkgFile := flag.String("pkg-file", "", "Path to a .pkg file") + pkgURL := flag.String("pkg-url", "", "URL where the package will be served") + flag.Parse() + + if *pkgFile == "" || *pkgURL == "" { + log.Fatal("both --pkg-file and --pkg-url must be provided") + } + + fp, err := os.Open(*pkgFile) + if err != nil { + log.Fatal(err) + } + defer fp.Close() + + m, err := appmanifest.Create(fp, *pkgURL) + if err != nil { + log.Fatal(err) + } + + var buf bytes.Buffer + enc := plist.NewEncoder(&buf) + enc.Indent(" ") + if err := enc.Encode(m); err != nil { + log.Fatal(err) + } + + fmt.Println(buf.String()) +}