Package SHA-256 Hash verification

git-svn-id: http://munki.googlecode.com/svn/branches/pkg-hash-verification@699 a4e17f2e-e282-11dd-95e1-755cbddbdd66
This commit is contained in:
Justin McWilliams
2010-08-31 15:15:47 +00:00
parent 0d3f9b9ed9
commit bf8cb1b7eb
3 changed files with 515 additions and 399 deletions

View File

@@ -395,8 +395,10 @@ def main():
if item and os.path.exists(item):
# get size of installer item
itemsize = 0
itemhash = "N/A"
if os.path.isfile(item):
itemsize = int(os.path.getsize(item))
itemhash = munkicommon.getsha256hash(item)
if os.path.isdir(item):
# need to walk the dir and add it all up
for (path, dirs, files) in os.walk(item):
@@ -435,6 +437,8 @@ def main():
exit(-1)
catinfo['installer_item_size'] = int(itemsize/1024)
catinfo['installer_item_hash'] = itemhash
# try to generate the correct item location
temppath = item
location = ""
@@ -469,7 +473,9 @@ def main():
location = os.path.split(uninstallerpath)[1]
catinfo['uninstaller_item_location'] = location
itemsize = int(os.path.getsize(uninstallerpath))
itemhash = munkicommon.getsha256hash(uninstallerpath)
catinfo['uninstaller_item_size'] = int(itemsize/1024)
catinfo['uninstaller_item_hash'] = itemhash
else:
print >> sys.stderr, "No uninstaller at %s" % \
uninstallerpath

View File

@@ -6,9 +6,9 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#
# http://www.apache.org/licenses/LICENSE-2.0
#
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,14 +22,14 @@ Created by Greg Neagle on 2008-11-18.
Common functions used by the munki tools.
"""
import sys
import os
import time
import subprocess
import tempfile
import shutil
import urllib2
import hashlib
import os
import shutil
import subprocess
import sys
import tempfile
import time
import urllib2
from distutils import version
from xml.dom import minidom
@@ -112,8 +112,8 @@ def display_info(msg):
elif verbose > 0:
print msg.encode('UTF-8')
sys.stdout.flush()
def display_detail(msg):
"""
Displays minor info messages, formatting as needed
@@ -128,8 +128,8 @@ def display_detail(msg):
sys.stdout.flush()
if pref('LoggingLevel') > 0:
log(msg)
def display_debug1(msg):
"""
Displays debug messages, formatting as needed
@@ -155,8 +155,8 @@ def display_debug2(msg):
print msg.encode('UTF-8')
if pref('LoggingLevel') > 2:
log("DEBUG2: %s" % msg)
def reset_warnings():
'''Rotate our warnings log.'''
warningsfile = os.path.join(os.path.dirname(pref("LogFile")),
@@ -216,8 +216,8 @@ def log(msg, logname=''):
fileobj.close()
except (OSError, IOError):
pass
def rotatelog(logname=''):
'''Rotate a log'''
if not logname:
@@ -267,19 +267,19 @@ def printreportitem(label, value, indent=0):
printreportitem(subkey, value[subkey], indent+1)
else:
print indentspace*indent, "%s: %s" % (label, value)
def printreport(reportdict):
"""Prints the report dictionary in a pretty(?) way"""
for key in reportdict.keys():
printreportitem(key, reportdict[key])
def savereport():
'''Save our report'''
FoundationPlist.writePlist(report,
FoundationPlist.writePlist(report,
os.path.join(pref('ManagedInstallDir'), "ManagedInstallReport.plist"))
def archive_report():
'''Archive a report'''
@@ -302,13 +302,13 @@ def archive_report():
except (OSError, IOError):
display_warning("Could not archive report.")
# now keep number of archived reports to 100 or fewer
proc = subprocess.Popen(['/bin/ls', '-t1', archivepath],
bufsize=1, stdout=subprocess.PIPE,
proc = subprocess.Popen(['/bin/ls', '-t1', archivepath],
bufsize=1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(output, err) = proc.communicate()
if output:
archiveitems = [item
for item in str(output).splitlines()
archiveitems = [item
for item in str(output).splitlines()
if item.startswith("ManagedInstallReport-")]
if len(archiveitems) > 100:
for item in archiveitems[100:]:
@@ -319,9 +319,9 @@ def archive_report():
except (OSError, IOError):
display_warning("Could not remove archive item %s"
% itempath)
# misc functions
def validPlist(path):
@@ -335,7 +335,7 @@ def validPlist(path):
def stopRequested():
"""Allows user to cancel operations when
"""Allows user to cancel operations when
MunkiStatus is being used"""
if munkistatusoutput:
if munkistatus.getStopButtonState() == 1:
@@ -354,8 +354,8 @@ def getconsoleuser():
def currentGUIusers():
'''Gets a list of GUI users by parsing the output of /usr/bin/who'''
gui_users = []
proc = subprocess.Popen("/usr/bin/who", shell=False,
stdin=subprocess.PIPE,
proc = subprocess.Popen("/usr/bin/who", shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = proc.communicate()
lines = str(output).splitlines()
@@ -363,15 +363,15 @@ def currentGUIusers():
if "console" in line:
parts = line.split()
gui_users.append(parts[0])
return gui_users
def pythonScriptRunning(scriptname):
'''Returns Process ID for a running python script'''
cmd = ['/bin/ps', '-eo', 'pid=,command=']
proc = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE,
proc = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = proc.communicate()
mypid = os.getpid()
@@ -379,28 +379,28 @@ def pythonScriptRunning(scriptname):
for line in lines:
(pid, process) = line.split(None, 1)
# first look for Python processes
if (process.find("MacOS/Python ") != -1 or
if (process.find("MacOS/Python ") != -1 or
process.find("python ") != -1):
if process.find(scriptname) != -1:
if int(pid) != int(mypid):
return pid
return 0
def osascript(osastring):
'''Wrapper to run AppleScript commands'''
cmd = ['/usr/bin/osascript', '-e', osastring]
proc = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE,
proc = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = proc.communicate()
if proc.returncode != 0:
print >> sys.stderr, "Error: ", err
if out:
return str(out).decode('UTF-8').rstrip("\n")
# dmg helpers
def mountdmg(dmgpath, use_shadow=False):
@@ -411,12 +411,12 @@ def mountdmg(dmgpath, use_shadow=False):
"""
mountpoints = []
dmgname = os.path.basename(dmgpath)
cmd = ['/usr/bin/hdiutil', 'attach', dmgpath,
cmd = ['/usr/bin/hdiutil', 'attach', dmgpath,
'-mountRandom', '/tmp', '-nobrowse', '-plist']
if use_shadow:
cmd.append('-shadow')
proc = subprocess.Popen(cmd,
bufsize=1, stdout=subprocess.PIPE,
bufsize=1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(pliststr, err) = proc.communicate()
if err:
@@ -435,36 +435,59 @@ def unmountdmg(mountpoint):
Unmounts the dmg at mountpoint
"""
proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
bufsize=1, stdout=subprocess.PIPE,
bufsize=1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(ouptut, err) = proc.communicate()
if proc.returncode:
display_warning("Attempting to force unmount %s" % mountpoint)
# try forcing the unmount
retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint,
retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint,
'-force'])
if retcode:
display_warning("Failed to unmount %s" % mountpoint)
def gethash(filename, hash_function):
"""
Calculates the hashvalue of the given file with the given hash_function.
Args:
filename: The file name to calculate the hash value of.
hash_function: The hash function object to use, which was instanciated
before calling this function, e.g. hashlib.md5().
Returns:
The hashvalue of the given file as hex string.
"""
if not os.path.isfile(filename):
return "NOT A FILE"
f = open(filename, 'rb')
while 1:
chunk = f.read(2**16)
if not chunk:
break
hash_function.update(chunk)
f.close()
return hash_function.hexdigest()
def getmd5hash(filename):
"""
Returns hex of MD5 checksum of a file
"""
if not os.path.isfile(filename):
return "NOT A FILE"
hash_function = hashlib.md5()
return gethash(filename, hash_function)
def getsha256hash(filename):
"""
Returns the SHA-256 hash value of a file as a hex string.
"""
hash_function = hashlib.sha256()
return gethash(filename, hash_function)
fileobj = open(filename, 'rb')
md5hash = hashlib.md5()
while 1:
chunk = fileobj.read(2**16)
if not chunk:
break
md5hash.update(chunk)
fileobj.close()
return md5hash.hexdigest()
def isApplication(pathname):
'''Returns true if path appears to be an OS X application'''
# No symlinks, please
@@ -481,11 +504,11 @@ def isApplication(pathname):
if 'CFBundlePackageType' in plist:
if plist['CFBundlePackageType'] != 'APPL':
return False
# get CFBundleExecutable,
# get CFBundleExecutable,
# falling back to bundle name if it's missing
bundleexecutable = plist.get('CFBundleExecutable',
bundleexecutable = plist.get('CFBundleExecutable',
os.path.basename(pathname))
bundleexecutablepath = os.path.join(pathname, "Contents",
bundleexecutablepath = os.path.join(pathname, "Contents",
"MacOS", bundleexecutable)
if os.path.exists(bundleexecutablepath):
return True
@@ -523,16 +546,16 @@ def prefs():
_prefs['SuppressUserNotification'] = False
_prefs['SuppressAutoInstall'] = False
_prefs['SuppressStopButtonOnInstall'] = False
prefsfile = "/Library/Preferences/ManagedInstalls.plist"
plist = {}
if os.path.exists(prefsfile):
try:
plist = FoundationPlist.readPlist(prefsfile)
except FoundationPlist.NSPropertyListSerializationException:
display_error("ERROR: Could not read preferences file %s."
display_error("ERROR: Could not read preferences file %s."
% prefsfile)
raise Exception("Could not read preferences file %s." %
raise Exception("Could not read preferences file %s." %
prefsfile)
try:
for key in plist.keys():
@@ -542,33 +565,33 @@ def prefs():
else:
_prefs[key] = plist[key]
except AttributeError:
display_error("ERROR: Prefs file %s contains invalid data."
display_error("ERROR: Prefs file %s contains invalid data."
% prefsfile)
raise Exception("Preferences file %s invalid." % prefsfile)
else:
# no prefs file, so we'll write out a "default" prefs file
del _prefs['LastNotifiedDate']
FoundationPlist.writePlist(_prefs, prefsfile)
return _prefs
def pref(prefname):
'''Return a prefernce'''
return prefs().get(prefname,'')
#####################################################
#####################################################
# Apple package utilities
#####################################################
def getInstallerPkgInfo(filename):
"""Uses Apple's installer tool to get basic info
"""Uses Apple's installer tool to get basic info
about an installer item."""
installerinfo = {}
proc = subprocess.Popen(["/usr/sbin/installer", "-pkginfo", "-verbose",
"-plist", "-pkg", filename],
bufsize=1, stdout=subprocess.PIPE,
proc = subprocess.Popen(["/usr/sbin/installer", "-pkginfo", "-verbose",
"-plist", "-pkg", filename],
bufsize=1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = proc.communicate()
@@ -584,7 +607,7 @@ def getInstallerPkgInfo(filename):
pass
if plist:
break
if plist:
if plist:
if 'Size' in plist:
installerinfo['installed_size'] = int(plist['Size'])
installerinfo['description'] = plist.get('Description',"")
@@ -592,21 +615,21 @@ def getInstallerPkgInfo(filename):
installerinfo['RestartAction'] = "RequireRestart"
if "Title" in plist:
installerinfo['display_name'] = plist['Title']
proc = subprocess.Popen(["/usr/sbin/installer",
"-query", "RestartAction",
"-pkg", filename],
bufsize=1,
stdout=subprocess.PIPE,
proc = subprocess.Popen(["/usr/sbin/installer",
"-query", "RestartAction",
"-pkg", filename],
bufsize=1,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = proc.communicate()
if out:
restartAction = str(out).rstrip('\n')
if restartAction != 'None':
installerinfo['RestartAction'] = restartAction
return installerinfo
def padVersionString(versString, tupleCount):
'''Normalize the format of a version string'''
@@ -619,7 +642,7 @@ def padVersionString(versString, tupleCount):
while len(components) < tupleCount :
components.append("0")
return ".".join(components)
def getVersionString(plist):
'''Gets a version string from the plist.
@@ -650,7 +673,7 @@ def getVersionString(plist):
# replace commas with periods
CFBundleVersion = CFBundleVersion.replace(',','.')
return CFBundleVersion
return ''
@@ -665,9 +688,9 @@ def getExtendedVersion(bundlepath):
versionstring = getVersionString(plist)
if versionstring:
return padVersionString(versionstring, 5)
# no version number in Info.plist. Maybe old-style package?
infopath = os.path.join(bundlepath, "Contents", "Resources",
infopath = os.path.join(bundlepath, "Contents", "Resources",
"English.lproj")
if os.path.exists(infopath):
for item in os.listdir(infopath):
@@ -683,13 +706,13 @@ def getExtendedVersion(bundlepath):
label = parts[0]
if label == "Version":
return padVersionString(parts[1], 5)
# didn't find a version number, so return 0...
return "0.0.0.0.0"
def parsePkgRefs(filename):
"""Parses a .dist or PackageInfo file looking for pkg-ref or pkg-info tags
"""Parses a .dist or PackageInfo file looking for pkg-ref or pkg-info tags
to get info on included sub-packages"""
info = []
dom = minidom.parse(filename)
@@ -702,7 +725,7 @@ def parsePkgRefs(filename):
# for key in keys:
# print key, "=>", \
# ref.attributes[key].value.encode('UTF-8')
pkginfo = {}
pkginfo['packageid'] = \
ref.attributes['id'].value.encode('UTF-8')
@@ -724,7 +747,7 @@ def parsePkgRefs(filename):
# for key in keys:
# print key, "=>", \
# ref.attributes[key].value.encode('UTF-8')
pkginfo = {}
pkginfo['packageid'] = \
ref.attributes['identifier'].value.encode('UTF-8')
@@ -751,14 +774,14 @@ def getFlatPackageInfo(pkgpath):
cwd = os.getcwd()
# change into our tmpdir so we can use xar to unarchive the flat package
os.chdir(pkgtmp)
returncode = subprocess.call(["/usr/bin/xar", "-xf", abspkgpath,
returncode = subprocess.call(["/usr/bin/xar", "-xf", abspkgpath,
"--exclude", "Payload"])
if returncode == 0:
currentdir = pkgtmp
packageinfofile = os.path.join(currentdir, "PackageInfo")
if os.path.exists(packageinfofile):
infoarray = parsePkgRefs(packageinfofile)
if not infoarray:
# didn't get any packageid info or no PackageInfo file
# look for subpackages at the top level
@@ -768,14 +791,14 @@ def getFlatPackageInfo(pkgpath):
packageinfofile = os.path.join(itempath, "PackageInfo")
if os.path.exists(packageinfofile):
infoarray.extend(parsePkgRefs(packageinfofile))
if not infoarray:
# found no PackageInfo files and no subpackages,
# so let's look at the Distribution file
distributionfile = os.path.join(currentdir, "Distribution")
if os.path.exists(distributionfile):
infoarray = parsePkgRefs(distributionfile)
# change back to original working dir
os.chdir(cwd)
shutil.rmtree(pkgtmp)
@@ -797,15 +820,15 @@ def getOnePackageInfo(pkgpath):
pkginfo['packageid'] = plist["Bundle identifier"]
else:
pkginfo['packageid'] = os.path.basename(pkgpath)
if "CFBundleName" in plist:
pkginfo['name'] = plist["CFBundleName"]
if "IFPkgFlagInstalledSize" in plist:
pkginfo['installed_size'] = plist["IFPkgFlagInstalledSize"]
pkginfo['version'] = getExtendedVersion(pkgpath)
except (AttributeError,
except (AttributeError,
FoundationPlist.NSPropertyListSerializationException):
pkginfo['packageid'] = "BAD PLIST in %s" % \
os.path.basename(pkgpath)
@@ -849,7 +872,7 @@ def getText(nodelist):
def getBundlePackageInfo(pkgpath):
'''Get metadata from a bundle-style package'''
infoarray = []
if pkgpath.endswith(".pkg"):
pkginfo = getOnePackageInfo(pkgpath)
if pkginfo:
@@ -860,7 +883,7 @@ def getBundlePackageInfo(pkgpath):
if os.path.exists(bundlecontents):
for item in os.listdir(bundlecontents):
if item.endswith(".dist"):
filename = os.path.join(bundlecontents, item)
filename = os.path.join(bundlecontents, item)
dom = minidom.parse(filename)
pkgrefs = dom.getElementsByTagName("pkg-ref")
if pkgrefs:
@@ -874,10 +897,10 @@ def getBundlePackageInfo(pkgpath):
pkginfo = getBundlePackageInfo(subpkgpath)
if pkginfo:
infoarray.extend(pkginfo)
if infoarray:
return infoarray
# no .dist file found, look for packages in subdirs
dirsToSearch = []
plistpath = os.path.join(pkgpath, "Contents", "Info.plist")
@@ -886,8 +909,8 @@ def getBundlePackageInfo(pkgpath):
if 'IFPkgFlagComponentDirectory' in plist:
componentdir = plist['IFPkgFlagComponentDirectory']
dirsToSearch.append(componentdir)
if dirsToSearch == []:
if dirsToSearch == []:
dirsToSearch = ['', 'Contents', 'Contents/Installers',
'Contents/Packages', 'Contents/Resources',
'Contents/Resources/Packages']
@@ -905,18 +928,18 @@ def getBundlePackageInfo(pkgpath):
pkginfo = getBundlePackageInfo(itempath)
if pkginfo:
infoarray.extend(pkginfo)
if infoarray:
return infoarray
else:
# couldn't find any subpackages,
# couldn't find any subpackages,
# just return info from the .dist file
# if it exists
for item in os.listdir(bundlecontents):
if item.endswith(".dist"):
distfile = os.path.join(bundlecontents, item)
infoarray.extend(parsePkgRefs(distfile))
return infoarray
@@ -930,10 +953,10 @@ def getReceiptInfo(pkgname):
if os.path.isdir(pkgname): # bundle-style package?
info = getBundlePackageInfo(pkgname)
elif pkgname.endswith('.dist'):
info = parsePkgRefs(pkgname)
return info
@@ -944,13 +967,13 @@ def getInstalledPackageVersion(pkgid):
Returns the version string of the installed pkg
if it exists, or an empty string if it does not
"""
# First check (Leopard and later) package database
proc = subprocess.Popen(["/usr/sbin/pkgutil",
"--pkg-info-plist", pkgid],
bufsize=1,
stdout=subprocess.PIPE,
proc = subprocess.Popen(["/usr/sbin/pkgutil",
"--pkg-info-plist", pkgid],
bufsize=1,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = proc.communicate()
@@ -962,16 +985,16 @@ def getInstalledPackageVersion(pkgid):
foundbundleid = plist["pkgid"]
if "pkg-version" in plist:
foundvers = plist["pkg-version"]
if pkgid == foundbundleid:
display_debug2("\tThis machine has %s, version %s" %
display_debug2("\tThis machine has %s, version %s" %
(pkgid, foundvers))
return padVersionString(foundvers, 5)
except (AttributeError,
FoundationPlist.NSPropertyListSerializationException):
pass
# If we got to this point, we haven't found the pkgid yet.
# If we got to this point, we haven't found the pkgid yet.
# Check /Library/Receipts
receiptsdir = "/Library/Receipts"
if os.path.exists(receiptsdir):
@@ -988,18 +1011,18 @@ def getInstalledPackageVersion(pkgid):
if version.LooseVersion(foundvers) > \
version.LooseVersion(highestversion):
highestversion = foundvers
if highestversion != "0":
display_debug2("\tThis machine has %s, version %s" %
display_debug2("\tThis machine has %s, version %s" %
(pkgid, highestversion))
return highestversion
# This package does not appear to be currently installed
display_debug2("\tThis machine does not have %s" % pkgid)
return ""
def nameAndVersion(aString):
"""
Splits a string into the name and version numbers:
@@ -1031,7 +1054,7 @@ def findInstallerItem(path):
for item in os.listdir(path):
if item.endswith('.pkg'):
return path
# we didn't find a pkg at this level
# look for a Packages dir
path = os.path.join(path,"Packages")
@@ -1058,19 +1081,19 @@ def getPackageMetaData(pkgitem):
name
version
description
receipts: an array of packageids that may be installed
receipts: an array of packageids that may be installed
(some may not be installed on some machines)
"""
pkgitem = findInstallerItem(pkgitem)
if pkgitem == None:
return {}
# first get the data /usr/sbin/installer will give us
# first get the data /usr/sbin/installer will give us
installerinfo = getInstallerPkgInfo(pkgitem)
# now look for receipt/subpkg info
receiptinfo = getReceiptInfo(pkgitem)
name = os.path.split(pkgitem)[1]
shortname = os.path.splitext(name)[0]
metaversion = getExtendedVersion(pkgitem)
@@ -1086,43 +1109,43 @@ def getPackageMetaData(pkgitem):
if "installed_size" in infoitem:
# note this is in KBytes
installedsize += infoitem['installed_size']
if metaversion == "0.0.0.0.0":
metaversion = highestpkgversion
elif len(receiptinfo) == 1:
# there is only one package in this item
metaversion = highestpkgversion
elif highestpkgversion.startswith(metaversion):
# for example, highestpkgversion is 2.0.3124.0,
# for example, highestpkgversion is 2.0.3124.0,
# version in filename is 2.0
metaversion = highestpkgversion
cataloginfo = {}
cataloginfo['name'] = nameAndVersion(shortname)[0]
cataloginfo['version'] = metaversion
for key in ('display_name', 'RestartAction', 'description'):
if key in installerinfo:
cataloginfo[key] = installerinfo[key]
if 'installed_size' in installerinfo:
if installerinfo['installed_size'] > 0:
cataloginfo['installed_size'] = installerinfo['installed_size']
elif installedsize:
cataloginfo['installed_size'] = installedsize
cataloginfo['receipts'] = receiptinfo
cataloginfo['receipts'] = receiptinfo
return cataloginfo
# some utility functions
def getAvailableDiskSpace(volumepath="/"):
'''Returns available diskspace in KBytes.'''
cmd = ["/usr/sbin/diskutil", "info", "-plist", volumepath]
proc = subprocess.Popen(cmd,
proc = subprocess.Popen(cmd,
bufsize=1,
stdout=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = proc.communicate()
if out:
@@ -1132,7 +1155,7 @@ def getAvailableDiskSpace(volumepath="/"):
if "FreeSpace" in plist:
# plist["FreeSpace"] is in bytes
return int(plist["FreeSpace"]/1024)
except (AttributeError,
FoundationPlist.NSPropertyListSerializationException):
pass
@@ -1146,12 +1169,12 @@ def cleanUpTmpDir():
global tmpdir
if tmpdir:
try:
shutil.rmtree(tmpdir)
shutil.rmtree(tmpdir)
except (OSError, IOError):
pass
tmpdir = None
# module globals
#debug = False
verbose = 1

File diff suppressed because it is too large Load Diff