Move dep-token sub-command to mdmctl (#141)

This commit is contained in:
Jesse Peterson
2017-04-19 09:13:27 -07:00
committed by GitHub
parent a40582af11
commit ad22a7a932
14 changed files with 560 additions and 330 deletions

View File

@@ -1,8 +1,11 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
@@ -42,6 +45,8 @@ func (cmd *applyCommand) Run(args []string) error {
switch strings.ToLower(args[0]) {
case "blueprints":
run = cmd.applyBlueprint
case "dep-tokens":
run = cmd.applyDEPTokens
default:
cmd.Usage()
os.Exit(1)
@@ -56,6 +61,7 @@ Apply a resource.
Valid resource types:
* blueprints
* dep-tokens
Examples:
# Get a list of devices
@@ -74,3 +80,31 @@ func (cmd *applyCommand) applyBlueprint(args []string) error {
}
return nil
}
func (cmd *applyCommand) applyDEPTokens(args []string) error {
flagset := flag.NewFlagSet("dep-tokens", flag.ExitOnError)
var (
flPublicKeyPath = flagset.String("import-token", "", "filename of p7m encrypted token file (downloaded from DEP portal)")
)
flagset.Usage = usageFor(flagset, "mdmctl apply dep-tokens [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
if *flPublicKeyPath == "" {
return errors.New("must provide -import-token parameter")
}
if _, err := os.Stat(*flPublicKeyPath); os.IsNotExist(err) {
return err
}
p7mBytes, err := ioutil.ReadFile(*flPublicKeyPath)
if err != nil {
return err
}
ctx := context.Background()
err = cmd.applysvc.ApplyDEPToken(ctx, p7mBytes)
if err != nil {
return err
}
fmt.Println("imported DEP token")
return nil
}

View File

@@ -2,12 +2,16 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
"crypto/x509"
"encoding/pem"
"github.com/go-kit/kit/log"
"github.com/micromdm/micromdm/core/list"
)
@@ -46,6 +50,8 @@ func (cmd *getCommand) Run(args []string) error {
switch strings.ToLower(args[0]) {
case "devices":
run = cmd.getDevices
case "dep-tokens":
run = cmd.getDepTokens
default:
cmd.Usage()
os.Exit(1)
@@ -62,6 +68,7 @@ Valid resource types:
* devices
* blueprints
* dep-tokens
Examples:
# Get a list of devices
@@ -104,3 +111,84 @@ func (cmd *getCommand) getDevices(args []string) error {
}
return nil
}
func (cmd *getCommand) getDepTokens(args []string) error {
flagset := flag.NewFlagSet("dep-tokens", flag.ExitOnError)
var (
flFullCK = flagset.Bool("v", false, "display full ConsumerKey in summary list")
flPublicKeyPath = flagset.String("export-public-key", "", "filename of public key to write (to be uploaded to deploy.apple.com)")
flTokenPath = flagset.String("export-token", "", "filename to save decrypted oauth token (JSON)")
)
flagset.Usage = usageFor(flagset, "mdmctl get dep-tokens [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintf(w, "ConsumerKey\tAccessTokenExpiry\n")
ctx := context.Background()
tokens, certBytes, err := cmd.list.GetDEPTokens(ctx)
if err != nil {
return err
}
var ckTrimmed string
for _, t := range tokens {
if len(t.ConsumerKey) > 40 && !*flFullCK {
ckTrimmed = t.ConsumerKey[0:39] + "…"
} else {
ckTrimmed = t.ConsumerKey
}
fmt.Fprintf(w, "%s\t%s\n", ckTrimmed, t.AccessTokenExpiry.String())
}
w.Flush()
if *flPublicKeyPath != "" && certBytes != nil {
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return err
}
err = WritePEMCertificateFile(cert, *flPublicKeyPath)
if err != nil {
return err
}
fmt.Printf("\nWrote DEP public key to: %s\n", *flPublicKeyPath)
}
if *flTokenPath != "" && len(tokens) > 0 {
t := tokens[0]
tokenFile, err := os.Create(*flTokenPath)
if err != nil {
return err
}
defer tokenFile.Close()
err = json.NewEncoder(tokenFile).Encode(t)
if err != nil {
return err
}
fmt.Printf("\nWrote DEP token JSON to: %s\n", *flTokenPath)
if len(tokens) > 1 {
fmt.Println("WARNING: more than one DEP token returned; only saved first")
}
}
return nil
}
// TODO: move into crypto package and use for all main.savePEMCert() invocations
func WritePEMCertificateFile(cert *x509.Certificate, path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return pem.Encode(
file,
&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
}

View File

@@ -27,6 +27,8 @@ func main() {
cmd := &getCommand{}
run = cmd.Run
case "apply":
cmd := &applyCommand{}
run = cmd.Run
default:
usage()
os.Exit(1)
@@ -43,6 +45,7 @@ func usage() error {
Available Commands:
get
apply
config
version

View File

@@ -25,9 +25,19 @@ func NewClient(instance string, logger log.Logger, token string) (Service, error
DecodeBlueprintRequest,
).Endpoint()
}
var applyDEPTokensEndpoint endpoint.Endpoint
{
applyDEPTokensEndpoint = httptransport.NewClient(
"PUT",
copyURL(u, "/v1/dep-tokens"),
encodeRequestWithToken(token, EncodeHTTPGenericRequest),
DecodeDEPTokensRequest,
).Endpoint()
}
return Endpoints{
ApplyBlueprintEndpoint: applyBlueprintEndpoint,
ApplyDEPTokensEndpoint: applyDEPTokensEndpoint,
}, nil
}

View File

@@ -9,6 +9,7 @@ import (
type Endpoints struct {
ApplyBlueprintEndpoint endpoint.Endpoint
ApplyDEPTokensEndpoint endpoint.Endpoint
}
func (e Endpoints) ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error {
@@ -20,6 +21,15 @@ func (e Endpoints) ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint)
return resp.(blueprintResponse).Err
}
func (e Endpoints) ApplyDEPToken(ctx context.Context, P7MContent []byte) error {
req := depTokensRequest{P7MContent: P7MContent}
resp, err := e.ApplyDEPTokensEndpoint(ctx, req)
if err != nil {
return err
}
return resp.(depTokensResponse).Err
}
func MakeApplyBlueprintEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(blueprintRequest)
@@ -30,6 +40,16 @@ func MakeApplyBlueprintEndpoint(svc Service) endpoint.Endpoint {
}
}
func MakeApplyDEPTokensEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(depTokensRequest)
err = svc.ApplyDEPToken(ctx, req.P7MContent)
return depTokensResponse{
Err: err,
}, nil
}
}
type blueprintRequest struct {
Blueprint *blueprint.Blueprint `json:"blueprint"`
}
@@ -37,3 +57,13 @@ type blueprintRequest struct {
type blueprintResponse struct {
Err error `json:"err,omitempty"`
}
type depTokensRequest struct {
P7MContent []byte `json:"p7m_content"`
}
type depTokensResponse struct {
Err error `json:"err,omitempty"`
}
func (r depTokensResponse) error() error { return r.Err }

View File

@@ -2,18 +2,116 @@ package apply
import (
"context"
"fmt"
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"github.com/fullsailor/pkcs7"
"io"
"net/textproto"
"github.com/boltdb/bolt"
"github.com/micromdm/micromdm/blueprint"
"github.com/micromdm/micromdm/core/list"
)
type Service interface {
ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error
ApplyDEPToken(ctx context.Context, P7MContent []byte) error
}
type ApplyService struct {
Blueprints *blueprint.DB
DB *bolt.DB // TODO: replace with reference to DEP token svc/pkg
}
func (svc *ApplyService) ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error {
return svc.Blueprints.Save(bp)
}
// unwrapSMIME removes the S/MIME-like wrapper around raw CMS/PKCS7 data
func unwrapSMIME(smime []byte) ([]byte, error) {
tr := textproto.NewReader(bufio.NewReader(bytes.NewReader(smime)))
if _, err := tr.ReadMIMEHeader(); err != nil {
return nil, err
}
dec := base64.NewDecoder(base64.StdEncoding, tr.DotReader())
buf := new(bytes.Buffer)
io.Copy(buf, dec)
return buf.Bytes(), nil
}
// unwrapTokenJSON removes the MIME-like headers and text surrounding the DEP token JSON
func unwrapTokenJSON(wrapped []byte) ([]byte, error) {
tr := textproto.NewReader(bufio.NewReader(bytes.NewReader(wrapped)))
if _, err := tr.ReadMIMEHeader(); err != nil {
return nil, err
}
tokenJSON := new(bytes.Buffer)
for {
line, err := tr.ReadLineBytes()
if err != nil && err == io.EOF {
break
} else if err != nil {
return nil, err
}
line = bytes.Trim(line, "-----BEGIN MESSAGE-----")
line = bytes.Trim(line, "-----END MESSAGE-----")
if _, err := tokenJSON.Write(line); err != nil {
return nil, err
}
}
return tokenJSON.Bytes(), nil
}
// TODO: move into seperate svc/pkg
const (
depTokenBucket = "mdm.DEPToken"
)
func PutDEPToken(db *bolt.DB, consumerKey string, json []byte) error {
err := db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(depTokenBucket))
if err != nil {
return err
}
return b.Put([]byte(consumerKey), json)
})
return err
}
func (svc *ApplyService) ApplyDEPToken(ctx context.Context, P7MContent []byte) error {
unwrapped, err := unwrapSMIME(P7MContent)
if err != nil {
return err
}
key, cert, err := list.GetDEPKeypair(svc.DB)
if err != nil {
return err
}
p7, err := pkcs7.Parse(unwrapped)
if err != nil {
return err
}
decrypted, err := p7.Decrypt(cert, key)
if err != nil {
return err
}
tokenJSON, err := unwrapTokenJSON(decrypted)
if err != nil {
return err
}
var depToken list.DEPToken
err = json.Unmarshal(tokenJSON, &depToken)
if err != nil {
return err
}
err = PutDEPToken(svc.DB, depToken.ConsumerKey, tokenJSON)
if err != nil {
return err
}
fmt.Println("stored DEP token with ck", depToken.ConsumerKey)
return nil
}

