mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-10 19:46:07 +08:00
move certhelper into mdmctl (#235)
Add mdmcert subcommand which helps create new MDM Push certificates.
This commit is contained in:
@@ -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
193
cmd/mdmctl/mdmcert.go
Normal 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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
285
crypto/mdmcertutil/certutil.go
Normal file
285
crypto/mdmcertutil/certutil.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user