move certhelper into mdmctl (#235)

Add mdmcert subcommand which helps create new MDM Push certificates.
This commit is contained in:
Victor Vrantchan
2017-10-15 14:47:10 -04:00
committed by GitHub
parent df2c83cdf0
commit aacac39945
4 changed files with 488 additions and 5 deletions

View File

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

193
cmd/mdmctl/mdmcert.go Normal file
View File

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

View File

@@ -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 <command> -h for additional usage of each command.

View File

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