diff --git a/CHANGELOG.md b/CHANGELOG.md index 69445fe1..a590b775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # TBD +* Incorporated certhelper into mdmctl. * Added ENV variables for sensitive flags: `MICROMDM_APNS_KEY_PASSWORD`,`MICROMDM_API_KEY` * Removed the `-redir-addr` flag. Redirect to HTTPS is only enabled when the 443 port is used. # v1.1.0 June 05 2017 -* Import and sign pkgs, generate appmanifest on import. +* Import and sign pkgs, generate appmanifest on import. * Support syncing devices from DEP when token is added. * Option to include SSL certificates in DEP profile template (-anchor and -use-server-cert) #107 * /push and /v1/commands API endpoints require API authentication #157 diff --git a/cmd/mdmctl/mdmcert.go b/cmd/mdmctl/mdmcert.go new file mode 100644 index 00000000..2ed3a34c --- /dev/null +++ b/cmd/mdmctl/mdmcert.go @@ -0,0 +1,193 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "github.com/micromdm/micromdm/crypto/mdmcertutil" +) + +type mdmcertCommand struct{} + +func (cmd *mdmcertCommand) Usage() error { + const usageText = ` +Create new MDM Push Certificate. +This utility helps obtain a MDM Push Certificate using the Apple Developer MDM CSR option in the enterprise developer portal. + +First you must create a vendor CSR which you will upload to the enterprise developer portal and get a signed MDM Vendor certificate. Use the MDM-CSR option in the dev portal when creating the certificate. +The MDM Vendor certificate is required in order to obtain the MDM push certificate. After you complete the MDM-CSR step, copy the downloaded file to the same folder as the private key. By default this will be +mdm-certificates/ + + mdmctl mdmcert vendor -password=secret -country=US -email=admin@acme.co + +Next, create a push CSR. This step generates a CSR required to get a signed a push certificate. + + mdmctl mdmcert push -password=secret -country=US -email=admin@acme.co + +Once you created the push CSR, you mush sign the push CSR with the MDM Vendor Certificate, and get a push certificate request file. + + mdmctl mdmcert vendor -sign -cert=./mdm-certificates/mdm.cer -password=secret + +Once generated, upload the PushCertificateRequest file to https://identity.apple.com to obtain your MDM Push Certificate. +Use the push private key and the push cert you got from identity.apple.com in your MDM server. + +Commands: + vendor + push +` + fmt.Println(usageText) + return nil + +} + +func (cmd *mdmcertCommand) Run(args []string) error { + if len(args) < 1 { + cmd.Usage() + os.Exit(1) + } + + var run func([]string) error + switch strings.ToLower(args[0]) { + case "vendor": + run = cmd.runVendor + case "push": + run = cmd.runPush + default: + cmd.Usage() + os.Exit(1) + } + + return run(args[1:]) +} + +const ( + pushCSRFilename = "PushCertificateRequest.csr" + pushCertificatePrivateKeyFilename = "PushCertificatePrivateKey.key" + vendorPKeyFilename = "VendorPrivateKey.key" + vendorCSRFilename = "VendorCertificateRequest.csr" + pushRequestFilename = "PushCertificateRequest" + mdmcertdir = "mdm-certificates" +) + +func (cmd *mdmcertCommand) runVendor(args []string) error { + flagset := flag.NewFlagSet("vendor", flag.ExitOnError) + flagset.Usage = usageFor(flagset, "mdmctl mdmcert vendor [flags]") + var ( + flSign = flagset.Bool("sign", false, "Signs a user CSR with the MDM vendor certificate.") + flEmail = flagset.String("email", "", "Email address to use in CSR Subject.") + flCountry = flagset.String("country", "US", "Two letter country code for the CSR Subject(example: US).") + flCN = flagset.String("cn", "micromdm-vendor", "CommonName for the CSR Subject.") + flPKeyPass = flagset.String("password", "", "Password to encrypt/read the RSA key.") + flVendorcertPath = flagset.String("cert", filepath.Join(mdmcertdir, "mdm.cer"), "Path to the MDM Vendor certificate from dev portal.") + flPushCSRPath = flagset.String("push-csr", filepath.Join(mdmcertdir, pushCSRFilename), "Path to the user CSR(required for the -sign step).") + flKeyPath = flagset.String("private-key", filepath.Join(mdmcertdir, vendorPKeyFilename), "Path to the vendor private key. A new RSA key will be created at this path.") + flCSRPath = flagset.String("out", filepath.Join(mdmcertdir, vendorCSRFilename), "Path to save the MDM Vendor CSR.") + ) + + if err := flagset.Parse(args); err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(*flCSRPath), 0755); err != nil { + errors.Wrapf(err, "create directory %s", filepath.Dir(*flCSRPath)) + } + + password := []byte(*flPKeyPass) + if *flSign { + request, err := mdmcertutil.CreatePushCertificateRequest( + *flVendorcertPath, + *flPushCSRPath, + *flKeyPath, + password, + ) + if err != nil { + return errors.Wrap(err, "signing push certificate request with vendor private key") + } + encoded, err := request.Encode() + if err != nil { + return errors.Wrap(err, "encode base64 push certificate request") + } + err = ioutil.WriteFile(filepath.Join(mdmcertdir, pushRequestFilename), encoded, 0600) + return errors.Wrap(err, "write PushCertificateRequest to file file") + } + + if err := checkCSRFlags(*flCN, *flCountry, *flEmail, password); err != nil { + return errors.Wrap(err, "Private key password, CN, Email, and country code must be specified when creating a CSR.") + } + + request := &mdmcertutil.CSRConfig{ + CommonName: *flCN, + Country: *flCountry, + Email: *flEmail, + PrivateKeyPassword: password, + PrivateKeyPath: *flKeyPath, + CSRPath: *flCSRPath, + } + + err := mdmcertutil.CreateCSR(request) + return errors.Wrap(err, "creating MDM vendor CSR") +} + +func (cmd *mdmcertCommand) runPush(args []string) error { + flagset := flag.NewFlagSet("push", flag.ExitOnError) + flagset.Usage = usageFor(flagset, "mdmctl mdmcert push [flags]") + var ( + flEmail = flagset.String("email", "", "Email address to use in CSR Subject.") + flCountry = flagset.String("country", "US", "Two letter country code for the CSR Subject(Example: US).") + flCN = flagset.String("cn", "micromdm-user", "CommonName for the CSR Subject.") + flPKeyPass = flagset.String("password", "", "Password to encrypt/read the RSA key.") + flKeyPath = flagset.String("private-key", filepath.Join(mdmcertdir, pushCertificatePrivateKeyFilename), "Path to the push certificate private key. A new RSA key will be created at this path.") + + flCSRPath = flagset.String("out", filepath.Join(mdmcertdir, pushCSRFilename), "Path to save the MDM Push Certificate request.") + ) + + if err := flagset.Parse(args); err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(*flCSRPath), 0755); err != nil { + errors.Wrapf(err, "create directory %s", filepath.Dir(*flCSRPath)) + } + + password := []byte(*flPKeyPass) + if err := checkCSRFlags(*flCN, *flCountry, *flEmail, password); err != nil { + return errors.Wrap(err, "Private key password, CN, Email, and country code must be specified when creating a CSR.") + } + + request := &mdmcertutil.CSRConfig{ + CommonName: *flCN, + Country: *flCountry, + Email: *flEmail, + PrivateKeyPassword: password, + PrivateKeyPath: *flKeyPath, + CSRPath: *flCSRPath, + } + + err := mdmcertutil.CreateCSR(request) + return errors.Wrap(err, "creating MDM Push certificate request.") +} + +func checkCSRFlags(cname, country, email string, password []byte) error { + if cname == "" { + return errors.New("cn flag not specified") + } + if email == "" { + return errors.New("email flag not specified") + } + if country == "" { + return errors.New("country flag not specified") + } + if len(password) == 0 { + return errors.New("private key password empty") + } + if len(country) != 2 { + return errors.New("must be a two letter country code") + } + return nil +} diff --git a/cmd/mdmctl/mdmdctl.go b/cmd/mdmctl/mdmdctl.go index c6be1458..8a335261 100644 --- a/cmd/mdmctl/mdmdctl.go +++ b/cmd/mdmctl/mdmdctl.go @@ -21,16 +21,19 @@ func main() { version.Print() return case "config": - cmd := &configCommand{} + cmd := new(configCommand) run = cmd.Run case "get": - cmd := &getCommand{} + cmd := new(getCommand) run = cmd.Run case "apply": - cmd := &applyCommand{} + cmd := new(applyCommand) run = cmd.Run case "remove": - cmd := &removeCommand{} + cmd := new(removeCommand) + run = cmd.Run + case "mdmcert": + cmd := new(mdmcertCommand) run = cmd.Run default: usage() @@ -51,6 +54,7 @@ Available Commands: apply config remove + mdmcert version Use micromdm -h for additional usage of each command. diff --git a/crypto/mdmcertutil/certutil.go b/crypto/mdmcertutil/certutil.go new file mode 100644 index 00000000..d1782a96 --- /dev/null +++ b/crypto/mdmcertutil/certutil.go @@ -0,0 +1,285 @@ +// Package mdmcertutil contains helpers for requesting MDM Push Certifificates. +// The process is described by Apple at +// https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/7-MDMVendorCSRSigningOverview/MDMVendorCSRSigningOverview.html#//apple_ref/doc/uid/TP40017387-CH6-SW4 +package mdmcertutil + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/groob/plist" + "github.com/pkg/errors" +) + +// CSRConfig defines arguments required to create a new CSR. +type CSRConfig struct { + CommonName, Country, Email string + PrivateKeyPassword []byte + PrivateKeyPath, CSRPath string +} + +// CreateCSR creates a new private key and CSR, saving both as PEM encoded files. +func CreateCSR(req *CSRConfig) error { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + pemKey, err := encryptedKey(key, req.PrivateKeyPassword) + if err != nil { + return err + } + + if err := ioutil.WriteFile(req.PrivateKeyPath, pemKey, 0600); err != nil { + return err + } + + derBytes, err := newCSR(key, strings.ToLower(req.Email), strings.ToUpper(req.Country), req.CommonName) + if err != nil { + return err + } + pemCSR := pemCSR(derBytes) + return ioutil.WriteFile(req.CSRPath, pemCSR, 0600) +} + +// The PushCertificateRequest structure required by identity.apple.com +// to create an MDM Push certificate. +type PushCertificateRequest struct { + PushCertRequestCSR string + PushCertCertificateChain string + PushCertSignature string +} + +// Encode marshals a PushCertificateRequest to an XML Plist file and returns a base64 encoded byte representation +// of the request. +func (p *PushCertificateRequest) Encode() ([]byte, error) { + data, err := plist.MarshalIndent(p, " ") + if err != nil { + return nil, errors.Wrap(err, "marshal PushCertificateRequest") + } + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data))) + base64.StdEncoding.Encode(encoded, data) + return encoded, nil +} + +const ( + wwdrIntermediaryURL = "https://developer.apple.com/certificationauthority/AppleWWDRCA.cer" + appleRootCAURL = "http://www.apple.com/appleca/AppleIncRootCertificate.cer" +) + +// CreatePushCertificateRequest creates a request structure required by identity.apple.com. +// It requires a "MDM CSR" certificate (the vendor certificate), a push CSR (the customer specific CSR), +// and the vendor private key. +func CreatePushCertificateRequest(mdmCertPath, pushCSRPath, pKeyPath string, pKeyPass []byte) (*PushCertificateRequest, error) { + // private key of the mdm vendor cert + key, err := loadKeyFromFile(pKeyPath, pKeyPass) + if err != nil { + return nil, errors.Wrapf(err, "load private key from %s", pKeyPath) + } + + // push csr + csr, err := loadCSRfromFile(pushCSRPath) + if err != nil { + return nil, errors.Wrapf(err, "load push CSR from %s", pushCSRPath) + } + + // csr signature + signature, err := signPushCSR(csr.Raw, key) + if err != nil { + return nil, errors.Wrapf(err, "sign push CSR with private key") + } + + // vendor cert + mdmCertBytes, err := loadDERCertFromFile(mdmCertPath) + if err != nil { + return nil, errors.Wrapf(err, "load vendor certificate from path %s", mdmCertPath) + } + mdmPEM := pemCert(mdmCertBytes) + + // wwdr cert + wwdrCertBytes, err := loadCertfromHTTP(wwdrIntermediaryURL) + if err != nil { + return nil, errors.Wrapf(err, "load WWDR certificate from %s", wwdrIntermediaryURL) + } + wwdrPEM := pemCert(wwdrCertBytes) + + // apple root certificate + rootCertBytes, err := loadCertfromHTTP(appleRootCAURL) + if err != nil { + return nil, errors.Wrapf(err, "load root certificate from %s", appleRootCAURL) + } + rootPEM := pemCert(rootCertBytes) + + csrB64 := base64.StdEncoding.EncodeToString(csr.Raw) + sig64 := base64.StdEncoding.EncodeToString(signature) + pushReq := &PushCertificateRequest{ + PushCertRequestCSR: csrB64, + PushCertCertificateChain: makeCertChain(mdmPEM, wwdrPEM, rootPEM), + PushCertSignature: sig64, + } + return pushReq, nil +} + +func makeCertChain(mdmPEM, wwdrPEM, rootPEM []byte) string { + return string(mdmPEM) + string(wwdrPEM) + string(rootPEM) +} + +func signPushCSR(csrData []byte, key *rsa.PrivateKey) ([]byte, error) { + h := sha1.New() + h.Write(csrData) + signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA1, h.Sum(nil)) + return signature, errors.Wrap(err, "signing push CSR") +} + +const ( + csrPEMBlockType = "CERTIFICATE REQUEST" +) + +// create a CSR using the same parameters as Keychain Access would produce +func newCSR(priv *rsa.PrivateKey, email, country, cname string) ([]byte, error) { + subj := pkix.Name{ + Country: []string{country}, + CommonName: cname, + ExtraNames: []pkix.AttributeTypeAndValue{pkix.AttributeTypeAndValue{ + Type: []int{1, 2, 840, 113549, 1, 9, 1}, + Value: email, + }}, + } + template := &x509.CertificateRequest{ + Subject: subj, + } + return x509.CreateCertificateRequest(rand.Reader, template, priv) +} + +// convert DER to PEM format +func pemCSR(derBytes []byte) []byte { + pemBlock := &pem.Block{ + Type: csrPEMBlockType, + Headers: nil, + Bytes: derBytes, + } + out := pem.EncodeToMemory(pemBlock) + return out +} + +// load PEM encoded CSR from file +func loadCSRfromFile(path string) (*x509.CertificateRequest, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + pemBlock, _ := pem.Decode(data) + if pemBlock == nil { + return nil, errors.New("cannot find the next PEM formatted block") + } + if pemBlock.Type != csrPEMBlockType || len(pemBlock.Headers) != 0 { + return nil, errors.New("unmatched type or headers") + } + return x509.ParseCertificateRequest(pemBlock.Bytes) +} + +const ( + rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" +) + +// protect an rsa key with a password +func encryptedKey(key *rsa.PrivateKey, password []byte) ([]byte, error) { + privBytes := x509.MarshalPKCS1PrivateKey(key) + privPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, rsaPrivateKeyPEMBlockType, privBytes, password, x509.PEMCipher3DES) + if err != nil { + return nil, err + } + + out := pem.EncodeToMemory(privPEMBlock) + return out, nil +} + +// load an encrypted private key from disk +func loadKeyFromFile(path string, password []byte) (*rsa.PrivateKey, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + pemBlock, _ := pem.Decode(data) + if pemBlock == nil { + return nil, errors.New("PEM decode failed") + } + if pemBlock.Type != rsaPrivateKeyPEMBlockType { + return nil, errors.New("unmatched type or headers") + } + + if string(password) != "" { + b, err := x509.DecryptPEMBlock(pemBlock, password) + if err != nil { + return nil, err + } + return x509.ParsePKCS1PrivateKey(b) + } + return x509.ParsePKCS1PrivateKey(pemBlock.Bytes) +} + +const ( + certificatePEMBlockType = "CERTIFICATE" +) + +func pemCert(derBytes []byte) []byte { + pemBlock := &pem.Block{ + Type: certificatePEMBlockType, + Headers: nil, + Bytes: derBytes, + } + out := pem.EncodeToMemory(pemBlock) + return out +} + +func loadDERCertFromFile(path string) ([]byte, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + crt, err := x509.ParseCertificate(data) + if err != nil { + return nil, err + } + return crt.Raw, nil +} + +func loadCertfromHTTP(url string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, errors.Wrapf(err, "create GET request for %s", url) + } + req.Header.Set("Accept", "*/*") // required by Apple at some point. + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "GET request to %s", url) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("got %s when trying to http.Get %s", resp.Status, url) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "reading get certificate request response body") + } + + crt, err := x509.ParseCertificate(data) + if err != nil { + return nil, errors.Wrap(err, "parse wwdr intermediate certificate") + } + return crt.Raw, nil +}