diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b2d22b2..722f0a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [v1.6.0](https://github.com/micromdm/micromdm/compare/v1.4.0...1.5.0) TBD * Add support for User Enrollment (#597) +* Add support for Signing Profiles (#602) ## [v1.5.0](https://github.com/micromdm/micromdm/compare/v1.4.0...1.5.0) June 15 2019 diff --git a/cmd/mdmctl/apply.go b/cmd/mdmctl/apply.go index 5e9376bc..cd19a204 100644 --- a/cmd/mdmctl/apply.go +++ b/cmd/mdmctl/apply.go @@ -2,7 +2,10 @@ package main import ( "context" + "crypto" + "crypto/x509" "encoding/json" + "encoding/pem" "flag" "fmt" "io/ioutil" @@ -15,7 +18,9 @@ import ( "github.com/go-kit/kit/log" "github.com/pkg/errors" uuid "github.com/satori/go.uuid" + "golang.org/x/crypto/pkcs12" + "github.com/micromdm/micromdm/pkg/crypto/profileutil" "github.com/micromdm/micromdm/platform/blueprint" "github.com/micromdm/micromdm/platform/profile" ) @@ -229,12 +234,89 @@ func (cmd *applyCommand) applyBlock(args []string) error { return nil } +func loadSigningKey(keyPass, keyPath, certPath string) (crypto.PrivateKey, *x509.Certificate, error) { + certData, err := ioutil.ReadFile(certPath) + if err != nil { + return nil, nil, err + } + + isP12 := filepath.Ext(certPath) == ".p12" + if isP12 { + pkey, cert, err := pkcs12.Decode(certData, keyPass) + return pkey, cert, errors.Wrap(err, "decode p12 contents") + } + + keyData, err := ioutil.ReadFile(keyPath) + if err != nil { + return nil, nil, errors.Wrap(err, "read key from file") + } + + keyDataBlock, _ := pem.Decode(keyData) + if keyDataBlock == nil { + return nil, nil, errors.Errorf("invalid PEM data for private key %s", keyPath) + } + var pemKeyData []byte + if x509.IsEncryptedPEMBlock(keyDataBlock) { + b, err := x509.DecryptPEMBlock(keyDataBlock, []byte(keyPass)) + if err != nil { + return nil, nil, fmt.Errorf("decrypting DES private key %s", err) + } + pemKeyData = b + } else { + pemKeyData = keyDataBlock.Bytes + } + + priv, err := x509.ParsePKCS1PrivateKey(pemKeyData) + if err != nil { + return nil, nil, errors.Wrap(err, "parse private key") + } + + pub, _ := pem.Decode(certData) + if pub == nil { + return nil, nil, errors.Errorf("invalid PEM data for certificate %q", certPath) + } + + cert, err := x509.ParseCertificate(pub.Bytes) + if err != nil { + return nil, nil, errors.Wrap(err, "parse PEM certificate data") + } + + return priv, cert, nil +} + func (cmd *applyCommand) applyProfile(args []string) error { flagset := flag.NewFlagSet("profiles", flag.ExitOnError) var ( - flProfilePath = flagset.String("f", "", "filename of profile to apply") + flProfilePath = flagset.String("f", "", "Path to profile payload.") + flSign = flagset.Bool("sign", false, "Sign the profile. Requires key and certificate path.") + flOut = flagset.String("out", "", "Output path for signed profile(optional).") + flKeyPass = flagset.String("password", "", "Password to encrypt/read the signing key(optional) or p12 file.") + flKeyPath = flagset.String("private-key", "", "Path to the signing private key. Don't use with p12 file.") + flCertPath = flagset.String("cert", "", "Path to the signing certificate or p12 file.") ) - flagset.Usage = usageFor(flagset, "mdmctl apply profiles [flags]") + flagset.Usage = func() { + fmt.Fprintf(os.Stderr, "%s\n", + `Upload profiles to the server. + +Uploaded profiles can also be specified in a blueprint, which will be applied on device enrollment. +This command can also be used to replace the enrollment profile. +Profiles can be signed before upload. + +Examples + + # Upload a mobileconfig + mdmctl apply profiles -f /path/to/profile.mobileconfig + + # Sign and upload + mdmctl apply profiles -f /path/to/profile.mobileconfig -private-key key.pem -cert certificate.pem -password secret -sign + + # Sign and save to local directory instead of uploading + # Use "-out -" print the output to stdout instead of a file. + mdmctl apply profiles -f /path/to/profile.mobileconfig -private-key key.pem -cert certificate.pem -password secret -sign -out signed.mobileconfig + +`) + usageFor(flagset, "mdmctl apply profiles [flags]")() + } if err := flagset.Parse(args); err != nil { return err } @@ -247,6 +329,26 @@ func (cmd *applyCommand) applyProfile(args []string) error { return err } + if *flSign { + priv, pub, err := loadSigningKey(*flKeyPass, *flKeyPath, *flCertPath) + if err != nil { + return errors.Wrap(err, "loading signing certificate and private key") + } + signed, err := profileutil.Sign(priv, pub, profileBytes) + if err != nil { + return errors.Wrap(err, "signing profile with the specified key") + } + + if *flOut == "-" { // print to stdout and return + _, err = os.Stdout.Write(signed) + return err + } else if *flOut != "" { // write to file and return + return ioutil.WriteFile(*flOut, signed, 0644) + } + + profileBytes = signed + } + // TODO: to consider just uploading the Mobileconfig data (without a // Profile struct and doing init server side) var p profile.Profile diff --git a/docs/user-guide/enrolling-devices.md b/docs/user-guide/enrolling-devices.md index a96cb7be..bfd04b61 100644 --- a/docs/user-guide/enrolling-devices.md +++ b/docs/user-guide/enrolling-devices.md @@ -41,7 +41,7 @@ curl -o enroll.mobileconfig https://mdm.acme.co/mdm/enroll ``` Next, use a text editor to modify the payload. -You can also [sign](https://github.com/micromdm/micromdm/wiki/Sign-the-enrollment-profile-with-Hancock) the profile. +You can also [sign](./mdmctl-signing-profiles.md) the profile. Once modified and signed, you can replace the default. diff --git a/docs/user-guide/mdmctl-signing-profiles.md b/docs/user-guide/mdmctl-signing-profiles.md new file mode 100644 index 00000000..7c5ddaa3 --- /dev/null +++ b/docs/user-guide/mdmctl-signing-profiles.md @@ -0,0 +1,33 @@ +# Overview + +The MicroMDM server does not composite or sign profiles for you. Instead, it gives you the option of providing a profile which you have signed through an external process, before uploading it to the server. +To make signing easier, MicroMDM adds the option to sign the profile as part of the `mdmctl` tool. +First, you will need a private key and certificate. Any certificate will do, but to be verified on the device, you should choose one that is trusted on the devices where you're sending the payloads. The `Developer ID` certificates from Apple are examples of trusted certificates. + +# Signing with mdmctl + +The `mdmctl apply profiles` command is used to upload a new configuration profile to the server. It can also be used to sign a profile. +First, run `mdmctl apply profiles -help` to see all the available command line flags. + +Example command to sign a profile before uploading it to the server: +``` + mdmctl apply profiles \ + -f /path/to/profile.mobileconfig \ + -private-key /path/to/key.pem \ + -cert /path/to/certificate.pem \ + -password=private_key_password \ + -sign +``` + +You can also specify the `-out /path/to/signed_output.mobileconfig` to save the signed output locally, instead of uploading it to the server. +Using `-out -` will print the signed contents to the standard output, allowing you to pipe the output to another operation. + +# Signing with other tools + +You can use the `security` command on the Mac to sign configuration profiles with a certificate stored in the Keychain. +Example: +``` +usr/bin/security cms -S -N 'Your KeyChain Cert' -i profile.mobileconfig -o signed.mobileconfig +``` + +Another popular choice is Jeremy Agostino's [Hancock](https://github.com/JeremyAgost/Hancock), a GUI utility for signing profiles and packages. diff --git a/pkg/crypto/profileutil/sign.go b/pkg/crypto/profileutil/sign.go new file mode 100644 index 00000000..d98467af --- /dev/null +++ b/pkg/crypto/profileutil/sign.go @@ -0,0 +1,25 @@ +// Package profileutil signs configuration profiles. +package profileutil + +import ( + "crypto" + "crypto/x509" + + "github.com/fullsailor/pkcs7" + "github.com/pkg/errors" +) + +// Sign takes an unsigned payload and signs it with the provided private key and certificate. +func Sign(key crypto.PrivateKey, cert *x509.Certificate, mobileconfig []byte) ([]byte, error) { + sd, err := pkcs7.NewSignedData(mobileconfig) + if err != nil { + return nil, errors.Wrap(err, "create signed data for mobileconfig") + } + + if err := sd.AddSigner(cert, key, pkcs7.SignerInfoConfig{}); err != nil { + return nil, errors.Wrap(err, "add crypto signer to mobileconfig signed data") + } + + signedMobileconfig, err := sd.Finish() + return signedMobileconfig, errors.Wrap(err, "complete mobileconfig signing") +}