View File

@@ -12,17 +12,37 @@ import (
"github.com/micromdm/micromdm/blueprint"
)
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) http.Handler {
var h http.Handler
h = httptransport.NewServer(
endpoints.ApplyBlueprintEndpoint,
decodeBlueprintRequest,
encodeResponse,
opts...,
)
type HTTPHandlers struct {
BlueprintHandler http.Handler
DEPTokensHandler http.Handler
}
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
h := HTTPHandlers{
BlueprintHandler: httptransport.NewServer(
endpoints.ApplyBlueprintEndpoint,
decodeBlueprintRequest,
encodeResponse,
opts...,
),
DEPTokensHandler: httptransport.NewServer(
endpoints.ApplyDEPTokensEndpoint,
decodeDEPTokensRequest,
encodeResponse,
opts...,
),
}
return h
}
func decodeDEPTokensRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req depTokensRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}
func decodeBlueprintRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var bp blueprint.Blueprint
if err := json.NewDecoder(r.Body).Decode(&bp); err != nil {
@@ -62,7 +82,10 @@ func encodeResponse(ctx context.Context, w http.ResponseWriter, response interfa
}
func EncodeError(ctx context.Context, err error, w http.ResponseWriter) {
http.Error(w, err.Error(), http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(errorWrapper{Error: err.Error()})
}
// EncodeHTTPGenericRequest is a transport/http.EncodeRequestFunc that
@@ -84,3 +107,12 @@ func DecodeBlueprintRequest(_ context.Context, r *http.Response) (interface{}, e
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}
func DecodeDEPTokensRequest(_ context.Context, r *http.Response) (interface{}, error) {
if r.StatusCode != http.StatusOK {
return nil, errorDecoder(r)
}
var resp depTokensResponse
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}

View File

@@ -25,9 +25,19 @@ func NewClient(instance string, logger log.Logger, token string) (Service, error
DecodeDevicesResponse,
).Endpoint()
}
var getDEPTokensEndpoint endpoint.Endpoint
{
getDEPTokensEndpoint = httptransport.NewClient(
"GET",
copyURL(u, "/v1/dep-tokens"),
encodeRequestWithToken(token, EncodeHTTPGenericRequest),
DecodeGetDEPTokensResponse,
).Endpoint()
}
return Endpoints{
ListDevicesEndpoint: listDevicesEndpoint,
ListDevicesEndpoint: listDevicesEndpoint,
GetDEPTokensEndpoint: getDEPTokensEndpoint,
}, nil
}

