mdmctl: support generating appmanifest (#199)

generate an appmanifest. validates and optionally signs packages.

For #93
Closes #203
This commit is contained in:
Victor Vrantchan
2017-06-02 00:09:13 -04:00
committed by GitHub
parent 744414dec5
commit c30f2806cc
4 changed files with 235 additions and 0 deletions

View File

@@ -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.

179
cmd/mdmctl/apply_app.go Normal file
View File

@@ -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(`<name>Distribution</name>`)), 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()
}

15
cmd/mdmctl/sign.go Normal file
View File

@@ -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
}

38
cmd/mdmctl/sign_darwin.go Normal file
View File

@@ -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`))
}