mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Merge remote-tracking branch 'origin/feat-save-certs' into save-certs-encrypted
This commit is contained in:
commit
141b5c9456
8 changed files with 176 additions and 156 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ type bodyHandler interface {
|
|||
|
||||
type FileResponse struct {
|
||||
DestPath string
|
||||
DestFile string
|
||||
destFilePath string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue