From 8ad9da71d6bfa608d43488fd89e0c9bb054058ee Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 6 May 2025 11:02:13 -0500 Subject: [PATCH] Only allow distribution packages for bootstrap package (#28787) For #27700 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. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality --- ...700-validate-bootstrap-is-distribution-pkg | 1 + ee/server/service/mdm.go | 28 +++++-- .../{helpers.ts => helpers.tsx} | 18 +++++ pkg/file/xar.go | 75 ++++++++++++------ server/fleet/errors.go | 3 + server/service/integration_mdm_test.go | 3 + .../not-distribution-signed.pkg | Bin 0 -> 16384 bytes 7 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 changes/27700-validate-bootstrap-is-distribution-pkg rename frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageUploader/{helpers.ts => helpers.tsx} (60%) create mode 100644 server/service/testdata/bootstrap-packages/not-distribution-signed.pkg 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 0000000000000000000000000000000000000000..e123eebf70681e8548711491affb69717abbb78e GIT binary patch literal 16384 zcmeIZ1yoe++CMx)4BZGw4kZnnlzwcV)*070-uJ#^?|WbIyMEWDhlvX;azz+VtI7`31-7xrxy9Wf5t0XcT%z84tr_dDZQ-&7k|nQ9;H@U6V1 z{F*#c`)zXJ@X<@!;~9y`x??zox|_knyAn-)Qdtrq`FByRulk+UmN#>KT)GgAr4~Ng z7d+YT(-J@4TX`=L;(%=7V?E$|94?7|j!J&64;)-qIgOFV8>EqQNKEMKUE4d?oZOc? zK5%_pxxF;gUJ%9_x%$czWiYeYA=k}|io`Er5#!-l|ggN~MU|_I;b7_1lqT&rXVR;X+Er zs$*d@bk||yu|7ibm_kO#9W0gc#iwObE%cV-Mn2YqY|^yMfp?DcJ)I8aEVjCW!i-

$44IW?b+pzDV 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 literal 0 HcmV?d00001