Merge remote-tracking branch 'origin/feat-save-certs' into save-certs-encrypted

This commit is contained in:
Roberto Dip 2024-05-27 16:40:00 -03:00
commit 141b5c9456
8 changed files with 176 additions and 156 deletions

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 {

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

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)
})
}

View file

@ -146,7 +146,7 @@ func GetSignedAPNSCSR(client *http.Client, csr *x509.CertificateRequest) error {
return nil
}
type WebsiteResponse struct {
type websiteSignCSRResponse struct {
CSR []byte `json:"csr"`
}
@ -182,12 +182,15 @@ func GetSignedAPNSCSRNoEmail(client *http.Client, csr *x509.CertificateRequest)
}
defer resp.Body.Close()
respBytes, _ := io.ReadAll(resp.Body)
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("parsing CSR body response from fleetdm api: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, FleetWebsiteError{Status: resp.StatusCode, message: string(respBytes)}
}
var csrResp WebsiteResponse
var csrResp websiteSignCSRResponse
if err := json.Unmarshal(respBytes, &csrResp); err != nil {
return nil, fmt.Errorf("unmarshalling signed csr response from fleetdm api: %w", err)
}

View file

@ -198,6 +198,7 @@ type bodyHandler interface {
type FileResponse struct {
DestPath string
DestFile string
destFilePath string
}

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) {

View file

@ -3,14 +3,18 @@ package service
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"database/sql"
"encoding/base64"
"encoding/json"
"encoding/pem"
"encoding/xml"
"errors"
"fmt"
"io"
"math/big"
"mime/multipart"
"net/http"
"net/http/httptest"
@ -272,9 +276,20 @@ func (s *integrationMDMTestSuite) SetupSuite() {
w.WriteHeader(status.(int))
resp := []byte(fmt.Sprintf("status: %d", status))
if status == http.StatusOK && strings.Contains(r.URL.RawQuery, "deliveryMethod=json") {
resp = []byte(fmt.Sprintf(`{"csr": "%s"}`, base64.StdEncoding.EncodeToString([]byte(`-----BEGIN CERTIFICATE REQUEST-----
foobar
-----END CERTIFICATE REQUEST-----`))))
rawBody, err := io.ReadAll(r.Body)
require.NoError(s.T(), err)
var req struct {
UnsignedCSRData []byte `json:"unsignedCsrData"`
}
err = json.Unmarshal(rawBody, &req)
require.NoError(s.T(), err)
resp = []byte(
fmt.Sprintf(
`{"csr": %q}`,
base64.StdEncoding.EncodeToString(req.UnsignedCSRData),
),
)
}
_, _ = w.Write(resp)
}))
@ -903,7 +918,8 @@ func (s *integrationMDMTestSuite) TestGetMDMCSR() {
t := s.T()
ctx := context.Background()
// TODO(JVE): validate that we get an error if no private key set
// trying to upload a certificate without generating a private key first is not allowed
s.uploadAPNSCert([]byte("-----BEGIN CERTIFICATE-----\nZm9vCg==\n-----END CERTIFICATE-----"), http.StatusBadRequest, "Please generate a private key first.")
// Check that we return bad gateway if the website API errors
s.FailNextCSRRequestWith(http.StatusInternalServerError)
@ -917,9 +933,9 @@ func (s *integrationMDMTestSuite) TestGetMDMCSR() {
s.SucceedNextCSRRequest()
s.DoJSON("GET", "/api/latest/fleet/mdm/apple/request_csr", getMDMAppleCSRRequest{}, http.StatusOK, &resp)
require.NotNil(t, resp.CSR)
require.Equal(t, string(resp.CSR), `-----BEGIN CERTIFICATE REQUEST-----
foobar
-----END CERTIFICATE REQUEST-----`)
block, _ := pem.Decode(resp.CSR)
require.NotNil(t, block)
require.Equal(t, "CERTIFICATE REQUEST", block.Type)
// Check that we created the right assets
var originalAssets []fleet.MDMConfigAsset
@ -931,9 +947,9 @@ foobar
s.SucceedNextCSRRequest()
s.DoJSON("GET", "/api/latest/fleet/mdm/apple/request_csr", getMDMAppleCSRRequest{}, http.StatusOK, &resp)
require.NotNil(t, resp.CSR)
require.Equal(t, string(resp.CSR), `-----BEGIN CERTIFICATE REQUEST-----
foobar
-----END CERTIFICATE REQUEST-----`)
block, _ = pem.Decode(resp.CSR)
require.NotNil(t, block)
require.Equal(t, "CERTIFICATE REQUEST", block.Type)
// Check that the assets stayed the same in the subsequent call
assets, err := s.ds.GetMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey, fleet.MDMAssetAPNSKey})
@ -941,10 +957,29 @@ foobar
require.Equal(t, originalAssets, assets)
// Invalid APNS cert upload attempt
s.uploadAPNSCert("apns_invalid.pem", http.StatusUnprocessableEntity, "Invalid certificate. Please provide a valid certificate from Apple Push Certificate Portal.")
s.uploadAPNSCert([]byte("invalid-cert"), http.StatusUnprocessableEntity, "Invalid certificate. Please provide a valid certificate from Apple Push Certificate Portal.")
// Successfully upload an APNS cert
s.uploadAPNSCert("apns.pem", http.StatusAccepted, "")
csr, err := x509.ParseCertificateRequest(block.Bytes)
require.NoError(t, err)
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(12345678),
Subject: csr.Subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
mockAppleSigner, err := tls.LoadX509KeyPair("testdata/server.pem", "testdata/server.key")
require.NoError(t, err)
mockAppleCert, err := x509.ParseCertificate(mockAppleSigner.Certificate[0])
require.NoError(t, err)
certDER, err := x509.CreateCertificate(rand.Reader, certTemplate, mockAppleCert, csr.PublicKey, mockAppleSigner.PrivateKey)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
s.uploadAPNSCert(certPEM, http.StatusAccepted, "")
assets, err = s.ds.GetMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey, fleet.MDMAssetAPNSKey, fleet.MDMAssetAPNSCert})
require.NoError(t, err)
@ -958,21 +993,14 @@ foobar
require.Len(t, assets, 0)
}
func (s *integrationMDMTestSuite) uploadAPNSCert(pemFileName string, expectedStatus int, wantErr string) {
func (s *integrationMDMTestSuite) uploadAPNSCert(pemBytes []byte, expectedStatus int, wantErr string) {
t := s.T()
read := func(name string) []byte {
b, err := os.ReadFile(filepath.Join("testdata", name))
require.NoError(t, err)
return b
}
pemBytes := read(pemFileName)
var b bytes.Buffer
w := multipart.NewWriter(&b)
// add the package field
fw, err := w.CreateFormFile("certificate", pemFileName)
fw, err := w.CreateFormFile("certificate", "certificate.pem")
require.NoError(t, err)
_, err = io.Copy(fw, bytes.NewBuffer(pemBytes))
require.NoError(t, err)

View file

@ -7,6 +7,7 @@ import (
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
@ -2289,6 +2290,10 @@ func (svc *Service) GetMDMAppleCSR(ctx context.Context) ([]byte, error) {
return signedCSRB64, nil
}
////////////////////////////////////////////////////////////////////////////////
// POST /mdm/apple/apns_certificate
////////////////////////////////////////////////////////////////////////////////
type uploadMDMAppleAPNSCertRequest struct {
File *multipart.FileHeader
}
@ -2345,10 +2350,6 @@ func (svc *Service) UploadMDMAppleAPNSCert(ctx context.Context, cert io.ReadSeek
return err
}
if len(svc.config.Server.PrivateKey) == 0 {
return ctxerr.Wrap(ctx, errors.New("no private key configured"))
}
if cert == nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "Invalid certificate. Please provide a valid certificate from Apple Push Certificate Portal."))
}
@ -2365,21 +2366,45 @@ func (svc *Service) UploadMDMAppleAPNSCert(ctx context.Context, cert io.ReadSeek
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "Invalid certificate. Please provide a valid certificate from Apple Push Certificate Portal."))
}
// Save to DB encrypted
encryptedCert, err := Encrypt(certBytes, svc.config.Server.PrivateKey)
if err != nil {
return ctxerr.Wrap(ctx, err, "encrypting apns certificate")
if err := svc.authz.Authorize(ctx, &fleet.AppleMDM{}, fleet.ActionRead); err != nil {
return err
}
assets, err := svc.ds.GetMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAPNSKey})
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving APNs key")
}
if len(assets) == 0 {
return ctxerr.Wrap(ctx, &fleet.BadRequestError{
Message: "Please generate a private key first.",
}, "uploading APNs certificate")
}
// this should never happen
if len(assets) != 1 || assets[0].Name != fleet.MDMAssetAPNSKey {
return ctxerr.New(ctx, "corrupt APNs information stored in the database")
}
_, err = tls.X509KeyPair(certBytes, assets[0].Value)
if err != nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("certificate", "Invalid certificate. Please provide a valid certificate from Apple Push Certificate Portal."))
}
// Save to DB
return ctxerr.Wrap(
ctx,
svc.ds.InsertMDMConfigAssets(ctx, []fleet.MDMConfigAsset{
{Name: fleet.MDMAssetAPNSCert, Value: encryptedCert},
{Name: fleet.MDMAssetAPNSCert, Value: certBytes},
}),
"writing apns cert to db",
)
}
////////////////////////////////////////////////////////////////////////////////
// DELETE /mdm/apple/apns_certificate
////////////////////////////////////////////////////////////////////////////////
type deleteMDMAppleAPNSCertRequest struct{}
type deleteMDMAppleAPNSCertResponse struct {