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:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
+18
@@ -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.{" "}
|
||||
<CustomLink
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/macos-distribution-packages`}
|
||||
text="Learn more"
|
||||
newTab
|
||||
variant="flash-message-link"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
default: {
|
||||
condition: () => false,
|
||||
message: "Couldn’t upload. Please try again.",
|
||||
+49
-26
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user