diff --git a/changes/27700-validate-bootstrap-is-distribution-pkg b/changes/27700-validate-bootstrap-is-distribution-pkg new file mode 100644 index 0000000000..90335ed32f --- /dev/null +++ b/changes/27700-validate-bootstrap-is-distribution-pkg @@ -0,0 +1 @@ +When uploading bootstrap package for macOS setup experience, validate that it is a Distribution package since that is required by Apple's InstallEnterpriseApplication MDM command. diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 705648c8a7..d40a7d356c 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -303,8 +303,14 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str ptrTeamId = &teamID } - hashBuf := bytes.NewBuffer(nil) - if err := file.CheckPKGSignature(io.TeeReader(pkg, hashBuf)); err != nil { + // Read the pkg into a buffer + buff := bytes.NewBuffer(nil) + if _, err := io.Copy(buff, pkg); err != nil { + return err + } + buffReader := bytes.NewReader(buff.Bytes()) + + if err := file.CheckPKGSignature(buffReader); err != nil { msg := "invalid package" if errors.Is(err, file.ErrInvalidType) || errors.Is(err, file.ErrNotSigned) { msg = err.Error() @@ -316,9 +322,21 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str } } - pkgBuf := bytes.NewBuffer(nil) + buffReader.Reset(buff.Bytes()) + hasDistribution, err := file.XARHasDistribution(buffReader) + if err != nil { + return &fleet.BadRequestError{ + Message: err.Error(), + InternalErr: err, + } + } + if !hasDistribution { + return &fleet.BadRequestError{Message: fleet.BootstrapPkgNotDistributionErrMsg} + } + + buffReader.Reset(buff.Bytes()) hash := sha256.New() - if _, err := io.Copy(hash, io.TeeReader(hashBuf, pkgBuf)); err != nil { + if _, err := io.Copy(hash, buffReader); err != nil { return err } @@ -327,7 +345,7 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str Name: name, Token: uuid.New().String(), Sha256: hash.Sum(nil), - Bytes: pkgBuf.Bytes(), + Bytes: buff.Bytes(), } if err := svc.ds.InsertMDMAppleBootstrapPackage(ctx, bp, svc.bootstrapPackageStore); err != nil { return err diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageUploader/helpers.ts b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageUploader/helpers.tsx similarity index 60% rename from frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageUploader/helpers.ts rename to frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageUploader/helpers.tsx index 2bbbbfa050..de70313206 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageUploader/helpers.ts +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageUploader/helpers.tsx @@ -1,5 +1,8 @@ +import React from "react"; import { AxiosResponse } from "axios"; import { IApiError } from "interfaces/errors"; +import CustomLink from "components/CustomLink"; +import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; export const UPLOAD_ERROR_MESSAGES = { wrongType: { @@ -11,6 +14,21 @@ export const UPLOAD_ERROR_MESSAGES = { message: "Couldn’t upload. The package must be signed. Click “Learn more” below to learn how to sign.", }, + noDistribution: { + condition: (reason: string) => + reason.includes("Bootstrap package must be a distribution package"), + message: ( + <> + Couldn't upload. Bootstrap package must be a distribution package.{" "} + + + ), + }, default: { condition: () => false, message: "Couldn’t upload. Please try again.", diff --git a/pkg/file/xar.go b/pkg/file/xar.go index c2e5cc49bf..967754b1bb 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -19,7 +19,6 @@ package file // https://github.com/sassoftware/relic import ( - "bytes" "compress/bzip2" "compress/zlib" "crypto" @@ -173,35 +172,40 @@ type distributionApp struct { ID string `xml:"id,attr"` } +// XARHasDistribution checks if XAR archive has a Distribution file +func XARHasDistribution(r io.Reader) (bool, error) { + hdr, err := readXARFileHeader(r) + if err != nil { + return false, err + } + root, err := decodeXARTOCData(r, hdr) + if err != nil { + return false, err + } + for _, f := range root.TOC.Files { + if f.Name == "Distribution" { + return true, nil + } + } + return false, nil +} + // ExtractXARMetadata extracts the name and version metadata from a .pkg file // in the XAR format. func ExtractXARMetadata(tfr *fleet.TempFileReader) (*InstallerMetadata, error) { - var hdr xarHeader - h := sha256.New() size, _ := io.Copy(h, tfr) // writes to a hash cannot fail - if err := tfr.Rewind(); err != nil { return nil, fmt.Errorf("rewind reader: %w", err) } - // read the file header - if err := binary.Read(tfr, binary.BigEndian, &hdr); err != nil { - return nil, fmt.Errorf("decode xar header: %w", err) - } - - zr, err := zlib.NewReader(io.LimitReader(tfr, hdr.CompressedSize)) + hdr, err := readXARFileHeader(tfr) if err != nil { - return nil, fmt.Errorf("create zlib reader: %w", err) + return nil, err } - defer zr.Close() - - // decode the TOC data (in XML inside the zlib-compressed data) - var root xmlXar - decoder := xml.NewDecoder(zr) - decoder.Strict = false - if err := decoder.Decode(&root); err != nil { - return nil, fmt.Errorf("decode xar xml: %w", err) + root, err := decodeXARTOCData(tfr, hdr) + if err != nil { + return nil, err } // look for the distribution file, with the metadata information @@ -245,6 +249,31 @@ func ExtractXARMetadata(tfr *fleet.TempFileReader) (*InstallerMetadata, error) { return &InstallerMetadata{SHASum: h.Sum(nil)}, nil } +func readXARFileHeader(r io.Reader) (xarHeader, error) { + var hdr xarHeader + if err := binary.Read(r, binary.BigEndian, &hdr); err != nil { + return hdr, fmt.Errorf("decode xar header: %w", err) + } + return hdr, nil +} + +func decodeXARTOCData(r io.Reader, hdr xarHeader) (xmlXar, error) { + var root xmlXar + zr, err := zlib.NewReader(io.LimitReader(r, hdr.CompressedSize)) + if err != nil { + return root, fmt.Errorf("create zlib reader: %w", err) + } + defer zr.Close() + + // decode the TOC data (in XML inside the zlib-compressed data) + decoder := xml.NewDecoder(zr) + decoder.Strict = false + if err := decoder.Decode(&root); err != nil { + return root, fmt.Errorf("decode xar xml: %w", err) + } + return root, nil +} + func readCompressedFile(rat io.ReaderAt, heapOffset int64, sectionLength int64, f *xmlFile) ([]byte, error) { var fileReader io.Reader heapReader := io.NewSectionReader(rat, heapOffset, sectionLength-heapOffset) @@ -544,13 +573,7 @@ func isValidAppFilePath(input string) (string, bool) { // // - If the file is not xar, it returns a ErrInvalidType error // - If the file is not signed, it returns a ErrNotSigned error -func CheckPKGSignature(pkg io.Reader) error { - buff := bytes.NewBuffer(nil) - if _, err := io.Copy(buff, pkg); err != nil { - return err - } - r := bytes.NewReader(buff.Bytes()) - +func CheckPKGSignature(r io.ReaderAt) error { hdr, hashType, err := parseHeader(io.NewSectionReader(r, 0, 28)) if err != nil { return err diff --git a/server/fleet/errors.go b/server/fleet/errors.go index a4821d91a6..da5829569a 100644 --- a/server/fleet/errors.go +++ b/server/fleet/errors.go @@ -646,6 +646,9 @@ const ( // Config InvalidServerURLMsg = `Fleet server URL must use “https” or “http”.` + // macOS setup experience + BootstrapPkgNotDistributionErrMsg = "Couldn’t add. Bootstrap package must be a distribution package. Learn more at: https://fleetdm.com/learn-more-about/macos-distribution-packages" + // NDES/SCEP validation MultipleSCEPPayloadsErrMsg = "Add only one SCEP payload." SCEPVariablesNotInSCEPPayloadErrMsg = "Variables prefixed with \"$FLEET_VAR_SCEP_\", \"$FLEET_VAR_CUSTOM_SCEP_\" and \"$FLEET_VAR_NDES_SCEP\" must only be in the SCEP payload." diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 2a8d80fe5c..cbc0119771 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -3252,6 +3252,9 @@ func (s *integrationMDMTestSuite) TestBootstrapPackage() { s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: unsignedPkg, Name: "pkg.pkg"}, http.StatusBadRequest, "file is not signed") // wrong TOC s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: wrongTOCPkg, Name: "pkg.pkg"}, http.StatusBadRequest, "invalid package") + // not a Distribution package + s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: read("not-distribution-signed.pkg"), Name: "pkg.pkg"}, http.StatusBadRequest, + fleet.BootstrapPkgNotDistributionErrMsg) // successfully upload a package s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: signedPkg, Name: "pkg.pkg", TeamID: 0}, http.StatusOK, "") // check the activity log diff --git a/server/service/testdata/bootstrap-packages/not-distribution-signed.pkg b/server/service/testdata/bootstrap-packages/not-distribution-signed.pkg new file mode 100644 index 0000000000..e123eebf70 Binary files /dev/null and b/server/service/testdata/bootstrap-packages/not-distribution-signed.pkg differ