Skip to content

Commit 5661b62

Browse files
Implement mdmcert.download support in mdmctl (micromdm#401)
1 parent 3cb96ac commit 5661b62

5 files changed

Lines changed: 357 additions & 9 deletions

File tree

Gopkg.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/mdmctl/mdmcert.download.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"encoding/base64"
8+
"encoding/hex"
9+
"encoding/json"
10+
"encoding/pem"
11+
"flag"
12+
"fmt"
13+
"io/ioutil"
14+
"net/http"
15+
"os"
16+
17+
"github.com/fullsailor/pkcs7"
18+
"github.com/go-kit/kit/log"
19+
"github.com/micromdm/micromdm/pkg/crypto"
20+
"github.com/micromdm/micromdm/pkg/crypto/mdmcertutil"
21+
"github.com/pkg/errors"
22+
)
23+
24+
const (
25+
mdmcertRequestURL = "https://mdmcert.download/api/v1/signrequest"
26+
// see
27+
// https://github.com/jessepeterson/commandment/blob/1352b51ba6697260d1111eccc3a5a0b5b9af60d0/commandment/mdmcert.py#L23-L28
28+
mdmcertAPIKey = "f847aea2ba06b41264d587b229e2712c89b1490a1208b7ff1aafab5bb40d47bc"
29+
)
30+
31+
// format of a signing request to mdmcert.download
32+
type signRequest struct {
33+
CSR string `json:"csr"` // base64 encoded PEM CSR
34+
Email string `json:"email"`
35+
Key string `json:"key"` // server key from above
36+
Encrypt string `json:"encrypt"` // mdmcert pki cert
37+
}
38+
39+
type mdmcertDownloadCommand struct {
40+
*remoteServices
41+
}
42+
43+
func (cmd *mdmcertDownloadCommand) setup() error {
44+
logger := log.NewLogfmtLogger(os.Stderr)
45+
remote, err := setupClient(logger)
46+
if err != nil {
47+
return err
48+
}
49+
cmd.remoteServices = remote
50+
return nil
51+
}
52+
53+
func (cmd *mdmcertDownloadCommand) Usage() error {
54+
const usageText = `
55+
Request new MDM Push Certificate from https://mdmcert.download
56+
This utility helps obtain an MDM Push Certificate using the service
57+
at mdmcert.download.
58+
59+
First we'll generate the initial request (which also generates a private key):
60+
61+
mdmctl mdmcert.download -new -email=cool.mdm.admin@example.org
62+
63+
This will output the private key into the file mdmcert.download.key.
64+
Then, after you check your email and download the request file you just
65+
need to decrypt the push certificate request:
66+
67+
mdmctl mdmcert.download -decrypt=~/Downloads/mdm_signed_request.20171122_094910_220.plist.b64.p7
68+
69+
This will output the push certificate request to mdmcert.download.req.
70+
Upload this file to https://identity.apple.com and download the signed
71+
certificate. Then use the 'mdmctl mdmcert upload' command to upload it,
72+
(and the above private key) into MicroMDM.
73+
74+
`
75+
fmt.Println(usageText)
76+
return nil
77+
78+
}
79+
80+
func (cmd *mdmcertDownloadCommand) Run(args []string) error {
81+
flagset := flag.NewFlagSet("mdmcert.download", flag.ExitOnError)
82+
flagset.Usage = usageFor(flagset, "mdmctl mdmcert.download [flags]")
83+
var (
84+
flNew = flagset.Bool("new", false, "Generates a new privkey and uploads new MDM request")
85+
flDecrypt = flagset.String("decrypt", "", "Decrypts and mdmcert.download push certificate request")
86+
flEmail = flagset.String("email", "", "Email address to use in mdmcert request & CSR Subject")
87+
flCountry = flagset.String("country", "US", "Two letter country code for the CSR Subject (example: US).")
88+
flCN = flagset.String("cn", "mdm-push", "CommonName for the CSR Subject.")
89+
flCertPath = flagset.String("pki-cert", "mdmcert.download.pki.crt", "Path for generated MDMCert pki exchange certificate")
90+
flKeyPath = flagset.String("pki-private-key", "mdmcert.download.pki.key", "Path for generated MDMCert pki exchange private key")
91+
flPKeyPass = flagset.String("pki-password", "", "Password to encrypt/read the RSA key.")
92+
flCCSRPath = flagset.String("push-csr", "mdmcert.download.push.csr", "Path for generated Push Certificate CSR")
93+
flCReqPath = flagset.String("push-req", "mdmcert.download.push.req", "Path for generated Push Certificate Request")
94+
flCKeyPath = flagset.String("push-private-key", "mdmcert.download.push.key", "Path to the generated Push Cert private key")
95+
flCPKeyPass = flagset.String("push-password", "", "Password to encrypt/read the push RSA key.")
96+
)
97+
98+
if err := flagset.Parse(args); err != nil {
99+
cmd.Usage()
100+
return err
101+
}
102+
103+
// neither flag was used
104+
if !*flNew && *flDecrypt == "" {
105+
cmd.Usage()
106+
return errors.New("bad input: must either use -new or -decrypt")
107+
}
108+
109+
// both flags used
110+
if *flNew && (*flDecrypt != "") {
111+
// cmd.Usage()
112+
return errors.New("bad input: can't use both -new and -decrypt")
113+
}
114+
115+
if *flNew {
116+
if *flEmail == "" {
117+
return errors.New("bad input: must provide -email")
118+
}
119+
120+
paths := []string{*flCertPath, *flKeyPath, *flCCSRPath, *flCKeyPath}
121+
for _, path := range paths {
122+
if _, err := os.Stat(path); err == nil {
123+
return fmt.Errorf("file already exists: %s", path)
124+
}
125+
}
126+
127+
pkiKey, pkiCert, err := crypto.SimpleSelfSignedRSAKeypair("mdmcert.download", 365)
128+
if err != nil {
129+
return errors.Wrap(err, "could not create PKI keypair")
130+
}
131+
132+
pemBlock := &pem.Block{
133+
Type: "CERTIFICATE",
134+
Headers: nil,
135+
Bytes: pkiCert.Raw,
136+
}
137+
pemPkiCert := pem.EncodeToMemory(pemBlock)
138+
139+
if err := crypto.WritePEMCertificateFile(pkiCert, *flCertPath); err != nil {
140+
return errors.Wrap(err, "could not write PKI cert")
141+
}
142+
143+
if *flPKeyPass != "" {
144+
err = crypto.WriteEncryptedPEMRSAKeyFile(pkiKey, []byte(*flPKeyPass), *flKeyPath)
145+
} else {
146+
err = crypto.WritePEMRSAKeyFile(pkiKey, *flKeyPath)
147+
}
148+
if err != nil {
149+
return errors.Wrap(err, "could not write private key")
150+
}
151+
152+
pushKey, err := rsa.GenerateKey(rand.Reader, 2048)
153+
if err != nil {
154+
return errors.Wrap(err, "could not generate push private key")
155+
}
156+
157+
if *flCPKeyPass != "" {
158+
err = crypto.WriteEncryptedPEMRSAKeyFile(pushKey, []byte(*flCPKeyPass), *flCKeyPath)
159+
} else {
160+
err = crypto.WritePEMRSAKeyFile(pushKey, *flCKeyPath)
161+
}
162+
if err != nil {
163+
return errors.Wrap(err, "could not write push private key")
164+
}
165+
166+
derBytes, err := mdmcertutil.NewCSR(pushKey, *flEmail, *flCountry, *flCN)
167+
if err != nil {
168+
return errors.Wrap(err, "could not generate push CSR")
169+
}
170+
pemCSR := mdmcertutil.PemCSR(derBytes)
171+
// Do we even need to write-out the CSR?
172+
err = ioutil.WriteFile(*flCCSRPath, pemCSR, 0600)
173+
if err != nil {
174+
return errors.Wrap(err, "could not write PEM file")
175+
}
176+
177+
sign := newMdmcertDownloadSignRequest(*flEmail, pemCSR, pemPkiCert)
178+
req, err := sign.HTTPRequest()
179+
if err != nil {
180+
return errors.Wrap(err, "could not create http request")
181+
}
182+
err = sendMdmcertDownloadRequest(http.DefaultClient, req)
183+
if err != nil {
184+
return errors.Wrap(err, "error sending http request")
185+
}
186+
187+
fmt.Print("Request successfully sent to mdmcert.download. Your CSR should now\n" +
188+
"be signed. Check your email for next steps. Then use the -decrypt option\n" +
189+
"to extract the CSR request which will then be uploaded to Apple.\n")
190+
191+
} else { // -decrypt switch
192+
if _, err := os.Stat(*flCReqPath); err == nil {
193+
return fmt.Errorf("file already exists: %s", *flCReqPath)
194+
}
195+
hexBytes, err := ioutil.ReadFile(*flDecrypt)
196+
if err != nil {
197+
return errors.Wrap(err, "reading encrypted file")
198+
}
199+
pkcsBytes, err := hex.DecodeString(string(hexBytes))
200+
if err != nil {
201+
return errors.Wrap(err, "error decoding hex")
202+
}
203+
pkiCert, err := crypto.ReadPEMCertificateFile(*flCertPath)
204+
if err != nil {
205+
return errors.Wrap(err, "reading PKI certificate")
206+
}
207+
var pkiKey *rsa.PrivateKey
208+
if *flPKeyPass != "" {
209+
pkiKey, err = crypto.ReadEncryptedPEMRSAKeyFile(*flKeyPath, []byte(*flPKeyPass))
210+
} else {
211+
pkiKey, err = crypto.ReadPEMRSAKeyFile(*flKeyPath)
212+
}
213+
if err != nil {
214+
return errors.Wrap(err, "reading PKI private key")
215+
}
216+
ioutil.WriteFile("/tmp/fubar.p7", pkcsBytes, 0666)
217+
p7, err := pkcs7.Parse(pkcsBytes)
218+
if err != nil {
219+
return errors.Wrap(err, "parsing mdmcert PKCS7 response")
220+
}
221+
// fmt.Println(p7)
222+
content, err := p7.Decrypt(pkiCert, pkiKey)
223+
if err != nil {
224+
return errors.Wrap(err, "decrypting mdmcert PKCS7 response")
225+
}
226+
err = ioutil.WriteFile(*flCReqPath, content, 0666)
227+
if err != nil {
228+
return errors.Wrap(err, "writing Push Request response")
229+
}
230+
231+
fmt.Printf("Successfully able to decrypt the MDM Push Certificate request! Please upload\n"+
232+
"the file '%s' to Apple by visiting https://identity.apple.com\n"+
233+
"Once your Push Certificate is signed by Apple you can download it\n"+
234+
"and import it into MicroMDM using the `mdmctl mdmcert upload` command\n", *flCReqPath)
235+
}
236+
237+
return nil
238+
}
239+
240+
func newMdmcertDownloadSignRequest(email string, pemCSR []byte, serverCertificate []byte) *signRequest {
241+
encodedCSR := base64.StdEncoding.EncodeToString(pemCSR)
242+
encodedServerCert := base64.StdEncoding.EncodeToString(serverCertificate)
243+
return &signRequest{
244+
CSR: encodedCSR,
245+
Email: email,
246+
Key: mdmcertAPIKey,
247+
Encrypt: encodedServerCert,
248+
}
249+
}
250+
251+
func (sign *signRequest) HTTPRequest() (*http.Request, error) {
252+
buf := new(bytes.Buffer)
253+
if err := json.NewEncoder(buf).Encode(sign); err != nil {
254+
return nil, err
255+
}
256+
req, err := http.NewRequest("POST", mdmcertRequestURL, ioutil.NopCloser(buf))
257+
if err != nil {
258+
return nil, err
259+
}
260+
req.Header.Add("Content-Type", "application/json")
261+
req.Header.Add("User-Agent", "micromdm/certhelper")
262+
return req, nil
263+
}
264+
265+
func sendMdmcertDownloadRequest(client *http.Client, req *http.Request) error {
266+
resp, err := client.Do(req)
267+
if err != nil {
268+
return nil
269+
}
270+
defer resp.Body.Close()
271+
if resp.StatusCode != http.StatusOK {
272+
return fmt.Errorf("received bad status from mdmcert.download. status=%q", resp.Status)
273+
}
274+
var jsn = struct {
275+
Result string
276+
}{}
277+
if err := json.NewDecoder(resp.Body).Decode(&jsn); err != nil {
278+
return err
279+
}
280+
if jsn.Result != "success" {
281+
return fmt.Errorf("got unexpected result body: %q\n", jsn.Result)
282+
}
283+
return nil
284+
}

cmd/mdmctl/mdmdctl.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ func main() {
3838
case "mdmcert":
3939
cmd := new(mdmcertCommand)
4040
run = cmd.Run
41+
case "mdmcert.download":
42+
cmd := new(mdmcertDownloadCommand)
43+
run = cmd.Run
4144
default:
4245
usage()
4346
os.Exit(1)
@@ -58,6 +61,7 @@ Available Commands:
5861
config
5962
remove
6063
mdmcert
64+
mdmcert.download
6165
version
6266
6367
Use micromdm <command> -h for additional usage of each command.

0 commit comments

Comments
 (0)