View File

@@ -8,7 +8,8 @@ import (
)
type Endpoints struct {
ListDevicesEndpoint endpoint.Endpoint
ListDevicesEndpoint endpoint.Endpoint
GetDEPTokensEndpoint endpoint.Endpoint
}
func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]DeviceDTO, error) {
@@ -20,6 +21,14 @@ func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]D
return response.(devicesResponse).Devices, response.(devicesResponse).Err
}
func (e Endpoints) GetDEPTokens(ctx context.Context) ([]DEPToken, []byte, error) {
resp, err := e.GetDEPTokensEndpoint(ctx, nil)
if err != nil {
return nil, nil, err
}
return resp.(depTokenResponse).DEPTokens, resp.(depTokenResponse).DEPPubKey, nil
}
func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(devicesRequest)
@@ -31,6 +40,17 @@ func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint {
}
}
func MakeGetDEPTokensEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
tokens, pubkey, err := svc.GetDEPTokens(ctx)
return depTokenResponse{
DEPTokens: tokens,
DEPPubKey: pubkey,
Err: err,
}, nil
}
}
type DeviceDTO struct {
SerialNumber string `json:"serial_number"`
UDID string `json:"udid"`
@@ -43,3 +63,17 @@ type devicesResponse struct {
Devices []DeviceDTO `json:"devices"`
Err error `json:"err,omitempty"`
}
type DEPToken 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"`
}
type depTokenResponse struct {
DEPTokens []DEPToken `json:"dep_tokens"`
DEPPubKey []byte `json:"public_key"`
Err error `json:"err,omitempty"`
}

