mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-08 10:45:34 +08:00
Implement mdmcert.download support in mdmctl (#401)
This commit is contained in:
2
Gopkg.lock
generated
2
Gopkg.lock
generated
@@ -21,7 +21,7 @@
|
||||
branch = "master"
|
||||
name = "github.com/fullsailor/pkcs7"
|
||||
packages = ["."]
|
||||
revision = "a009d8d7de53d9503c797cb8ec66fa3b21eed209"
|
||||
revision = "ae226422660e5ca10db350d33f81c6608f3fbcdd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
||||
284
cmd/mdmctl/mdmcert.download.go
Normal file
284
cmd/mdmctl/mdmcert.download.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/fullsailor/pkcs7"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/micromdm/micromdm/pkg/crypto"
|
||||
"github.com/micromdm/micromdm/pkg/crypto/mdmcertutil"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
mdmcertRequestURL = "https://mdmcert.download/api/v1/signrequest"
|
||||
// see
|
||||
// https://github.com/jessepeterson/commandment/blob/1352b51ba6697260d1111eccc3a5a0b5b9af60d0/commandment/mdmcert.py#L23-L28
|
||||
mdmcertAPIKey = "f847aea2ba06b41264d587b229e2712c89b1490a1208b7ff1aafab5bb40d47bc"
|
||||
)
|
||||
|
||||
// format of a signing request to mdmcert.download
|
||||
type signRequest struct {
|
||||
CSR string `json:"csr"` // base64 encoded PEM CSR
|
||||
Email string `json:"email"`
|
||||
Key string `json:"key"` // server key from above
|
||||
Encrypt string `json:"encrypt"` // mdmcert pki cert
|
||||
}
|
||||
|
||||
type mdmcertDownloadCommand struct {
|
||||
*remoteServices
|
||||
}
|
||||
|
||||
func (cmd *mdmcertDownloadCommand) setup() error {
|
||||
logger := log.NewLogfmtLogger(os.Stderr)
|
||||
remote, err := setupClient(logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.remoteServices = remote
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *mdmcertDownloadCommand) Usage() error {
|
||||
const usageText = `
|
||||
Request new MDM Push Certificate from https://mdmcert.download
|
||||
This utility helps obtain an MDM Push Certificate using the service
|
||||
at mdmcert.download.
|
||||
|
||||
First we'll generate the initial request (which also generates a private key):
|
||||
|
||||
mdmctl mdmcert.download -new -email=cool.mdm.admin@example.org
|
||||
|
||||
This will output the private key into the file mdmcert.download.key.
|
||||
Then, after you check your email and download the request file you just
|
||||
need to decrypt the push certificate request:
|
||||
|
||||
mdmctl mdmcert.download -decrypt=~/Downloads/mdm_signed_request.20171122_094910_220.plist.b64.p7
|
||||
|
||||
This will output the push certificate request to mdmcert.download.req.
|
||||
Upload this file to https://identity.apple.com and download the signed
|
||||
certificate. Then use the 'mdmctl mdmcert upload' command to upload it,
|
||||
(and the above private key) into MicroMDM.
|
||||
|
||||
`
|
||||
fmt.Println(usageText)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (cmd *mdmcertDownloadCommand) Run(args []string) error {
|
||||
flagset := flag.NewFlagSet("mdmcert.download", flag.ExitOnError)
|
||||
flagset.Usage = usageFor(flagset, "mdmctl mdmcert.download [flags]")
|
||||
var (
|
||||
flNew = flagset.Bool("new", false, "Generates a new privkey and uploads new MDM request")
|
||||
flDecrypt = flagset.String("decrypt", "", "Decrypts and mdmcert.download push certificate request")
|
||||
flEmail = flagset.String("email", "", "Email address to use in mdmcert request & CSR Subject")
|
||||
flCountry = flagset.String("country", "US", "Two letter country code for the CSR Subject (example: US).")
|
||||
flCN = flagset.String("cn", "mdm-push", "CommonName for the CSR Subject.")
|
||||
flCertPath = flagset.String("pki-cert", "mdmcert.download.pki.crt", "Path for generated MDMCert pki exchange certificate")
|
||||
flKeyPath = flagset.String("pki-private-key", "mdmcert.download.pki.key", "Path for generated MDMCert pki exchange private key")
|
||||
flPKeyPass = flagset.String("pki-password", "", "Password to encrypt/read the RSA key.")
|
||||
flCCSRPath = flagset.String("push-csr", "mdmcert.download.push.csr", "Path for generated Push Certificate CSR")
|
||||
flCReqPath = flagset.String("push-req", "mdmcert.download.push.req", "Path for generated Push Certificate Request")
|
||||
flCKeyPath = flagset.String("push-private-key", "mdmcert.download.push.key", "Path to the generated Push Cert private key")
|
||||
flCPKeyPass = flagset.String("push-password", "", "Password to encrypt/read the push RSA key.")
|
||||
)
|
||||
|
||||
if err := flagset.Parse(args); err != nil {
|
||||
cmd.Usage()
|
||||
return err
|
||||
}
|
||||
|
||||
// neither flag was used
|
||||
if !*flNew && *flDecrypt == "" {
|
||||
cmd.Usage()
|
||||
return errors.New("bad input: must either use -new or -decrypt")
|
||||
}
|
||||
|
||||
// both flags used
|
||||
if *flNew && (*flDecrypt != "") {
|
||||
// cmd.Usage()
|
||||
return errors.New("bad input: can't use both -new and -decrypt")
|
||||
}
|
||||
|
||||
if *flNew {
|
||||
if *flEmail == "" {
|
||||
return errors.New("bad input: must provide -email")
|
||||
}
|
||||
|
||||
paths := []string{*flCertPath, *flKeyPath, *flCCSRPath, *flCKeyPath}
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return fmt.Errorf("file already exists: %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
pkiKey, pkiCert, err := crypto.SimpleSelfSignedRSAKeypair("mdmcert.download", 365)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create PKI keypair")
|
||||
}
|
||||
|
||||
pemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Headers: nil,
|
||||
Bytes: pkiCert.Raw,
|
||||
}
|
||||
pemPkiCert := pem.EncodeToMemory(pemBlock)
|
||||
|
||||
if err := crypto.WritePEMCertificateFile(pkiCert, *flCertPath); err != nil {
|
||||
return errors.Wrap(err, "could not write PKI cert")
|
||||
}
|
||||
|
||||
if *flPKeyPass != "" {
|
||||
err = crypto.WriteEncryptedPEMRSAKeyFile(pkiKey, []byte(*flPKeyPass), *flKeyPath)
|
||||
} else {
|
||||
err = crypto.WritePEMRSAKeyFile(pkiKey, *flKeyPath)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not write private key")
|
||||
}
|
||||
|
||||
pushKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not generate push private key")
|
||||
}
|
||||
|
||||
if *flCPKeyPass != "" {
|
||||
err = crypto.WriteEncryptedPEMRSAKeyFile(pushKey, []byte(*flCPKeyPass), *flCKeyPath)
|
||||
} else {
|
||||
err = crypto.WritePEMRSAKeyFile(pushKey, *flCKeyPath)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not write push private key")
|
||||
}
|
||||
|
||||
derBytes, err := mdmcertutil.NewCSR(pushKey, *flEmail, *flCountry, *flCN)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not generate push CSR")
|
||||
}
|
||||
pemCSR := mdmcertutil.PemCSR(derBytes)
|
||||
// Do we even need to write-out the CSR?
|
||||
err = ioutil.WriteFile(*flCCSRPath, pemCSR, 0600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not write PEM file")
|
||||
}
|
||||
|
||||
sign := newMdmcertDownloadSignRequest(*flEmail, pemCSR, pemPkiCert)
|
||||
req, err := sign.HTTPRequest()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create http request")
|
||||
}
|
||||
err = sendMdmcertDownloadRequest(http.DefaultClient, req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error sending http request")
|
||||
}
|
||||
|
||||
fmt.Print("Request successfully sent to mdmcert.download. Your CSR should now\n" +
|
||||
"be signed. Check your email for next steps. Then use the -decrypt option\n" +
|
||||
"to extract the CSR request which will then be uploaded to Apple.\n")
|
||||
|
||||
} else { // -decrypt switch
|
||||
if _, err := os.Stat(*flCReqPath); err == nil {
|
||||
return fmt.Errorf("file already exists: %s", *flCReqPath)
|
||||
}
|
||||
hexBytes, err := ioutil.ReadFile(*flDecrypt)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading encrypted file")
|
||||
}
|
||||
pkcsBytes, err := hex.DecodeString(string(hexBytes))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error decoding hex")
|
||||
}
|
||||
pkiCert, err := crypto.ReadPEMCertificateFile(*flCertPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading PKI certificate")
|
||||
}
|
||||
var pkiKey *rsa.PrivateKey
|
||||
if *flPKeyPass != "" {
|
||||
pkiKey, err = crypto.ReadEncryptedPEMRSAKeyFile(*flKeyPath, []byte(*flPKeyPass))
|
||||
} else {
|
||||
pkiKey, err = crypto.ReadPEMRSAKeyFile(*flKeyPath)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading PKI private key")
|
||||
}
|
||||
ioutil.WriteFile("/tmp/fubar.p7", pkcsBytes, 0666)
|
||||
p7, err := pkcs7.Parse(pkcsBytes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parsing mdmcert PKCS7 response")
|
||||
}
|
||||
// fmt.Println(p7)
|
||||
content, err := p7.Decrypt(pkiCert, pkiKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decrypting mdmcert PKCS7 response")
|
||||
}
|
||||
err = ioutil.WriteFile(*flCReqPath, content, 0666)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "writing Push Request response")
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully able to decrypt the MDM Push Certificate request! Please upload\n"+
|
||||
"the file '%s' to Apple by visiting https://identity.apple.com\n"+
|
||||
"Once your Push Certificate is signed by Apple you can download it\n"+
|
||||
"and import it into MicroMDM using the `mdmctl mdmcert upload` command\n", *flCReqPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMdmcertDownloadSignRequest(email string, pemCSR []byte, serverCertificate []byte) *signRequest {
|
||||
encodedCSR := base64.StdEncoding.EncodeToString(pemCSR)
|
||||
encodedServerCert := base64.StdEncoding.EncodeToString(serverCertificate)
|
||||
return &signRequest{
|
||||
CSR: encodedCSR,
|
||||
Email: email,
|
||||
Key: mdmcertAPIKey,
|
||||
Encrypt: encodedServerCert,
|
||||
}
|
||||
}
|
||||
|
||||
func (sign *signRequest) HTTPRequest() (*http.Request, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(buf).Encode(sign); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", mdmcertRequestURL, ioutil.NopCloser(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("User-Agent", "micromdm/certhelper")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func sendMdmcertDownloadRequest(client *http.Client, req *http.Request) error {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("received bad status from mdmcert.download. status=%q", resp.Status)
|
||||
}
|
||||
var jsn = struct {
|
||||
Result string
|
||||
}{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&jsn); err != nil {
|
||||
return err
|
||||
}
|
||||
if jsn.Result != "success" {
|
||||
return fmt.Errorf("got unexpected result body: %q\n", jsn.Result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -38,6 +38,9 @@ func main() {
|
||||
case "mdmcert":
|
||||
cmd := new(mdmcertCommand)
|
||||
run = cmd.Run
|
||||
case "mdmcert.download":
|
||||
cmd := new(mdmcertDownloadCommand)
|
||||
run = cmd.Run
|
||||
default:
|
||||
usage()
|
||||
os.Exit(1)
|
||||
@@ -58,6 +61,7 @@ Available Commands:
|
||||
config
|
||||
remove
|
||||
mdmcert
|
||||
mdmcert.download
|
||||
version
|
||||
|
||||
Use micromdm <command> -h for additional usage of each command.
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateRandomCertificateSerialNumber() (*big.Int, error) {
|
||||
@@ -66,6 +67,11 @@ func ReadPEMCertificateFile(path string) (*x509.Certificate, error) {
|
||||
return certs[0], nil
|
||||
}
|
||||
|
||||
const (
|
||||
rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY"
|
||||
certificatePEMBlockType = "CERTIFICATE"
|
||||
)
|
||||
|
||||
func ReadPEMCertificatesFile(path string) ([]*x509.Certificate, error) {
|
||||
pemData, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -76,7 +82,7 @@ func ReadPEMCertificatesFile(path string) ([]*x509.Certificate, error) {
|
||||
for {
|
||||
var block *pem.Block
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
if block == nil || block.Type != certificatePEMBlockType {
|
||||
return nil, errors.New("failed to decode PEM block containing certificate")
|
||||
}
|
||||
asn1data = append(asn1data, block.Bytes...)
|
||||
@@ -87,6 +93,40 @@ func ReadPEMCertificatesFile(path string) ([]*x509.Certificate, error) {
|
||||
return x509.ParseCertificates(asn1data)
|
||||
}
|
||||
|
||||
func ReadPEMRSAKeyFile(path string) (*rsa.PrivateKey, error) {
|
||||
return ReadEncryptedPEMRSAKeyFile(path, nil)
|
||||
}
|
||||
|
||||
func ReadEncryptedPEMRSAKeyFile(path string, password []byte) (*rsa.PrivateKey, error) {
|
||||
pemData, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pemBlock, _ := pem.Decode(pemData)
|
||||
if pemBlock == nil {
|
||||
return nil, errors.New("PEM decode failed")
|
||||
}
|
||||
if pemBlock.Type != rsaPrivateKeyPEMBlockType {
|
||||
return nil, fmt.Errorf("expecting PEM type of %s, but got %s", rsaPrivateKeyPEMBlockType, pemBlock.Type)
|
||||
}
|
||||
|
||||
if x509.IsEncryptedPEMBlock(pemBlock) {
|
||||
if password == nil {
|
||||
return nil, errors.New("no supplied password for encrypted PEM")
|
||||
}
|
||||
derBytes, err := x509.DecryptPEMBlock(pemBlock, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x509.ParsePKCS1PrivateKey(derBytes)
|
||||
} else if password != nil {
|
||||
return nil, errors.New("supplied PEM password, but not encrypted")
|
||||
}
|
||||
|
||||
return x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
|
||||
}
|
||||
|
||||
func WritePEMCertificateFile(cert *x509.Certificate, path string) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
@@ -97,7 +137,7 @@ func WritePEMCertificateFile(cert *x509.Certificate, path string) error {
|
||||
return pem.Encode(
|
||||
file,
|
||||
&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Type: certificatePEMBlockType,
|
||||
Bytes: cert.Raw,
|
||||
})
|
||||
}
|
||||
@@ -112,11 +152,31 @@ func WritePEMRSAKeyFile(key *rsa.PrivateKey, path string) error {
|
||||
return pem.Encode(
|
||||
file,
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Type: rsaPrivateKeyPEMBlockType,
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
})
|
||||
}
|
||||
|
||||
func WriteEncryptedPEMRSAKeyFile(key *rsa.PrivateKey, password []byte, path string) error {
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encPemBlock, err := x509.EncryptPEMBlock(
|
||||
rand.Reader,
|
||||
rsaPrivateKeyPEMBlockType,
|
||||
x509.MarshalPKCS1PrivateKey(key),
|
||||
password,
|
||||
x509.PEMCipher3DES)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pem.Encode(file, encPemBlock)
|
||||
}
|
||||
|
||||
// TopicFromCert extracts the push certificate topic from the provided certificate.
|
||||
func TopicFromCert(cert *x509.Certificate) (string, error) {
|
||||
var oidASN1UserID = asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1}
|
||||
|
||||
@@ -43,11 +43,11 @@ func CreateCSR(req *CSRConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
derBytes, err := newCSR(key, strings.ToLower(req.Email), strings.ToUpper(req.Country), req.CommonName)
|
||||
derBytes, err := NewCSR(key, strings.ToLower(req.Email), strings.ToUpper(req.Country), req.CommonName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pemCSR := pemCSR(derBytes)
|
||||
pemCSR := PemCSR(derBytes)
|
||||
return ioutil.WriteFile(req.CSRPath, pemCSR, 0600)
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ const (
|
||||
)
|
||||
|
||||
// create a CSR using the same parameters as Keychain Access would produce
|
||||
func newCSR(priv *rsa.PrivateKey, email, country, cname string) ([]byte, error) {
|
||||
func NewCSR(priv *rsa.PrivateKey, email, country, cname string) ([]byte, error) {
|
||||
subj := pkix.Name{
|
||||
Country: []string{country},
|
||||
CommonName: cname,
|
||||
@@ -161,7 +161,7 @@ func newCSR(priv *rsa.PrivateKey, email, country, cname string) ([]byte, error)
|
||||
}
|
||||
|
||||
// convert DER to PEM format
|
||||
func pemCSR(derBytes []byte) []byte {
|
||||
func PemCSR(derBytes []byte) []byte {
|
||||
pemBlock := &pem.Block{
|
||||
Type: csrPEMBlockType,
|
||||
Headers: nil,
|
||||
|
||||
Reference in New Issue
Block a user