From c30f2806cc4dd5920b3ad314fa206b7d7020fd8d Mon Sep 17 00:00:00 2001 From: Victor Vrantchan Date: Fri, 2 Jun 2017 00:09:13 -0400 Subject: [PATCH] mdmctl: support generating appmanifest (#199) generate an appmanifest. validates and optionally signs packages. For #93 Closes #203 --- cmd/mdmctl/apply.go | 3 + cmd/mdmctl/apply_app.go | 179 ++++++++++++++++++++++++++++++++++++++ cmd/mdmctl/sign.go | 15 ++++ cmd/mdmctl/sign_darwin.go | 38 ++++++++ 4 files changed, 235 insertions(+) create mode 100644 cmd/mdmctl/apply_app.go create mode 100644 cmd/mdmctl/sign.go create mode 100644 cmd/mdmctl/sign_darwin.go diff --git a/cmd/mdmctl/apply.go b/cmd/mdmctl/apply.go index 2f347f71..dead51a0 100644 --- a/cmd/mdmctl/apply.go +++ b/cmd/mdmctl/apply.go @@ -56,6 +56,8 @@ func (cmd *applyCommand) Run(args []string) error { run = cmd.applyDEPProfile case "profiles": run = cmd.applyProfile + case "app": + run = cmd.applyApp default: cmd.Usage() os.Exit(1) @@ -73,6 +75,7 @@ Valid resource types: * profiles * dep-tokens * dep-profiles + * app Examples: # Apply a Blueprint. diff --git a/cmd/mdmctl/apply_app.go b/cmd/mdmctl/apply_app.go new file mode 100644 index 00000000..b4aec7e8 --- /dev/null +++ b/cmd/mdmctl/apply_app.go @@ -0,0 +1,179 @@ +package main + +import ( + "bytes" + "compress/zlib" + "encoding/binary" + "flag" + "fmt" + "io/ioutil" + "log" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/groob/plist" + "github.com/micromdm/micromdm/appmanifest" + "github.com/pkg/errors" +) + +func (cmd *applyCommand) applyApp(args []string) error { + flagset := flag.NewFlagSet("app", flag.ExitOnError) + var ( + flPkgPath = flagset.String("pkg", "", "path to a distribution pkg.") + flPkgURL = flagset.String("pkg-url", "", "use custom pkg url") + flAppManifest = flagset.String("manifest", "-", `path to an app manifest. optional, + will be created if file does not exist.`) + + flHashSize = flagset.Int64("md5size", appmanifest.DefaultMD5Size, "md5 hash size in bytes (optional)") + flSign = flagset.String("sign", "", "sign package before importing, requires specifying a product ID (optional)") + ) + flagset.Usage = usageFor(flagset, "mdmctl apply app [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + + pkgurl := *flPkgURL + if pkgurl == "" { + su, err := cmd.serverRepoURL() + if err != nil { + return err + } + pkgurl = pkgURL(su, *flPkgPath) + } + + pkg := *flPkgPath + signed, err := checkSignature(pkg) + if err != nil { + return err + } + + distribution, err := checkDistribution(*flPkgPath) + if err != nil { + return err + } + if !distribution { + fmt.Println(` +[WARNING] The package you're importing is not a macOS distribution package. MDM requires distribution packages. +You can turn a flat package to a distribution one with the productbuild command: + +productbuild --package someFlatPkg.pkg myNewPkg.pkg + +Please rebuild the package and re-run the command. + + `) + } + + if !signed { + if *flSign == "" { + flagset.Usage() + return errors.New(`MDM packages must be signed. Provide signed package or Developer ID with -sign flag`) + } + outpath := filepath.Join(os.TempDir(), filepath.Base(*flPkgPath)) + if err := signPackage(*flPkgPath, outpath, *flSign); err != nil { + return err + } + pkg = outpath // use signed package to create the manifest + } + + // open pkg file + f, err := os.Open(pkg) + if err != nil { + return err + } + defer f.Close() + + opts := []appmanifest.Option{appmanifest.WithMD5Size(*flHashSize)} + manifest, err := appmanifest.Create(&appFile{f}, pkgurl, opts...) + if err != nil { + return errors.Wrap(err, "creating manifest") + } + + var buf bytes.Buffer + enc := plist.NewEncoder(&buf) + enc.Indent(" ") + if err := enc.Encode(manifest); err != nil { + return err + } + + switch *flAppManifest { + case "": + case "-": + _, err := os.Stdout.Write(buf.Bytes()) + return err + default: + return ioutil.WriteFile(*flAppManifest, buf.Bytes(), 0644) + } + + return nil +} + +func checkDistribution(pkgPath string) (bool, error) { + const ( + xarHeaderMagic = 0x78617221 + xarHeaderSize = 28 + ) + + f, err := os.Open(pkgPath) + if err != nil { + return false, err + } + defer f.Close() + + hdr := make([]byte, xarHeaderSize) + _, err = f.ReadAt(hdr, 0) + if err != nil { + return false, err + } + tocLenZlib := binary.BigEndian.Uint64(hdr[8:16]) + ztoc := make([]byte, tocLenZlib) + _, err = f.ReadAt(ztoc, xarHeaderSize) + if err != nil { + return false, err + } + + br := bytes.NewBuffer(ztoc) + zr, err := zlib.NewReader(br) + if err != nil { + return false, err + } + toc, err := ioutil.ReadAll(zr) + if err != nil { + return false, err + } + return bytes.Contains(toc, []byte(`Distribution`)), nil +} + +func (cmd *applyCommand) serverRepoURL() (string, error) { + serverURL, err := url.Parse(cmd.config.ServerURL) + if err != nil { + return "", err + } + serverURL.Path = "/repo" + return serverURL.String(), nil +} + +func pkgURL(repoURL, pkgPath string) string { + return path.Join(repoURL, filepath.Base(pkgPath)) +} + +// replaces .pkg with .plist +func manifestURL(repoURL, pkgPath string) string { + pu := pkgURL(repoURL, pkgPath) + trimmed := strings.TrimSuffix(pu, path.Ext(pu)) + return trimmed + ".plist" +} + +type appFile struct { + *os.File +} + +func (af *appFile) Size() int64 { + info, err := af.Stat() + if err != nil { + log.Fatal(err) + } + return info.Size() +} diff --git a/cmd/mdmctl/sign.go b/cmd/mdmctl/sign.go new file mode 100644 index 00000000..0fd226a9 --- /dev/null +++ b/cmd/mdmctl/sign.go @@ -0,0 +1,15 @@ +// +build !darwin + +package main + +import "fmt" + +func signPackage(path, outpath, developerID string) error { + fmt.Println("[WARNING] package signing only implemented on macOS") + return nil +} + +func checkSignature(pkgpath string) (bool, error) { + fmt.Println("[WARNING] package signing only implemented on macOS. An unsigned macOS package will not install with MDM.") + return true, nil +} diff --git a/cmd/mdmctl/sign_darwin.go b/cmd/mdmctl/sign_darwin.go new file mode 100644 index 00000000..dd70a7ae --- /dev/null +++ b/cmd/mdmctl/sign_darwin.go @@ -0,0 +1,38 @@ +package main + +import ( + "bytes" + "os" + "os/exec" + + "github.com/pkg/errors" +) + +func signPackage(pkgpath, outpath string, developerID string) error { + cmd := exec.Command("/usr/bin/productsign", "--sign", developerID, pkgpath, outpath) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "signing package") + } + return nil +} + +func checkSignature(pkgpath string) (bool, error) { + cmd := exec.Command("pkgutil", "--check-signature", pkgpath) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + if err != nil && !isNoSignature(out) { + return false, errors.Wrap(err, "checking signature") + } + + if bytes.Contains(out, []byte(`Status: signed`)) { + return true, nil + } + + return false, nil +} + +func isNoSignature(out []byte) bool { + return bytes.Contains(out, []byte(`Status: no signature`)) +}