View File

@@ -1,7 +1,18 @@
package list
import (
"bytes"
"context"
"encoding/json"
"time"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"github.com/boltdb/bolt"
"github.com/micromdm/micromdm/device"
)
@@ -16,10 +27,12 @@ type ListDevicesOption struct {
type Service interface {
ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error)
GetDEPTokens(ctx context.Context) ([]DEPToken, []byte, error)
}
type ListService struct {
Devices *device.DB
DB *bolt.DB // TODO: replace with reference to DEP token svc/pkg
}
func (svc *ListService) ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) {
@@ -35,3 +48,142 @@ func (svc *ListService) ListDevices(ctx context.Context, opt ListDevicesOption)
}
return dto, err
}
// TODO: move into seperate svc/pkg
const (
depTokenBucket = "mdm.DEPToken"
)
// TODO: move into seperate svc/pkg
func GetDEPTokens(db *bolt.DB) ([]DEPToken, error) {
var result []DEPToken
err := db.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte(depTokenBucket)).Cursor()
prefix := []byte("CK_")
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
var depToken DEPToken
err := json.Unmarshal(v, &depToken)
if err != nil {
// TODO: log problematic DEP token, or remove altogether?
continue
}
result = append(result, depToken)
}
return nil
})
return result, err
}
// TODO: move into seperate svc/pkg
func generateAndStoreDEPKeypair(db *bolt.DB) (key *rsa.PrivateKey, cert *x509.Certificate, err error) {
key, cert, err = SimpleSelfSignedRSAKeypair("micromdm-dep-token", 365)
if err != nil {
return
}
pkBytes := x509.MarshalPKCS1PrivateKey(key)
certBytes := cert.Raw
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(depTokenBucket))
err := b.Put([]byte("key"), pkBytes)
if err != nil {
return err
}
err = b.Put([]byte("certificate"), certBytes)
if err != nil {
return err
}
return nil
})
return
}
// TODO: move into seperate svc/pkg
func GetDEPKeypair(db *bolt.DB) (key *rsa.PrivateKey, cert *x509.Certificate, err error) {
var keyBytes, certBytes []byte
err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(depTokenBucket))
keyBytes = b.Get([]byte("key"))
certBytes = b.Get([]byte("certificate"))
return nil
})
if err != nil {
return
}
if keyBytes == nil || certBytes == nil {
// if there is no certificate or private key then generate
key, cert, err = generateAndStoreDEPKeypair(db)
} else {
key, err = x509.ParsePKCS1PrivateKey(keyBytes)
if err != nil {
return
}
cert, err = x509.ParseCertificate(certBytes)
if err != nil {
return
}
}
return
}
func (svc *ListService) GetDEPTokens(ctx context.Context) ([]DEPToken, []byte, error) {
_, cert, err := GetDEPKeypair(svc.DB)
if err != nil {
return nil, nil, err
}
var certBytes []byte
if cert != nil {
certBytes = cert.Raw
}
tokens, err := GetDEPTokens(svc.DB)
if err != nil {
return nil, certBytes, err
}
return tokens, certBytes, nil
}
// TODO: move into crypto package
func RandomCertificateSerialNumber() (*big.Int, error) {
limit := new(big.Int).Lsh(big.NewInt(1), 128)
return rand.Int(rand.Reader, limit)
}
// TODO: move into crypto package
func SimpleSelfSignedRSAKeypair(cn string, days int) (key *rsa.PrivateKey, cert *x509.Certificate, err error) {
key, err = rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return
}
serialNumber, err := RandomCertificateSerialNumber()
if err != nil {
return
}
timeNow := time.Now()
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: cn,
},
NotBefore: timeNow,
NotAfter: timeNow.Add(time.Duration(days) * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return
}
cert, err = x509.ParseCertificate(certBytes)
if err != nil {
return
}
return
}

