mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-08 18:55:34 +08:00
286 lines
7.8 KiB
Go
286 lines
7.8 KiB
Go
// 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/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/micromdm/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://www.apple.com/certificateauthority/AppleWWDRCAG3.cer"
|
|
appleRootCAURL = "https://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 := sha256.New()
|
|
h.Write(csrData)
|
|
signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, 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{{
|
|
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
|
|
}
|