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
This commit is contained in:
Victor Lyuboslavsky
2025-05-06 11:02:13 -05:00
committed by GitHub
parent 06399a2685
commit 8ad9da71d6
7 changed files with 97 additions and 31 deletions
@@ -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.
+23 -5
View File
@@ -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
@@ -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:
"Couldnt 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&apos;t upload. Bootstrap package must be a distribution package.{" "}
<CustomLink
url={`${LEARN_MORE_ABOUT_BASE_LINK}/macos-distribution-packages`}
text="Learn more"
newTab
variant="flash-message-link"
/>
</>
),
},
default: {
condition: () => false,
message: "Couldnt upload. Please try again.",
+49 -26
View File
@@ -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
+3
View File
@@ -646,6 +646,9 @@ const (
// Config
InvalidServerURLMsg = `Fleet server URL must use “https” or “http”.`
// macOS setup experience
BootstrapPkgNotDistributionErrMsg = "Couldnt 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."
+3
View File
@@ -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