View File

@@ -11,17 +11,32 @@ import (
httptransport "github.com/go-kit/kit/transport/http"
)
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) http.Handler {
var h http.Handler
h = httptransport.NewServer(
endpoints.ListDevicesEndpoint,
decodeListDevicesRequest,
encodeResponse,
opts...,
)
type HTTPHandlers struct {
ListDevicesHandler http.Handler
GetDEPTokensHandler http.Handler
}
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
h := HTTPHandlers{
ListDevicesHandler: httptransport.NewServer(
endpoints.ListDevicesEndpoint,
decodeListDevicesRequest,
encodeResponse,
opts...,
),
GetDEPTokensHandler: httptransport.NewServer(
endpoints.GetDEPTokensEndpoint,
decodeGetDEPTokensRequest,
encodeResponse,
opts...),
}
return h
}
func decodeGetDEPTokensRequest(ctx context.Context, r *http.Request) (interface{}, error) {
return nil, nil
}
func decodeListDevicesRequest(ctx context.Context, r *http.Request) (interface{}, error) {
req := devicesRequest{
Opts: ListDevicesOption{},
@@ -79,3 +94,12 @@ func DecodeDevicesResponse(_ context.Context, r *http.Response) (interface{}, er
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}
func DecodeGetDEPTokensResponse(_ context.Context, r *http.Response) (interface{}, error) {
if r.StatusCode != http.StatusOK {
return nil, errorDecoder(r)
}
var resp depTokenResponse
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}

View File

@@ -1,274 +0,0 @@
package main
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"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"
"github.com/pkg/errors"
)
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("dep-token", flag.ExitOnError)
var (
flPublicKey = flagset.String("export-public-key", "", "filename of public key to write (to be uploaded to deploy.apple.com)")
flImportToken = flagset.String("import-token", "", "filename of p7m encrypted token file (downloaded from DEP portal)")
flExportToken = flagset.String("export-token", "", "filename to save decrypted oauth token JSON")
)
flagset.Usage = usageFor(flagset, "micromdm dep-token [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
}
err = savePEMKey(keyPath, pk)
if err != nil {
return err
}
} 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
}
}
if *flPublicKey == "" && *flImportToken == "" && *flExportToken == "" {
flagset.Usage()
return nil
}
if *flPublicKey != "" {
if _, err := os.Stat(certPath); os.IsExist(err) {
return errors.New("public key filename already exists, please choose another")
}
certOut, err := os.Create(*flPublicKey)
if err != nil {
return err
}
defer certOut.Close()
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
return err
}
fmt.Println("wrote", *flPublicKey)
}
if *flImportToken != "" {
f, err := os.Open(*flImportToken)
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
}
tokenJSON := new(bytes.Buffer)
for {
line, err := tr.ReadLineBytes()
if err != nil && err == io.EOF {
break
} else if err != nil {
return err
}
line = bytes.Trim(line, "-----BEGIN MESSAGE-----")
line = bytes.Trim(line, "-----END MESSAGE-----")
if _, err := tokenJSON.Write(line); err != nil {
return err
}
}
var depToken DEPTokenJSON
err = json.Unmarshal(tokenJSON.Bytes(), &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()
if sm.err != nil {
return sm.err
}
err = sm.db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(depTokenBucket))
if err != nil {
return err
}
return b.Put([]byte(depConfig.ConsumerKey), tokenJSON.Bytes())
})
if err != nil {
return err
}
fmt.Println("saved token", depConfig.ConsumerKey)
}
if *flExportToken != "" {
sm := &config{}
sm.setupBolt()
if sm.err != nil {
return sm.err
}
err := sm.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(depTokenBucket))
if b == nil {
fmt.Println("no DEP server token found. using depsim")
return nil
}
_, v := b.Cursor().First()
if v == nil {
return errors.New("no dep token found. did you import it?")
}
f, err := os.Create(*flExportToken)
if err != nil {
return errors.Wrap(err, "create file to save DEP token")
}
defer f.Close()
if _, err := f.Write(v); err != nil {
return errors.Wrap(err, "saving DEP token JSON")
}
return nil
})
if err != nil {
return err
}
fmt.Println("saved oauth token file")
}
return nil
}

