add CLI for the new MDM cert flow (#19240)

for #19022


# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Roberto Dip
2024-05-27 11:14:37 -03:00
committed by GitHub
parent e11f44a89b
commit 42876a69bb
5 changed files with 87 additions and 123 deletions
+3
View File
@@ -21,6 +21,9 @@ import (
"github.com/urfave/cli/v2"
)
var ErrGeneric = errors.New(`Something's gone wrong. Please try again. If this keeps happening please file an issue:
https://github.com/fleetdm/fleet/issues/new/choose`)
func unauthenticatedClientFromCLI(c *cli.Context) (*service.Client, error) {
cc, err := clientConfigFromCLI(c)
if err != nil {
+45 -83
View File
@@ -4,22 +4,18 @@ import (
"fmt"
"os"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/urfave/cli/v2"
)
const (
apnsKeyPath = "fleet-mdm-apple-apns.key"
scepCACertPath = "fleet-mdm-apple-scep.crt"
scepCAKeyPath = "fleet-mdm-apple-scep.key"
apnsCSRPath = "fleet-mdm-csr.csr"
bmPublicKeyCertPath = "fleet-apple-mdm-bm-public-key.crt"
bmPrivateKeyPath = "fleet-apple-mdm-bm-private.key"
)
func generateCommand() *cli.Command {
return &cli.Command{
Name: "generate",
Usage: "Generate certificates and keys required for MDM",
Usage: "Generate certificates and keys required for MDM.",
Flags: []cli.Flag{
configFlag(),
contextFlag(),
@@ -36,91 +32,54 @@ func generateMDMAppleCommand() *cli.Command {
return &cli.Command{
Name: "mdm-apple",
Aliases: []string{"mdm_apple"},
Usage: "Generates certificate signing request (CSR) and key for Apple Push Notification Service (APNs) and certificate and key for Simple Certificate Enrollment Protocol (SCEP) to turn on MDM features.",
Usage: "Generates certificate signing request (CSR) to turn on MDM features.",
Flags: []cli.Flag{
contextFlag(),
debugFlag(),
&cli.StringFlag{
Name: "email",
Usage: "The email address to send the signed APNS csr to.",
Required: true,
},
&cli.StringFlag{
Name: "org",
Usage: "The organization requesting the signed APNS csr.",
Required: true,
},
&cli.StringFlag{
Name: "apns-key",
Usage: "The output path for the APNs private key.",
Value: apnsKeyPath,
},
&cli.StringFlag{
Name: "scep-cert",
Usage: "The output path for the SCEP CA certificate.",
Value: scepCACertPath,
},
&cli.StringFlag{
Name: "scep-key",
Usage: "The output path for the SCEP CA private key.",
Value: scepCAKeyPath,
Name: "csr",
Usage: "The output path for the APNs CSR.",
Value: apnsCSRPath,
},
},
Action: func(c *cli.Context) error {
email := c.String("email")
org := c.String("org")
apnsKeyPath := c.String("apns-key")
scepCACertPath := c.String("scep-cert")
scepCAKeyPath := c.String("scep-key")
csrPath := c.String("csr")
// get the fleet API client first, so that any login requirement are met
// before printing the CSR output message.
client, err := clientFromCLI(c)
if err != nil {
return err
fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err)
return ErrGeneric
}
fmt.Fprintf(
c.App.Writer,
`Sending certificate signing request (CSR) for Apple Push Notification service (APNs) to %s...
Generating APNs key, Simple Certificate Enrollment Protocol (SCEP) certificate, and SCEP key...
`,
email,
)
csr, err := client.RequestAppleCSR(email, org)
csr, err := client.RequestAppleCSR()
if err != nil {
return err
fmt.Fprintf(c.App.ErrWriter, "requesting APNs CSR: %s", err)
return ErrGeneric
}
if err := os.WriteFile(apnsKeyPath, csr.APNsKey, defaultFileMode); err != nil {
return fmt.Errorf("failed to write APNs private key: %w", err)
if err := os.WriteFile(csrPath, csr, defaultFileMode); err != nil {
fmt.Fprintf(c.App.ErrWriter, "write CSR: %s", err)
return ErrGeneric
}
if err := os.WriteFile(scepCACertPath, csr.SCEPCert, defaultFileMode); err != nil {
return fmt.Errorf("failed to write SCEP CA certificate: %w", err)
}
if err := os.WriteFile(scepCAKeyPath, csr.SCEPKey, defaultFileMode); err != nil {
return fmt.Errorf("failed to write SCEP CA private key: %w", err)
appCfg, err := client.GetAppConfig()
if err != nil {
fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err)
return ErrGeneric
}
fmt.Fprintf(
c.App.Writer,
`Success!
Generated your APNs key at %s
Generated your certificate signing request (CSR) at %s
Generated your SCEP certificate at %s
Generated your SCEP key at %s
Go to your email to download a CSR from Fleet. Then, visit https://identity.apple.com/pushcert to upload the CSR. You should receive an APNs certificate in return from Apple.
Next, use the generated certificates to deploy Fleet with `+"`mdm`"+` configuration: https://fleetdm.com/docs/deploying/configuration#mobile-device-management-mdm
Go to %s/settings/integrations/mdm/apple and follow the steps.
`,
apnsKeyPath,
scepCACertPath,
scepCAKeyPath,
csrPath,
appCfg.ServerSettings.ServerURL,
)
return nil
@@ -132,7 +91,7 @@ func generateMDMAppleBMCommand() *cli.Command {
return &cli.Command{
Name: "mdm-apple-bm",
Aliases: []string{"mdm_apple_bm"},
Usage: "Generate Apple Business Manager public and private keys to enable automatic enrollment for macOS hosts.",
Usage: "Generate Apple Business Manager public key to enable automatic enrollment for macOS hosts.",
Flags: []cli.Flag{
contextFlag(),
debugFlag(),
@@ -141,27 +100,33 @@ func generateMDMAppleBMCommand() *cli.Command {
Usage: "The output path for the Apple Business Manager public key certificate.",
Value: bmPublicKeyCertPath,
},
&cli.StringFlag{
Name: "private-key",
Usage: "The output path for the Apple Business Manager private key.",
Value: bmPrivateKeyPath,
},
},
Action: func(c *cli.Context) error {
publicKeyPath := c.String("public-key")
privateKeyPath := c.String("private-key")
publicKeyPEM, privateKeyPEM, err := apple_mdm.NewDEPKeyPairPEM()
// get the fleet API client first, so that any login requirement are met
// before printing the CSR output message.
client, err := clientFromCLI(c)
if err != nil {
return fmt.Errorf("generate key pair: %w", err)
fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err)
return ErrGeneric
}
if err := os.WriteFile(publicKeyPath, publicKeyPEM, defaultFileMode); err != nil {
return fmt.Errorf("write public key: %w", err)
publicKey, err := client.RequestAppleABM()
if err != nil {
fmt.Fprintf(c.App.ErrWriter, "requesting ABM public key: %s", err)
return ErrGeneric
}
if err := os.WriteFile(privateKeyPath, privateKeyPEM, defaultFileMode); err != nil {
return fmt.Errorf("write private key: %w", err)
if err := os.WriteFile(publicKeyPath, publicKey, defaultFileMode); err != nil {
fmt.Fprintf(c.App.ErrWriter, "write public key: %s", err)
return ErrGeneric
}
appCfg, err := client.GetAppConfig()
if err != nil {
fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err)
return ErrGeneric
}
fmt.Fprintf(
@@ -170,14 +135,11 @@ func generateMDMAppleBMCommand() *cli.Command {
Generated your public key at %s
Generated your private key at %s
Go to %s/settings/integrations/automatic-enrollment/apple and follow the steps.
Visit https://business.apple.com/ and create a new MDM server with the public key. Then, download the new MDM server's token.
Next, deploy Fleet with with `+"`mdm`"+` configuration: https://fleetdm.com/docs/deploying/configuration#mobile-device-management-mdm
`,
publicKeyPath,
privateKeyPath,
appCfg.ServerSettings.ServerURL,
)
return nil
+21 -30
View File
@@ -1,8 +1,8 @@
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
@@ -14,37 +14,33 @@ import (
)
func TestGenerateMDMAppleBM(t *testing.T) {
// TODO(roberto): update when the new endpoint to get a CSR is ready
t.Skip()
outdir, err := os.MkdirTemp("", t.Name())
require.NoError(t, err)
defer os.Remove(outdir)
publicKeyPath := filepath.Join(outdir, "public-key.crt")
privateKeyPath := filepath.Join(outdir, "private-key.key")
out := runAppForTest(t, []string{
"generate", "mdm-apple-bm",
"--public-key", publicKeyPath,
"--private-key", privateKeyPath,
})
require.Contains(t, out, fmt.Sprintf("Generated your public key at %s", outdir))
require.Contains(t, out, fmt.Sprintf("Generated your private key at %s", outdir))
// validate that the keypair is valid
cert, err := tls.LoadX509KeyPair(publicKeyPath, privateKeyPath)
// validate that the certificate is valid
certPEMBlock, err := os.ReadFile(publicKeyPath)
require.NoError(t, err)
parsed, err := x509.ParseCertificate(cert.Certificate[0])
parsed, err := x509.ParseCertificate(certPEMBlock)
require.NoError(t, err)
require.Equal(t, "FleetDM", parsed.Issuer.CommonName)
}
func TestGenerateMDMApple(t *testing.T) {
t.Run("missing input", func(t *testing.T) {
runAppCheckErr(t, []string{"generate", "mdm-apple"}, `Required flags "email, org" not set`)
runAppCheckErr(t, []string{"generate", "mdm-apple", "--email", "user@example.com"}, `Required flag "org" not set`)
runAppCheckErr(t, []string{"generate", "mdm-apple", "--org", "Acme"}, `Required flag "email" not set`)
})
t.Run("CSR API call fails", func(t *testing.T) {
// TODO(roberto): update when the new endpoint to get a CSR is ready
t.Skip()
_, _ = runServerWithMockedDS(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// fail this call
@@ -57,14 +53,14 @@ func TestGenerateMDMApple(t *testing.T) {
t,
[]string{
"generate", "mdm-apple",
"--email", "user@example.com",
"--org", "Acme",
},
`POST /api/latest/fleet/mdm/apple/request_csr received status 422 Validation Failed: this email address is not valid: bad request`,
)
})
t.Run("successful run", func(t *testing.T) {
// TODO(roberto): update when the new endpoint to get a CSR is ready
t.Skip()
_, _ = runServerWithMockedDS(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@@ -76,29 +72,24 @@ func TestGenerateMDMApple(t *testing.T) {
outdir, err := os.MkdirTemp("", "TestGenerateMDMApple")
require.NoError(t, err)
defer os.Remove(outdir)
apnsKeyPath := filepath.Join(outdir, "apns.key")
scepCertPath := filepath.Join(outdir, "scep.crt")
scepKeyPath := filepath.Join(outdir, "scep.key")
csrPath := filepath.Join(outdir, "csr.csr")
out := runAppForTest(t, []string{
"generate", "mdm-apple",
"--email", "user@example.com",
"--org", "Acme",
"--apns-key", apnsKeyPath,
"--scep-cert", scepCertPath,
"--scep-key", scepKeyPath,
"--csr", csrPath,
"--debug",
"--context", "default",
})
require.Contains(t, out, fmt.Sprintf("Generated your APNs key at %s", apnsKeyPath))
require.Contains(t, out, fmt.Sprintf("Generated your SCEP certificate at %s", scepCertPath))
require.Contains(t, out, fmt.Sprintf("Generated your SCEP key at %s", scepKeyPath))
require.Contains(t, out, fmt.Sprintf("Generated your SCEP key at %s", csrPath))
// validate that the keypair is valid
scepCrt, err := tls.LoadX509KeyPair(scepCertPath, scepKeyPath)
// validate that the CSR is valid
csrPEM, err := os.ReadFile(csrPath)
require.NoError(t, err)
parsed, err := x509.ParseCertificate(scepCrt.Certificate[0])
block, _ := pem.Decode(csrPEM)
require.NotNil(t, block)
require.Equal(t, "CERTIFICATE REQUEST", block.Type)
_, err = x509.ParseCertificateRequest(block.Bytes)
require.NoError(t, err)
require.Equal(t, "FleetDM", parsed.Issuer.CommonName)
})
}
+1
View File
@@ -198,6 +198,7 @@ type bodyHandler interface {
type FileResponse struct {
DestPath string
DestFile string
destFilePath string
}
+17 -10
View File
@@ -41,16 +41,23 @@ func (c *Client) GetAppleBM() (*fleet.AppleBM, error) {
}
// RequestAppleCSR requests a signed CSR from the Fleet server and returns the
// SCEP certificate and key along with the APNs key used for the CSR.
func (c *Client) RequestAppleCSR(email, org string) (*fleet.AppleCSR, error) {
verb, path := "POST", "/api/latest/fleet/mdm/apple/request_csr"
request := requestMDMAppleCSRRequest{
EmailAddress: email,
Organization: org,
}
var responseBody requestMDMAppleCSRResponse
err := c.authenticatedRequest(request, verb, path, &responseBody)
return responseBody.AppleCSR, err
// CSR bytes
func (c *Client) RequestAppleCSR() ([]byte, error) {
verb, path := "GET", "/api/v1/fleet/mdm/apple/request_csr"
// TODO(roberto): adjust request/response type when the endpoint is ready
var request, resp map[string][]byte
err := c.authenticatedRequest(request, verb, path, &resp)
return resp["csr"], err
}
// RequestAppleABM requests a signed CSR from the Fleet server and returns the
// public key bytes
func (c *Client) RequestAppleABM() ([]byte, error) {
verb, path := "GET", "/api/v1/fleet/mdm/apple/abm_public_key?alt=media"
// TODO(roberto): adjust this request type when the endpoint is ready
var request, resp map[string][]byte
err := c.authenticatedRequest(request, verb, path, &resp)
return resp["public_key"], err
}
func (c *Client) GetBootstrapPackageMetadata(teamID uint, forUpdate bool) (*fleet.MDMAppleBootstrapPackage, error) {