From 4dcc18907d7c393b3b22f02efd60d020b37a8cc0 Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Sat, 18 Mar 2017 02:18:39 -0700 Subject: [PATCH] Implement deptoken sub-command helper. Generates self-signed cert for working with DEP & decrypts deploy.apple.com tokens. --- deptoken.go | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++ lock.json | 26 +++++- main.go | 3 + 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 deptoken.go diff --git a/deptoken.go b/deptoken.go new file mode 100644 index 00000000..feaa4511 --- /dev/null +++ b/deptoken.go @@ -0,0 +1,246 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "net/textproto" + "os" + "path" + "time" + + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + + "github.com/boltdb/bolt" + "github.com/fullsailor/pkcs7" + "github.com/micromdm/dep" +) + +const ( + depTokenRSAKeyFilename = "deptoken.key" + depTokenCertFilename = "deptoken.pem" + depTokenBucket = "mdm.DEPToken" +) + +type DEPTokenJSON struct { + ConsumerKey string `json:"consumer_key"` + ConsumerSecret string `json:"consumer_secret"` + AccessToken string `json:"access_token"` + AccessSecret string `json:"access_secret"` + AccessTokenExpiry time.Time `json:"access_token_expiry"` +} + +func depToken(args []string) error { + flagset := flag.NewFlagSet("deptoken", flag.ExitOnError) + var ( + flPublicKey = flagset.String("public-key", "", "filename of public key to write (to be uploaded to deploy.apple.com)") + flTokenFile = flagset.String("token", "", "filename of p7 encrypted token file") + ) + flagset.Usage = usageFor(flagset, "micromdm deptoken [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + + keyPath := path.Join(configDBPath, depTokenRSAKeyFilename) + var pk *rsa.PrivateKey + if _, err := os.Stat(keyPath); os.IsNotExist(err) { + // key doesn't yet exist, make it + pk, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + pemBlock := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)} + + certOut, err := os.Create(keyPath) + if err != nil { + return err + } + + pem.Encode(certOut, &pemBlock) + certOut.Close() + + // fmt.Println("generated and saved key", keyPath) + } else { + // key exists, load it + pemKey, err := ioutil.ReadFile(keyPath) + if err != nil { + return err + } + block, _ := pem.Decode(pemKey) + + if block == nil || block.Type != "RSA PRIVATE KEY" { + return errors.New("invalid DEP token private key") + } + + if pk, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + return err + } + + // fmt.Println("loaded key", keyPath) + } + + certPath := path.Join(configDBPath, depTokenCertFilename) + var cert []byte + if _, err := os.Stat(certPath); os.IsNotExist(err) { + // cert doesn't yet exist, make it + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "micromdm-dep-token", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + cert, err := x509.CreateCertificate(rand.Reader, &template, &template, &pk.PublicKey, pk) + if err != nil { + return err + } + + certOut, err := os.Create(certPath) + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) + certOut.Close() + + // fmt.Println("generated and saved cert", certPath) + } else { + // cert exists, load it + pemCert, err := ioutil.ReadFile(certPath) + if err != nil { + return err + } + block, _ := pem.Decode(pemCert) + + if block == nil || block.Type != "CERTIFICATE" { + return errors.New("invalid DEP token cert") + } + + cert = block.Bytes + + if _, err = x509.ParseCertificate(cert); err != nil { + return err + } + + // fmt.Println("loaded cert", keyPath) + } + + if *flPublicKey == "" && *flTokenFile == "" { + flagset.Usage() + return nil + } + + if *flPublicKey != "" { + if _, err := os.Stat(certPath); err == nil { + return errors.New("public key filename already exists, please choose another") + } + certOut, err := os.Create(*flPublicKey) + if err != nil { + return err + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) + certOut.Close() + fmt.Println("wrote", *flPublicKey) + } + + if *flTokenFile != "" { + f, err := os.Open(*flTokenFile) + if err != nil { + return err + } + defer f.Close() + + tr := textproto.NewReader(bufio.NewReader(f)) + if _, err := tr.ReadMIMEHeader(); err != nil { + return err + } + dec := base64.NewDecoder(base64.StdEncoding, tr.DotReader()) + buf := new(bytes.Buffer) + io.Copy(buf, dec) + p7, err := pkcs7.Parse(buf.Bytes()) + if err != nil { + return err + } + + parsedCert, err := x509.ParseCertificate(cert) + if err != nil { + return err + } + + decrypted, err := p7.Decrypt(parsedCert, pk) + if err != nil { + return err + } + + // the contained decrypted data is also wrapped in a textproto-like + // wrapper. strip it, too. + + tr = textproto.NewReader(bufio.NewReader(bytes.NewReader(decrypted))) + if _, err := tr.ReadMIMEHeader(); err != nil { + return err + } + body, err := ioutil.ReadAll(tr.DotReader()) + + // the body part of the textproto is an almost PEM-like structure. + // unpack it as well. + + startFound := bytes.Index(body, []byte("-----BEGIN MESSAGE-----")) + endFound := bytes.Index(body, []byte("-----END MESSAGE-----")) + + if endFound <= startFound { + return errors.New("invalid format in decrypted message") + } + + // finally the JSON for the token values! + + tokenJSON := body[startFound+24 : endFound-1] + + depToken := &DEPTokenJSON{} + + err = json.Unmarshal(tokenJSON, depToken) + if err != nil { + return err + } + + // copy over values + depConfig := &dep.Config{} + depConfig.ConsumerKey = depToken.ConsumerKey + depConfig.ConsumerSecret = depToken.ConsumerSecret + depConfig.AccessToken = depToken.AccessToken + depConfig.AccessSecret = depToken.AccessSecret + + sm := &config{} + sm.setupBolt() + + err = sm.db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte(depTokenBucket)) + if err == nil { + err = b.Put([]byte(depConfig.ConsumerKey), tokenJSON) + } + return err + }) + + if err != nil { + return err + } + + fmt.Println("saved token", depConfig.ConsumerKey) + } + + return nil +} diff --git a/lock.json b/lock.json index ae808643..643d4fd6 100644 --- a/lock.json +++ b/lock.json @@ -1,5 +1,5 @@ { - "memo": "8f28fd12310c838617234aced70466223894111f2be4f1696581595e4403d1ae", + "memo": "8083a2cf13559fb206e86028e580bb739c2704e8e052742970dd35b5424aaeb9", "projects": [ { "name": "github.com/RobotsAndPencils/buford", @@ -19,6 +19,22 @@ "." ] }, + { + "name": "github.com/fullsailor/pkcs7", + "branch": "master", + "revision": "eb67e7e564b9eae64dc7d95fae0784d6086a5fc4", + "packages": [ + "." + ] + }, + { + "name": "github.com/garyburd/go-oauth", + "branch": "master", + "revision": "4f191fd0442e75c0c73bd00e4f5f27d4c08312ee", + "packages": [ + "oauth" + ] + }, { "name": "github.com/go-kit/kit", "version": "v0.4.0", @@ -94,6 +110,14 @@ "." ] }, + { + "name": "github.com/micromdm/dep", + "branch": "master", + "revision": "38c81b11544f31c2be14e0b3bcfa12f656fcc709", + "packages": [ + "." + ] + }, { "name": "github.com/micromdm/mdm", "branch": "master", diff --git a/main.go b/main.go index d22b8ab6..2d4cfce1 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,8 @@ func main() { switch strings.ToLower(os.Args[1]) { case "serve": run = serve + case "deptoken": + run = depToken default: usage() os.Exit(1) @@ -39,6 +41,7 @@ func usage() error { Available Commands: serve + deptoken version Use micromdm -h for additional usage of each command.