├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── appmanifest ├── appmanifest.go ├── appmanifest_test.go └── release.sh ├── certhelper ├── README.md ├── cert.go ├── certhelper.go ├── csr.go ├── key.go ├── mdmcert_download.go ├── mdmcert_download_test.go └── release.sh └── poke ├── README.md ├── poke.go ├── release.sh └── sample_env /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | build/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Victor Vrantchan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO := go 2 | glide := glide 3 | release := ./release.sh 4 | .PHONY: all clean appmanifest poke certhelper 5 | 6 | all: clean buildall 7 | 8 | clean: 9 | rm -rf ./build/* 10 | 11 | buildall: appmanifest poke certhelper 12 | 13 | appmanifest: 14 | rm -rf ./build/appmanifest 15 | @echo ">> building appmanifest" 16 | cd ./appmanifest && $(release) 17 | 18 | poke: 19 | rm -rf ./build/poke 20 | @echo ">> building poke" 21 | cd ./poke && $(release) 22 | 23 | certhelper: 24 | rm -rf ./build/certhelper 25 | @echo ">> building certhelper" 26 | cd ./certhelper && $(release) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Install 2 | see Releases for binary builds 3 | 4 | to build from source, use the make Makefile 5 | 6 | 7 | # appmanifest 8 | 9 | `appmanifest` takes a pkg and prints an [application manifest](http://help.apple.com/deployment/osx/#/ior5df10f73a) 10 | the current version only creates the `assets` array. 11 | 12 | The documentation says the metadata is required, but installs work without the metadata dict. 13 | Adding one only affects what shows up in `Launchpad` 14 | 15 | ``` 16 | appmanifest [options] /path/to/some.pkg 17 | -url string 18 | url of the pkg as it will be on the server 19 | -version 20 | prints the version 21 | ``` 22 | 23 | 24 | # certhelper 25 | create and manage push certificate 26 | ``` 27 | # creates an MDM CSR and private key for vendor cert 28 | # upload the created MDM CSR to enterprise portal to get a push certificate 29 | certhelper vendor -csr -cn=mdm-certtool -password=secret -country=US -email=foo@gmail.com 30 | 31 | # create a "provider" or a "customer" csr. This will be signed by the vendor cert and submitted to apple to get a push cert 32 | certhelper provider -csr -cn=mdm-certtool -password=secret -country=US -email=foo@gmail.com 33 | 34 | # sign the provider csr with the vendor private key 35 | # assumes `mdm.cer` is in the folder with all the other files. You can specify each path separately as well. 36 | certhelper vendor -sign -password=secret 37 | 38 | # Now upload the PushCertificateRequest to https://identity.apple.com/pushcert 39 | ``` 40 | see the `certhelper` README for more details 41 | 42 | # poke 43 | send mdm push notification to APNS 44 | see `poke` README.md for usage. 45 | 46 | -------------------------------------------------------------------------------- /appmanifest/appmanifest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | 11 | "github.com/groob/plist" 12 | ) 13 | 14 | // DefaultMD5Size is the default size of each file chunk that needs to be hashed 15 | const DefaultMD5Size = 10 << 20 // 10MB 16 | 17 | // http://help.apple.com/deployment/osx/#/ior5df10f73a 18 | type manifest struct { 19 | ManifestItems []manifestItem `plist:"items"` 20 | } 21 | 22 | type manifestItem struct { 23 | Assets []asset `plist:"assets"` 24 | // Apple claims the metadata struct is required, 25 | // but testing shows otherwise. 26 | Metadata *metadata `plist:"metadata,omitempty"` 27 | } 28 | 29 | type asset struct { 30 | Kind string `plist:"kind"` 31 | MD5Size int64 `plist:"md5-size"` 32 | MD5s []string `plist:"md5s"` 33 | URL string `plist:"url"` 34 | } 35 | 36 | type metadata struct { 37 | bundleInfo 38 | Items []bundleInfo `plist:"items,omitempty"` 39 | Kind string `plist:"kind"` 40 | Subtitle string `plist:"subtitle"` 41 | Title string `plist:"title"` 42 | } 43 | 44 | type bundleInfo struct { 45 | BundleIdentifier string `plist:"bundle-identifier"` 46 | BundleVersion string `plist:"bundle-version"` 47 | } 48 | 49 | var ( 50 | version = "unreleased" 51 | gitHash = "unknown" 52 | ) 53 | 54 | const usage = `appmanifest [options] /path/to/some.pkg` 55 | 56 | func main() { 57 | flVersion := flag.Bool("version", false, "prints the version") 58 | flURL := flag.String("url", "", "url of the pkg as it will be on the server") 59 | flMD5Size := flag.Int64("md5size", DefaultMD5Size, "md5 hash size in bytes") 60 | 61 | // set usage 62 | flag.Usage = func() { 63 | fmt.Fprintf(os.Stderr, "%s\n", usage) 64 | flag.PrintDefaults() 65 | } 66 | flag.Parse() 67 | 68 | if *flVersion { 69 | fmt.Printf("appmanifest - %v\n", version) 70 | fmt.Printf("git revision - %v\n", gitHash) 71 | os.Exit(0) 72 | } 73 | 74 | args := flag.Args() 75 | if len(args) == 0 { 76 | log.Println("must specify a path to a pkg") 77 | fmt.Println(usage) 78 | os.Exit(1) 79 | } 80 | 81 | path := args[0] 82 | if err := createAppManifest(path, *flURL, os.Stdout, *flMD5Size); err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | } 87 | 88 | // create manifest and return back a writer 89 | func createAppManifest(path, url string, writer io.Writer, md5Size int64) error { 90 | file, err := os.Open(path) 91 | if err != nil { 92 | return err 93 | } 94 | defer file.Close() 95 | 96 | // get file info 97 | info, err := file.Stat() 98 | if err != nil { 99 | return err 100 | } 101 | fSize := info.Size() 102 | if md5Size > fSize { 103 | md5Size = fSize 104 | } 105 | 106 | // create a list of md5s 107 | md5s, err := calculateMD5s(file, md5Size) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | // create an asset 113 | ast := asset{ 114 | Kind: "software-package", 115 | MD5Size: md5Size, 116 | MD5s: md5s, 117 | URL: url, 118 | } 119 | 120 | // make a manifest 121 | m := manifest{ 122 | ManifestItems: []manifestItem{ 123 | manifestItem{ 124 | Assets: []asset{ast}, 125 | }, 126 | }, 127 | } 128 | 129 | // write a plist 130 | enc := plist.NewEncoder(writer) 131 | enc.Indent(" ") 132 | return enc.Encode(&m) 133 | } 134 | 135 | // reads a file and returns a slice of hashes, one for each 136 | // 10mb chunk 137 | func calculateMD5s(f io.Reader, s int64) ([]string, error) { 138 | h := md5.New() 139 | var md5s []string 140 | for { 141 | n, err := io.CopyN(h, f, s) 142 | if n > 0 { 143 | md5s = append(md5s, fmt.Sprintf("%x", h.Sum(nil))) 144 | h.Reset() 145 | } 146 | if err != nil { 147 | if err == io.EOF { 148 | err = nil 149 | } 150 | return md5s, err 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /appmanifest/appmanifest_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestCalucalteMD5s(t *testing.T) { 9 | ok := []string{ 10 | "f1c9645dbc14efddc7d8a322685f26eb", 11 | "f1c9645dbc14efddc7d8a322685f26eb", 12 | "f1c9645dbc14efddc7d8a322685f26eb", 13 | "93b885adfe0da089cdf634904fd59f71", 14 | } 15 | buf := make([]byte, DefaultMD5Size*3+1) 16 | r := bytes.NewReader(buf) 17 | md5s, err := calculateMD5s(r, DefaultMD5Size) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | if len(md5s) != len(ok) { 22 | t.Fatal("expected", len(ok), "got", len(md5s)) 23 | } 24 | for i, h := range md5s { 25 | if ok[i] != h { 26 | t.Fatal("expected", ok[i], "got", h) 27 | } 28 | } 29 | } 30 | 31 | /* 32 | Benchmark10MB-8 500000 3297 ns/op 32896 B/op 3 allocs/op 33 | Benchmark100MB-8 300000 3528 ns/op 32897 B/op 3 allocs/op 34 | Benchmark1000MB-8 1 1967685209 ns/op 3334064 B/op 829 allocs/op 35 | */ 36 | 37 | func benchmarkSize(b *testing.B, size int) { 38 | var buf = make([]byte, size) 39 | r := bytes.NewReader(buf) 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | calculateMD5s(r, DefaultMD5Size) 43 | } 44 | } 45 | 46 | func Benchmark10MB(b *testing.B) { 47 | benchmarkSize(b, DefaultMD5Size) 48 | } 49 | 50 | func Benchmark100MB(b *testing.B) { 51 | benchmarkSize(b, DefaultMD5Size*10) 52 | } 53 | 54 | func Benchmark1000MB(b *testing.B) { 55 | benchmarkSize(b, DefaultMD5Size*100) 56 | } 57 | -------------------------------------------------------------------------------- /appmanifest/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION="1.0.1" 4 | NAME=appmanifest 5 | OUTPUT=../build 6 | 7 | echo "Building $NAME version $VERSION" 8 | 9 | mkdir -p ${OUTPUT} 10 | 11 | build() { 12 | echo -n "=> $1-$2: " 13 | GOOS=$1 GOARCH=$2 go build -o ${OUTPUT}/$NAME -ldflags "-X main.version=$VERSION -X main.gitHash=`git rev-parse HEAD`" ./${NAME}.go 14 | du -h ${OUTPUT}/${NAME} 15 | } 16 | 17 | build "darwin" "amd64" 18 | -------------------------------------------------------------------------------- /certhelper/README.md: -------------------------------------------------------------------------------- 1 | create and manage push certificate 2 | 3 | ## Example: 4 | ``` 5 | # creates an MDM CSR and private key for vendor cert 6 | # upload the created MDM CSR to enterprise portal to get a push certificate 7 | certhelper vendor -csr -cn=mdm-certtool -password=secret -country=US -email=foo@gmail.com 8 | 9 | # create a "provider" or a "customer" csr. This will be signed by the vendor cert and submitted to apple to get a push cert 10 | certhelper provider -csr -cn=mdm-certtool -password=secret -country=US -email=foo@gmail.com 11 | 12 | # sign the provider csr with the vendor private key 13 | # assumes `mdm.cer` is in the folder with all the other files. You can specify each path separately as well. 14 | certhelper vendor -sign -password=secret 15 | 16 | # Now upload the PushCertificateRequest to https://identity.apple.com/pushcert 17 | ``` 18 | 19 | ## Full Usage: 20 | ``` 21 | usage: certhelper [] 22 | vendor manage mdm vendor certs 23 | provider manage certs as a provider(mdm server administrator) 24 | type --help to see usage for each subcommand 25 | 26 | 27 | Usage of vendor: 28 | -cert string 29 | path to mdm vendor cert provided by apple (default "mdm.cer") 30 | -cn string 31 | common name for certificate request 32 | -country string 33 | two letter country flag for CSR Subject(example: US) (default "US") 34 | -csr 35 | create a CSR for MDM vendor certificate 36 | -email string 37 | email address to use in CSR request Subject 38 | -password string 39 | rsa private key password 40 | -private-key string 41 | path to provider csr which needs to be signed (default "VendorPrivateKey.key") 42 | -provider-csr string 43 | path to csr which needs to be signed (default "ProviderUnsignedPushCertificateRequest.csr") 44 | -sign 45 | sign a provider push csr with the vendor certificate 46 | 47 | 48 | Usage of provider: 49 | -cn string 50 | common name for certificate request 51 | -country string 52 | two letter country flag for CSR Subject(example: US) (default "US") 53 | -csr 54 | create a CSR for a push certificate request 55 | -email string 56 | email address to use in CSR request Subject 57 | -password string 58 | rsa private key password 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /certhelper/cert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | const ( 11 | certificatePEMBlockType = "CERTIFICATE" 12 | ) 13 | 14 | func pemCert(derBytes []byte) []byte { 15 | pemBlock := &pem.Block{ 16 | Type: certificatePEMBlockType, 17 | Headers: nil, 18 | Bytes: derBytes, 19 | } 20 | out := pem.EncodeToMemory(pemBlock) 21 | return out 22 | } 23 | 24 | func loadDERCertFromFile(path string) ([]byte, error) { 25 | data, err := ioutil.ReadFile(path) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | crt, err := x509.ParseCertificate(data) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return crt.Raw, nil 35 | } 36 | 37 | func loadCertfromHTTP(url string) ([]byte, error) { 38 | resp, err := http.Get(url) 39 | if err != nil { 40 | return nil, err 41 | } 42 | defer resp.Body.Close() 43 | data, err := ioutil.ReadAll(resp.Body) 44 | if err != nil { 45 | return nil, err 46 | } 47 | crt, err := x509.ParseCertificate(data) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return crt.Raw, nil 52 | } 53 | -------------------------------------------------------------------------------- /certhelper/certhelper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha1" 8 | "encoding/base64" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "os" 15 | "strings" 16 | 17 | "github.com/groob/plist" 18 | ) 19 | 20 | var ( 21 | version = "unreleased" 22 | gitHash = "unknown" 23 | wwdrIntermediaryURL = "https://developer.apple.com/certificationauthority/AppleWWDRCA.cer" 24 | appleRootCAURL = "http://www.apple.com/appleca/AppleIncRootCertificate.cer" 25 | providerCSRFilename = "ProviderUnsignedPushCertificateRequest.csr" 26 | providerPKeyFilename = "ProviderPrivateKey.key" 27 | vendorPKeyFilename = "VendorPrivateKey.key" 28 | vendorCSRFilename = "VendorCertificateRequest.csr" 29 | pushRequestFilename = "PushCertificateRequest" 30 | ) 31 | 32 | func main() { 33 | // vendor cmd flags 34 | vendorCMD := flag.NewFlagSet("vendor", flag.ExitOnError) 35 | vendorCSRFlag := vendorCMD.Bool("csr", false, "create a CSR for MDM vendor certificate") 36 | vendorCSREmail := vendorCMD.String("email", "", "email address to use in CSR request Subject") 37 | vendorCSRCountry := vendorCMD.String("country", "US", "two letter country flag for CSR Subject(example: US)") 38 | vendorCSRCName := vendorCMD.String("cn", "", "common name for certificate request") 39 | vendorPKeyPass := vendorCMD.String("password", "", "rsa private key password") 40 | vendorSignFlag := vendorCMD.Bool("sign", false, "sign a provider push csr with the vendor certificate") 41 | vendorCertPath := vendorCMD.String("cert", "mdm.cer", "path to mdm vendor cert provided by apple") 42 | vendorProviderCSRPath := vendorCMD.String("provider-csr", providerCSRFilename, "path to csr which needs to be signed") 43 | vendorPKeyPath := vendorCMD.String("private-key", vendorPKeyFilename, "path to provider csr which needs to be signed") 44 | 45 | // provider cmd flags 46 | providerCMD := flag.NewFlagSet("provider", flag.ExitOnError) 47 | providerCSRFlag := providerCMD.Bool("csr", false, "create a CSR for a push certificate request") 48 | providerCSREmail := providerCMD.String("email", "", "email address to use in CSR request Subject") 49 | providerCSRCountry := providerCMD.String("country", "US", "two letter country flag for CSR Subject(example: US)") 50 | providerCSRCName := providerCMD.String("cn", "", "common name for certificate request") 51 | providerPKeyPass := providerCMD.String("password", "", "rsa private key password") 52 | 53 | // mdmcert.download 54 | mdmcertdCMD := flag.NewFlagSet("mdmcert.download", flag.ExitOnError) 55 | var ( 56 | mdmcertdEmail = mdmcertdCMD.String("email", "", "email address registered with mdmcert.download") 57 | mdmcertdCSR = mdmcertdCMD.String("csr", providerCSRFilename, "path to PEM encoded csr. default generated by provider command.") 58 | mdmcertdCERT = mdmcertdCMD.String("cert", "", "PEM encoded server certificate. Use the TLS certificate of your mdm server.") 59 | mdmcertdPriv = mdmcertdCMD.String("key", "", "path to PEM encoded server private key(for use with -decode)") 60 | mdmcertdPass = mdmcertdCMD.String("password", "", "rsa private key password(for server cert)") 61 | mdmcertdDec = mdmcertdCMD.String("decode", "", "path to mdm_signed_request.p7 file") 62 | ) 63 | // general flags 64 | flVersion := flag.Bool("version", false, "prints the version") 65 | // set usage 66 | flag.Usage = func() { 67 | fmt.Println("usage: certhelper []") 68 | fmt.Println(" vendor manage mdm vendor certs") 69 | fmt.Println(" provider manage certs as a provider(mdm server administrator)") 70 | fmt.Println(" mdmcert.download request a certificate from mdmcert.download(mdm server administrator)") 71 | fmt.Println("type --help to see usage for each subcommand") 72 | } 73 | 74 | flag.Parse() 75 | 76 | if *flVersion { 77 | fmt.Printf("certhelper - %v\n", version) 78 | fmt.Printf("git revision - %v\n", gitHash) 79 | os.Exit(0) 80 | } 81 | 82 | if len(os.Args) <= 2 { 83 | flag.Usage() 84 | return 85 | } 86 | 87 | switch os.Args[1] { 88 | case "vendor": 89 | vendorCMD.Parse(os.Args[2:]) 90 | case "provider": 91 | providerCMD.Parse(os.Args[2:]) 92 | case "mdmcert.download": 93 | mdmcertdCMD.Parse(os.Args[2:]) 94 | default: 95 | fmt.Printf("%q is not valid command.\n", os.Args[1]) 96 | os.Exit(2) 97 | } 98 | 99 | if vendorCMD.Parsed() { 100 | password := []byte(*vendorPKeyPass) 101 | if *vendorCSRFlag { 102 | // validate CSR arguments 103 | if err := checkCSRFlags(*vendorCSRCName, *vendorCSRCountry, *vendorCSREmail, password); err != nil { 104 | fmt.Println("private key password, cn, email, and country code must be provided for CSR") 105 | fmt.Printf("err: %s\n", err) 106 | os.Exit(1) 107 | } 108 | // create CSR 109 | req := &csrRequest{ 110 | cname: *vendorCSRCName, 111 | email: *vendorCSREmail, 112 | country: *vendorCSRCountry, 113 | password: password, 114 | pkeyFilename: vendorPKeyFilename, 115 | csrFilename: vendorCSRFilename, 116 | } 117 | if err := makeCSR(req); err != nil { 118 | fmt.Printf("err: %s\n", err) 119 | os.Exit(1) 120 | } 121 | } 122 | // sign a csr request 123 | if *vendorSignFlag { 124 | pushRequest, err := makePushRequestPlist( 125 | *vendorCertPath, 126 | *vendorProviderCSRPath, 127 | *vendorPKeyPath, 128 | password, 129 | ) 130 | if err != nil { 131 | fmt.Printf("err: %s\n", err) 132 | os.Exit(1) 133 | } 134 | if err := writePushCertRequest(pushRequest); err != nil { 135 | if err != nil { 136 | fmt.Printf("err: %s\n", err) 137 | os.Exit(1) 138 | } 139 | } 140 | } 141 | } 142 | 143 | if providerCMD.Parsed() { 144 | password := []byte(*providerPKeyPass) 145 | if *providerCSRFlag { 146 | // validate CSR arguments 147 | if err := checkCSRFlags(*providerCSRCName, *providerCSRCountry, *providerCSREmail, password); err != nil { 148 | fmt.Println("private key password, cn, email, and country code must be provided for CSR") 149 | fmt.Printf("err: %s\n", err) 150 | os.Exit(1) 151 | } 152 | // create CSR 153 | req := &csrRequest{ 154 | cname: *providerCSRCName, 155 | email: *providerCSREmail, 156 | country: *providerCSRCountry, 157 | password: password, 158 | pkeyFilename: providerPKeyFilename, 159 | csrFilename: providerCSRFilename, 160 | } 161 | if err := makeCSR(req); err != nil { 162 | fmt.Printf("err: %s\n", err) 163 | os.Exit(1) 164 | } 165 | } 166 | } 167 | 168 | if mdmcertdCMD.Parsed() { 169 | if *mdmcertdDec != "" { 170 | if err := decodeSignedRequest(*mdmcertdDec, *mdmcertdCERT, *mdmcertdPriv, *mdmcertdPass); err != nil { 171 | fmt.Printf("err: %s\n", err) 172 | os.Exit(1) 173 | } 174 | fmt.Printf("mdmcert.download_%s created. You can now upload it to https://identity.apple.com/\n", pushRequestFilename) 175 | fmt.Println("Once signed by Apple, you will be able to send push notifications to MDM enrolled devices.") 176 | return 177 | } 178 | 179 | fmt.Printf("requesting cert from mdmcert.download with \n\temail=%q\n\tcsr=%q\n\tservercert=%q\n", 180 | *mdmcertdEmail, 181 | *mdmcertdCSR, 182 | *mdmcertdCERT, 183 | ) 184 | e := new(errReader) 185 | csrBytes := e.ReadFile(*mdmcertdCSR) 186 | certBytes := e.ReadFile(*mdmcertdCERT) 187 | if e.err != nil { 188 | fmt.Printf("err: %s\n", e.err) 189 | os.Exit(1) 190 | } 191 | sign := newSignRequest(*mdmcertdEmail, csrBytes, certBytes) 192 | req, err := sign.HTTPRequest() 193 | if err != nil { 194 | fmt.Printf("err: %s\n", err) 195 | os.Exit(1) 196 | } 197 | client := http.DefaultClient 198 | if err := sendRequest(client, req); err != nil { 199 | fmt.Printf("err: %s\n", err) 200 | os.Exit(1) 201 | } 202 | fmt.Println(`mdmcert.download successfuly signed your certificate. check your email for next steps. 203 | the -decode flag can be used to extract the certificate in the email attachment.`) 204 | } 205 | } 206 | 207 | type errReader struct { 208 | err error 209 | } 210 | 211 | func (e *errReader) ReadFile(path string) []byte { 212 | if e.err != nil { 213 | return nil 214 | } 215 | var d []byte 216 | d, e.err = ioutil.ReadFile(path) 217 | return d 218 | } 219 | 220 | func checkCSRFlags(cname, country, email string, password []byte) error { 221 | if cname == "" { 222 | return errors.New("cn flag not specified") 223 | } 224 | if email == "" { 225 | return errors.New("email flag not specified") 226 | } 227 | if country == "" { 228 | return errors.New("country flag not specified") 229 | } 230 | if len(password) == 0 { 231 | return errors.New("private key password empty") 232 | } 233 | if len(country) != 2 { 234 | return errors.New("must be a two letter country code") 235 | } 236 | return nil 237 | } 238 | 239 | // plist for push certificate request 240 | type pushCertRequest struct { 241 | PushCertRequestCSR string 242 | PushCertCertificateChain string 243 | PushCertSignature string 244 | } 245 | 246 | // args for a csr request 247 | type csrRequest struct { 248 | cname, country, email string 249 | password []byte 250 | pkeyFilename, csrFilename string 251 | } 252 | 253 | // create a private key and CSR and save both to disk 254 | func makeCSR(req *csrRequest) error { 255 | key, err := rsa.GenerateKey(rand.Reader, 2048) 256 | if err != nil { 257 | return err 258 | } 259 | pemKey, err := encryptedKey(key, req.password) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | if err := ioutil.WriteFile(req.pkeyFilename, pemKey, 0600); err != nil { 265 | return err 266 | } 267 | 268 | derBytes, err := newCSR(key, strings.ToLower(req.email), strings.ToUpper(req.country), req.cname) 269 | if err != nil { 270 | return err 271 | } 272 | pemCSR := pemCSR(derBytes) 273 | return ioutil.WriteFile(req.csrFilename, pemCSR, 0600) 274 | } 275 | 276 | // create a push request plist 277 | func makePushRequestPlist(mdmCertPath, providerCSRPath, pKeyPath string, pKeyPass []byte) (*pushCertRequest, error) { 278 | // private key of the mdm vendor cert 279 | key, err := loadKeyFromFile(pKeyPath, pKeyPass) 280 | if err != nil { 281 | return nil, err 282 | } 283 | 284 | // provider csr 285 | csr, err := loadCSRfromFile(providerCSRPath) 286 | if err != nil { 287 | return nil, err 288 | } 289 | 290 | // csr signature 291 | signature, err := signProviderCSR(csr.Raw, key) 292 | if err != nil { 293 | return nil, err 294 | } 295 | 296 | // vendor cert 297 | mdmCertBytes, err := loadDERCertFromFile(mdmCertPath) 298 | if err != nil { 299 | return nil, err 300 | } 301 | mdmPEM := pemCert(mdmCertBytes) 302 | 303 | // wwdr cert 304 | wwdrCertBytes, err := loadCertfromHTTP(wwdrIntermediaryURL) 305 | if err != nil { 306 | return nil, err 307 | } 308 | wwdrPEM := pemCert(wwdrCertBytes) 309 | 310 | // apple root certificate 311 | rootCertBytes, err := loadCertfromHTTP(appleRootCAURL) 312 | if err != nil { 313 | return nil, err 314 | } 315 | rootPEM := pemCert(rootCertBytes) 316 | 317 | csrB64 := base64.StdEncoding.EncodeToString(csr.Raw) 318 | sig64 := base64.StdEncoding.EncodeToString(signature) 319 | pushReq := &pushCertRequest{ 320 | PushCertRequestCSR: csrB64, 321 | PushCertCertificateChain: makeCertChain(mdmPEM, wwdrPEM, rootPEM), 322 | PushCertSignature: sig64, 323 | } 324 | return pushReq, nil 325 | } 326 | 327 | // save plist as base64 encoded string 328 | func writePushCertRequest(req *pushCertRequest) error { 329 | data, err := plist.MarshalIndent(req, " ") 330 | if err != nil { 331 | return err 332 | } 333 | encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data))) 334 | base64.StdEncoding.Encode(encoded, data) 335 | if err := ioutil.WriteFile(pushRequestFilename, encoded, 0600); err != nil { 336 | return err 337 | } 338 | return nil 339 | } 340 | 341 | func makeCertChain(mdmPEM, wwdrPEM, rootPEM []byte) string { 342 | return string(mdmPEM) + string(wwdrPEM) + string(rootPEM) 343 | } 344 | 345 | func signProviderCSR(csrData []byte, key *rsa.PrivateKey) ([]byte, error) { 346 | h := sha1.New() 347 | h.Write(csrData) 348 | signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA1, h.Sum(nil)) 349 | if err != nil { 350 | fmt.Fprintf(os.Stderr, "Error from signing: %s\n", err) 351 | return nil, err 352 | } 353 | return signature, nil 354 | } 355 | -------------------------------------------------------------------------------- /certhelper/csr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "errors" 10 | "io/ioutil" 11 | ) 12 | 13 | const ( 14 | csrPEMBlockType = "CERTIFICATE REQUEST" 15 | ) 16 | 17 | // create a CSR using the same parameters as Keychain Access would produce 18 | func newCSR(priv *rsa.PrivateKey, email, country, cname string) ([]byte, error) { 19 | subj := pkix.Name{ 20 | Country: []string{country}, 21 | CommonName: cname, 22 | ExtraNames: []pkix.AttributeTypeAndValue{pkix.AttributeTypeAndValue{ 23 | Type: []int{1, 2, 840, 113549, 1, 9, 1}, 24 | Value: email, 25 | }}, 26 | } 27 | template := &x509.CertificateRequest{ 28 | Subject: subj, 29 | } 30 | return x509.CreateCertificateRequest(rand.Reader, template, priv) 31 | } 32 | 33 | // convert DER to PEM format 34 | func pemCSR(derBytes []byte) []byte { 35 | pemBlock := &pem.Block{ 36 | Type: csrPEMBlockType, 37 | Headers: nil, 38 | Bytes: derBytes, 39 | } 40 | out := pem.EncodeToMemory(pemBlock) 41 | return out 42 | } 43 | 44 | // load PEM encoded CSR from file 45 | func loadCSRfromFile(path string) (*x509.CertificateRequest, error) { 46 | data, err := ioutil.ReadFile(path) 47 | if err != nil { 48 | return nil, err 49 | } 50 | pemBlock, _ := pem.Decode(data) 51 | if pemBlock == nil { 52 | return nil, errors.New("cannot find the next PEM formatted block") 53 | } 54 | if pemBlock.Type != csrPEMBlockType || len(pemBlock.Headers) != 0 { 55 | return nil, errors.New("unmatched type or headers") 56 | } 57 | return x509.ParseCertificateRequest(pemBlock.Bytes) 58 | } 59 | -------------------------------------------------------------------------------- /certhelper/key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "errors" 9 | "io/ioutil" 10 | ) 11 | 12 | const ( 13 | rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" 14 | privateKeyPEMBlockType = "PRIVATE KEY" 15 | ) 16 | 17 | // protect an rsa key with a password 18 | func encryptedKey(key *rsa.PrivateKey, password []byte) ([]byte, error) { 19 | privBytes := x509.MarshalPKCS1PrivateKey(key) 20 | privPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, rsaPrivateKeyPEMBlockType, privBytes, password, x509.PEMCipher3DES) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | out := pem.EncodeToMemory(privPEMBlock) 26 | return out, nil 27 | } 28 | 29 | // load an encrypted private key from disk 30 | func loadKeyFromFile(path string, password []byte) (*rsa.PrivateKey, error) { 31 | data, err := ioutil.ReadFile(path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | pemBlock, _ := pem.Decode(data) 37 | if pemBlock == nil { 38 | return nil, errors.New("PEM decode failed") 39 | } 40 | 41 | if string(password) != "" { 42 | b, err := x509.DecryptPEMBlock(pemBlock, password) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return x509.ParsePKCS1PrivateKey(b) 47 | } 48 | return x509.ParsePKCS1PrivateKey(pemBlock.Bytes) 49 | } 50 | -------------------------------------------------------------------------------- /certhelper/mdmcert_download.go: -------------------------------------------------------------------------------- 1 | // Integration with Jesse's mdmcert.download 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "crypto/x509" 8 | "encoding/base64" 9 | "encoding/hex" 10 | "encoding/json" 11 | "encoding/pem" 12 | "errors" 13 | "fmt" 14 | "io/ioutil" 15 | "net/http" 16 | 17 | "github.com/fullsailor/pkcs7" 18 | ) 19 | 20 | const ( 21 | mdmcertRequestURL = "https://mdmcert.download/api/v1/signrequest" 22 | // see 23 | // https://github.com/jessepeterson/commandment/blob/1352b51ba6697260d1111eccc3a5a0b5b9af60d0/commandment/mdmcert.py#L23-L28 24 | mdmcertServerKey = "f847aea2ba06b41264d587b229e2712c89b1490a1208b7ff1aafab5bb40d47bc" 25 | ) 26 | 27 | // format of a signing request to mdmcert.download 28 | type signRequest struct { 29 | CSR string `json:"csr"` // base64 encoded PEM CSR 30 | Email string `json:"email"` 31 | Key string `json:"key"` // server key from above 32 | Encrypt string `json:"encrypt"` // server cert 33 | } 34 | 35 | func newSignRequest(email string, pemCSR []byte, serverCertificate []byte) *signRequest { 36 | encodedCSR := base64.StdEncoding.EncodeToString(pemCSR) 37 | encodedServerCert := base64.StdEncoding.EncodeToString(serverCertificate) 38 | return &signRequest{ 39 | CSR: encodedCSR, 40 | Email: email, 41 | Key: mdmcertServerKey, 42 | Encrypt: encodedServerCert, 43 | } 44 | } 45 | 46 | func (sign *signRequest) HTTPRequest() (*http.Request, error) { 47 | buf := new(bytes.Buffer) 48 | if err := json.NewEncoder(buf).Encode(sign); err != nil { 49 | return nil, err 50 | } 51 | req, err := http.NewRequest("POST", mdmcertRequestURL, ioutil.NopCloser(buf)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | req.Header.Add("Content-Type", "application/json") 56 | req.Header.Add("User-Agent", "micromdm/certhelper") 57 | return req, nil 58 | } 59 | 60 | func sendRequest(client *http.Client, req *http.Request) error { 61 | resp, err := client.Do(req) 62 | if err != nil { 63 | return nil 64 | } 65 | defer resp.Body.Close() 66 | if resp.StatusCode != http.StatusOK { 67 | return fmt.Errorf("received bad status from mdmcert.download. status=%q", resp.Status) 68 | } 69 | var jsn = struct { 70 | Result string 71 | }{} 72 | if err := json.NewDecoder(resp.Body).Decode(&jsn); err != nil { 73 | return err 74 | } 75 | if jsn.Result != "success" { 76 | return fmt.Errorf("got unexpected result body: %q\n", jsn.Result) 77 | } 78 | return nil 79 | } 80 | 81 | // The user will receive a hex encoded p7 file as an email attachment. 82 | // the file contents is a pkcs7 file, encrypted using the server certificate as the 83 | // intended recipient. 84 | // We use the server private key to decode the pkcs7 envelope and extract a 85 | // base64 encoded plist (same format as the once created by `mapkePushRequestPlist` 86 | // Once the pkcs7 file is decrypted, we save the file to disk for the user to upload 87 | // to identity.apple.com for a push certificate. 88 | func decodeSignedRequest(p7Path, certPath, privPath, privPass string) error { 89 | hexBytes, err := ioutil.ReadFile(p7Path) 90 | if err != nil { 91 | return err 92 | } 93 | key, err := loadKeyFromFile(privPath, []byte(privPass)) 94 | if err != nil { 95 | return err 96 | } 97 | certPemBytes, err := ioutil.ReadFile(certPath) 98 | if err != nil { 99 | return err 100 | } 101 | pemBlock, _ := pem.Decode(certPemBytes) 102 | if pemBlock == nil { 103 | return errors.New("PEM decode failed") 104 | } 105 | if pemBlock.Type != "CERTIFICATE" { 106 | return errors.New("certificate: unmatched type or headers") 107 | } 108 | cert, err := x509.ParseCertificate(pemBlock.Bytes) 109 | if err != nil { 110 | return err 111 | } 112 | pkcsBytes, err := hex.DecodeString(string(hexBytes)) 113 | if err != nil { 114 | return err 115 | } 116 | p7, err := pkcs7.Parse(pkcsBytes) 117 | if err != nil { 118 | return err 119 | } 120 | content, err := p7.Decrypt(cert, key) 121 | if err != nil { 122 | return err 123 | } 124 | return ioutil.WriteFile(fmt.Sprintf("mdmcert.download_%s", pushRequestFilename), content, 0666) 125 | } 126 | -------------------------------------------------------------------------------- /certhelper/mdmcert_download_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func Test_signRequest_HTTP(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | fields *signRequest 13 | wantErr bool 14 | }{ 15 | { 16 | name: "default", 17 | fields: newSignRequest("groob@acme.co", []byte("fakecsr"), []byte("fakecert")), 18 | }, 19 | } 20 | for _, tt := range tests { 21 | sign := &signRequest{ 22 | CSR: tt.fields.CSR, 23 | Email: tt.fields.Email, 24 | Key: tt.fields.Key, 25 | Encrypt: tt.fields.Encrypt, 26 | } 27 | got, err := sign.HTTPRequest() 28 | if (err != nil) != tt.wantErr { 29 | t.Errorf("%q. signRequest.HTTP() error = %v, wantErr %v", tt.name, err, tt.wantErr) 30 | continue 31 | } 32 | 33 | if have, want := got.Header.Get("Content-Type"), "application/json"; have != want { 34 | t.Errorf("have %q, want %q\n", have, want) 35 | } 36 | if have, want := got.Method, "POST"; have != want { 37 | t.Errorf("have %q, want %q\n", have, want) 38 | } 39 | 40 | var have signRequest 41 | if err := json.NewDecoder(got.Body).Decode(&have); err != nil { 42 | t.Fatal(err) 43 | } 44 | if !reflect.DeepEqual(have, *sign) { 45 | t.Errorf("have %#v, want %#v", have, *sign) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /certhelper/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION="1.2.0" 4 | NAME=certhelper 5 | OUTPUT=../build 6 | 7 | echo "Building $NAME version $VERSION" 8 | 9 | mkdir -p ${OUTPUT} 10 | 11 | build() { 12 | echo -n "=> $1-$2: " 13 | GOOS=$1 GOARCH=$2 go build -o ${OUTPUT}/$NAME -ldflags "-X main.version=$VERSION -X main.gitHash=`git rev-parse HEAD`" ./*.go 14 | du -h ${OUTPUT}/${NAME} 15 | } 16 | 17 | build "darwin" "amd64" 18 | -------------------------------------------------------------------------------- /poke/README.md: -------------------------------------------------------------------------------- 1 | poke - sends a MDM push notification to a device using the HTTP2 APNS gateway 2 | 3 | 4 | An utility for testing of MDM push notifications; asking devices to connect to their MDM server. 5 | 6 | # How to use 7 | ``` 8 | Usage of poke: 9 | -magic string 10 | pushmagic 11 | -push-cert string 12 | path to push certificate 13 | -push-pass string 14 | push certificate password 15 | -token string 16 | deviceToken 17 | -version 18 | print version information 19 | ``` 20 | 21 | We can limit the number of flags we set by declaring some of the configuration as environment variables: 22 | 23 | ``` 24 | # ~/push_env 25 | 26 | # export from keychain as PKCS#12, not .cer 27 | export MDM_PUSH_CERT=/path/to/pushcert.p12 28 | export MDM_PUSH_PASS=secret 29 | ``` 30 | 31 | ``` 32 | source push_env 33 | ./poke -magic=2AD29D04-2440-4816-B9C2-935F2F0AC1C4 -token=f8b4ccc6da57207807fcff9767a0e15aec204721fee7ff2cd6cd5c16402b1ad5 34 | ``` 35 | 36 | `poke` will send a MDM formatted push notification to the APNS gateway and return back an UUID 37 | 38 | # Debugging information 39 | `poke` will print additional verbose APNS logs with `GODEBUG=http2debug=2` environment variable. 40 | ``` 41 | GODEBUG=http2debug=2 ./poke -magic=6E2881DC-6088-43E5-8E1F-EF267FC8B71D -token=f824ccc6da57207807fcff9767a0e15aee404721fee7ff28d69d5016604a08d5 42 | ``` 43 | 44 | Example debug output: 45 | ``` 46 | 2016/03/03 11:36:06 http2: Transport failed to get client conn for api.push.apple.com:443: http2: no cached connection was available 47 | 2016/03/03 11:36:06 http2: Transport creating client conn to 17.172.234.15:443 48 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: wrote SETTINGS len=18, settings: ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=4194304, MAX_HEADER_LIST_SIZE=10485760 49 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: wrote WINDOW_UPDATE len=4 (conn) incr=1073741824 50 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: read SETTINGS len=24, settings: HEADER_TABLE_SIZE=4096, MAX_CONCURRENT_STREAMS=500, MAX_FRAME_SIZE=16384, MAX_HEADER_LIST_SIZE=8000 51 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: wrote SETTINGS flags=ACK len=0 52 | 2016/03/03 11:36:06 Unhandled Setting: [HEADER_TABLE_SIZE = 4096] 53 | 2016/03/03 11:36:06 Unhandled Setting: [MAX_HEADER_LIST_SIZE = 8000] 54 | 2016/03/03 11:36:06 http2: Transport encoding header ":authority" = "api.push.apple.com" 55 | 2016/03/03 11:36:06 http2: Transport encoding header ":method" = "POST" 56 | 2016/03/03 11:36:06 http2: Transport encoding header ":path" = "/3/device/xxx" 57 | 2016/03/03 11:36:06 http2: Transport encoding header ":scheme" = "https" 58 | 2016/03/03 11:36:06 http2: Transport encoding header "content-type" = "application/json" 59 | 2016/03/03 11:36:06 http2: Transport encoding header "content-length" = "46" 60 | 2016/03/03 11:36:06 http2: Transport encoding header "accept-encoding" = "gzip" 61 | 2016/03/03 11:36:06 http2: Transport encoding header "user-agent" = "Go-http-client/2.0" 62 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: wrote HEADERS flags=END_HEADERS stream=1 len=132 63 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: wrote DATA stream=1 len=46 data="{\"mdm\":\"\"}" 64 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: wrote DATA flags=END_STREAM stream=1 len=0 data="" 65 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: read GOAWAY len=46 LastStreamID=0 ErrCode=NO_ERROR Debug="{\"reason\":\"BadCertificateEnvironment\"}" 66 | 2016/03/03 11:36:06 http2: Transport received GOAWAY len=46 LastStreamID=0 ErrCode=NO_ERROR Debug="{\"reason\":\"BadCertificateEnvironment\"}" 67 | 2016/03/03 11:36:06 http2: Framer 0xc8204e9800: read GOAWAY len=8 LastStreamID=0 ErrCode=NO_ERROR Debug="" 68 | 2016/03/03 11:36:06 http2: Transport received GOAWAY len=8 LastStreamID=0 ErrCode=NO_ERROR Debug="" 69 | 2016/03/03 11:36:06 Transport readFrame error: (*errors.errorString) EOF 70 | 2016/03/03 11:36:06 RoundTrip failure: unexpected EOF 71 | ``` 72 | -------------------------------------------------------------------------------- /poke/poke.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/RobotsAndPencils/buford/certificate" 11 | "github.com/RobotsAndPencils/buford/payload" 12 | "github.com/RobotsAndPencils/buford/push" 13 | ) 14 | 15 | var ( 16 | version = "unreleased" 17 | gitHash = "unknown" 18 | ) 19 | 20 | func main() { 21 | // flags 22 | var ( 23 | flMagic = flag.String("magic", "", "pushmagic") 24 | flToken = flag.String("token", "", "deviceToken") 25 | flVersion = flag.Bool("version", false, "print version information") 26 | flPushCert = flag.String("push-cert", envString("MDM_PUSH_CERT", ""), "path to push certificate") 27 | flPushPass = flag.String("push-pass", envString("MDM_PUSH_PASS", ""), "push certificate password") 28 | ) 29 | flag.Parse() 30 | 31 | if *flVersion { 32 | fmt.Printf("poke - %v\n", version) 33 | fmt.Printf("git revision - %v\n", gitHash) 34 | os.Exit(0) 35 | } 36 | 37 | // load apns cert 38 | cert, key, err := certificate.Load(*flPushCert, *flPushPass) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // check the validity of the token 44 | if !push.IsDeviceTokenValid(*flToken) { 45 | log.Fatal("invalid token") 46 | } 47 | 48 | // create bufford client 49 | client, err := push.NewClient(certificate.TLS(cert, key)) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | // push service 55 | service := push.Service{ 56 | Client: client, 57 | Host: push.Production, 58 | } 59 | 60 | expiration := time.Now().Add(5 * time.Minute) // expire in 5 minutes 61 | headers := &push.Headers{ 62 | LowPriority: true, 63 | Expiration: expiration, 64 | } 65 | 66 | // notification payload 67 | p := payload.MDM{Token: *flMagic} 68 | id, err := service.Push(*flToken, headers, p) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | fmt.Printf("notification sent successfuly: id: %s", id) 73 | } 74 | 75 | func envString(key, def string) string { 76 | if env := os.Getenv(key); env != "" { 77 | return env 78 | } 79 | return def 80 | } 81 | 82 | func envBool(key string) bool { 83 | if env := os.Getenv(key); env == "true" { 84 | return true 85 | } 86 | return false 87 | } 88 | -------------------------------------------------------------------------------- /poke/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION="1.0.0" 4 | NAME=poke 5 | OUTPUT=../build 6 | 7 | echo "Building $NAME version $VERSION" 8 | 9 | mkdir -p ${OUTPUT} 10 | 11 | build() { 12 | echo -n "=> $1-$2: " 13 | GOOS=$1 GOARCH=$2 go build -o ${OUTPUT}/$NAME -ldflags "-X main.version=$VERSION -X main.gitHash=`git rev-parse HEAD`" ./${NAME}.go 14 | du -h ${OUTPUT}/${NAME} 15 | } 16 | 17 | build "darwin" "amd64" 18 | -------------------------------------------------------------------------------- /poke/sample_env: -------------------------------------------------------------------------------- 1 | # export from keychain as PKCS#12, not .cer 2 | MDM_PUSH_CERT=/path/to/pushcert.p12 3 | MDM_PUSH_PASS=secret 4 | # defaults to false 5 | MDM_PUSH_PRODUCTION=true 6 | --------------------------------------------------------------------------------