View File

@@ -28,8 +28,6 @@ func main() {
return
case "serve":
run = serve
case "dep-token":
run = depToken
case "get":
run = getResource
case "apply":
@@ -55,7 +53,6 @@ Available Commands:
dev
get
apply
dep-token
version
Use micromdm <command> -h for additional usage of each command.

View File

@@ -8,7 +8,6 @@ import (
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
@@ -193,7 +192,7 @@ func serve(args []string) error {
var listsvc list.Service
{
listsvc = &list.ListService{Devices: devDB}
listsvc = &list.ListService{Devices: devDB, DB: sm.db}
}
var listDevicesEndpoint endpoint.Endpoint
{
@@ -201,12 +200,13 @@ func serve(args []string) error {
}
listEndpoints := list.Endpoints{
ListDevicesEndpoint: listDevicesEndpoint,
ListDevicesEndpoint: listDevicesEndpoint,
GetDEPTokensEndpoint: list.MakeGetDEPTokensEndpoint(listsvc),
}
var applysvc apply.Service
{
applysvc = &apply.ApplyService{Blueprints: bpDB}
applysvc = &apply.ApplyService{Blueprints: bpDB, DB: sm.db}
}
var applyBlueprintEndpoint endpoint.Endpoint
@@ -216,6 +216,7 @@ func serve(args []string) error {
applyEndpoints := apply.Endpoints{
ApplyBlueprintEndpoint: applyBlueprintEndpoint,
ApplyDEPTokensEndpoint: apply.MakeApplyDEPTokensEndpoint(applysvc),
}
applyAPIHandlers := apply.MakeHTTPHandlers(ctx, applyEndpoints, connectOpts...)
@@ -242,8 +243,10 @@ func serve(args []string) error {
// API commands. Only handled if the user provides an api key.
if *flAPIKey != "" {
r.Handle("/v1/devices", apiAuthMiddleware(*flAPIKey, listAPIHandlers)).Methods("GET")
r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, applyAPIHandlers)).Methods("PUT")
r.Handle("/v1/devices", apiAuthMiddleware(*flAPIKey, listAPIHandlers.ListDevicesHandler)).Methods("GET")
r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPTokensHandler)).Methods("GET")
r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DEPTokensHandler)).Methods("PUT")
r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.BlueprintHandler)).Methods("PUT")
}
if *flRepoPath != "" {
@@ -601,6 +604,22 @@ func (c *config) depClient() (dep.Client, error) {
// depsim config
depsim := c.depsim
var conf *dep.Config
// try getting the oauth config from bolt
tokens, err := list.GetDEPTokens(c.db)
if err != nil {
return nil, err
}
if len(tokens) >= 1 {
conf = new(dep.Config)
conf.ConsumerSecret = tokens[0].ConsumerSecret
conf.ConsumerKey = tokens[0].ConsumerKey
conf.AccessSecret = tokens[0].AccessSecret
conf.AccessToken = tokens[0].AccessToken
// TODO: handle expiration
}
// override with depsim keys if specified on CLI
if depsim {
conf = &dep.Config{
ConsumerKey: "CK_48dd68d198350f51258e885ce9a5c37ab7f98543c4a697323d75682a6c10a32501cb247e3db08105db868f73f2c972bdb6ae77112aea803b9219eb52689d42e6",
@@ -610,40 +629,13 @@ func (c *config) depClient() (dep.Client, error) {
}
}
// try getting the oauth config from bolt
err := c.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(depTokenBucket))
if b == nil {
return nil
}
_, v := b.Cursor().First()
if v == nil {
return nil
}
var token DEPTokenJSON
err := json.Unmarshal(v, &token)
if err != nil {
return err
}
conf = new(dep.Config)
conf.ConsumerSecret = token.ConsumerSecret
conf.ConsumerKey = token.ConsumerKey
conf.AccessSecret = token.AccessSecret
conf.AccessToken = token.AccessToken
// TODO handle expiration.
return nil
})
if err != nil {
return nil, err
}
if conf == nil {
return nil, nil
}
depServerURL := "https://mdmenrollment.apple.com"
if depsim {
// TODO: support supplied depsim URL
depServerURL = "http://dep.micromdm.io:9000"
}
client, err := dep.NewClient(conf, dep.ServerURL(depServerURL))