前面两篇分别介绍了苹果软件安装包证书过期的问题和现实中所做成的麻烦,下面来说说如何应对它。
首先,让我们来回顾一下Apple的建议,它说应该重新到Apple的Download网站中下载该程序包。但是有些问题,Apple并不提供所有软件包的重新打包更新,它只重新发布它认为目前有理由使用的,旧的就没有了;另外,有的软件包你可能不容易得到,比如App Store上的iLife '11等。最后,判断、下载、更新、排故障等等又是好多的工作,对于个人来说,这些工作量并不大,还可以接受;对于企业来说,长远的计划应该如此,救急的情况就不那么实用了。
忽略法:
在企业程序安装中,都会使用命令行,但是目前来说,安装器Installer的命令行中没有忽略证书的选项,不过可以在GUI图形界面中选择忽略,比如在Lion中,安装器Installer会提示你证书过期,你选择继续就还可以正常安装。这个"忽略"的方法对于最终用户和少量软件安装的可以使用,对于企业用户或者众多软件部署安装的情形依然不适用。
过时法:
这里先简单描述一下安装器Installer是如何确认证书过期的,其实简单,安装器Installer获得当前系统日期和时间,然后比对所打开的安装包中的证书有效日期,如果后者比前者时间还早,那么判定证书过期了。
既然证书的过期是根据当前Mac机的系统时间比对证书过期日期来确认的,而我们无法变更证书的过期时间,那么一个想当然的方法就是,不让证书过期,把系统时间改为过去的一个比证书时间还早的时间,比如2012年3月22日,这样Installer应该认为证书没有过期,于是正常安装。通过实践,这个方法可行。
修改系统日期时间当然可以在时间系统偏好中修改,不过还是使用命令行来得直接和方便:
systemsetup -setdate 03:22:12
这个方法可以使用ARD批量发送到所有管理机器上,也可以轻松集成到其他的企业部署工具中,只不过需要注意的是,修改系统时间要在所有软件安装之前,在最后再把时间修正过来。
比如,如果使用网络时间服务同步功能,那么可以用下面语句,让电脑实现同步:
systemsetup -setusingnetworktime on
它虽然可行,但是显然不是一个最好的方法,本人认为只能用来救急。
祛除法:
也就是说有没有简单方便的方法,把一个安装包中的认证祛除掉?
其实,就目前绝大多数的软件安装包都还没有使用证书签名以保证该安装包的完整和原始性,而即便是在Apple已经发布的安装包中,也有好多没有采用。而Apple的安装器至今10.7的Lion版本,也没有强制使用证书认证,所以从安装包中去掉这个证书,并不影响软件的安装。
其实这个过程是相当的简单,对于我们已经确认来源的安装包,使用下面的命令来重新打包,就可以得到一个没有证书的安装包。
pkgutil --expand Expired.pkg /tmp/Expired.pkg pkgutil --flatten /tmp/Expired.pkg ExpiredFixed.pkg
这个过程简单明了,适合批量处理,尤其适合系统管理员。而这种灵活性,也是我之所以喜欢Mac系统的一个原因。
其他的工具:
这里的是一个Guru, Greg编写的, 此人前面介绍过,这个代码检查一个包中的证书是否过期,供大家欣赏:
#!/usr/bin/env python # encoding: utf-8 """ checkPackageSignatures.py """ # Copyright 2012 Greg Neagle. # # 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. # See the License for the specific language governing permissions and # limitations under the License. import sys import os import optparse import plistlib import shutil import subprocess import tempfile from xml.parsers.expat import ExpatError # dmg helpers def getFirstPlist(textString): """Gets the next plist from a text string that may contain one or more text-style plists. Returns a tuple - the first plist (if any) and the remaining string after the plist""" plist_header = '<?xml version' plist_footer = '</plist>' plist_start_index = textString.find(plist_header) if plist_start_index == -1: # not found return ("", textString) plist_end_index = textString.find( plist_footer, plist_start_index + len(plist_header)) if plist_end_index == -1: # not found return ("", textString) # adjust end value plist_end_index = plist_end_index + len(plist_footer) return (textString[plist_start_index:plist_end_index], textString[plist_end_index:]) def DMGhasSLA(dmgpath): '''Returns true if dmg has a Software License Agreement. These dmgs normally cannot be attached without user intervention''' hasSLA = False proc = subprocess.Popen( ['/usr/bin/hdiutil', 'imageinfo', dmgpath, '-plist'], bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() if err: print >> sys.stderr, ( 'hdiutil error %s with image %s.' % (err, dmgpath)) (pliststr, out) = getFirstPlist(out) if pliststr: try: plist = plistlib.readPlistFromString(pliststr) properties = plist.get('Properties') if properties: hasSLA = properties.get('Software License Agreement', False) except ExpatError: pass return hasSLA def mountdmg(dmgpath): """ Attempts to mount the dmg at dmgpath and returns a list of mountpoints Skip verification for speed. """ mountpoints = [] dmgname = os.path.basename(dmgpath) stdin = '' if DMGhasSLA(dmgpath): stdin = 'Y\n' cmd = ['/usr/bin/hdiutil', 'attach', dmgpath, '-mountRandom', '/tmp', '-nobrowse', '-plist', '-noverify', '-owners', 'on'] proc = subprocess.Popen(cmd, bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) (out, err) = proc.communicate(stdin) if proc.returncode: print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname) (pliststr, out) = getFirstPlist(out) if pliststr: plist = plistlib.readPlistFromString(pliststr) for entity in plist['system-entities']: if 'mount-point' in entity: mountpoints.append(entity['mount-point']) return mountpoints def unmountdmg(mountpoint): """ Unmounts the dmg at mountpoint """ proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint], bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (unused_output, err) = proc.communicate() if proc.returncode: print >> sys.stderr, 'Polite unmount failed: %s' % err print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint # try forcing the unmount retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint, '-force']) if retcode: print >> sys.stderr, 'Failed to unmount %s' % mountpoint def str_to_ascii(s): """Given str (unicode, latin-1, or not) return ascii. Args: s: str, likely in Unicode-16BE, UTF-8, or Latin-1 charset Returns: str, ascii form, no >7bit chars """ try: return unicode(s).encode('ascii', 'ignore') except UnicodeDecodeError: return s.decode('ascii', 'ignore') def checkSignature(pkg): proc = subprocess.Popen( ['/usr/sbin/pkgutil', '--check-signature', pkg], stderr=subprocess.PIPE, stdout=subprocess.PIPE) stdout, stderr = proc.communicate() if proc.returncode: if not "Status: no signature" in stdout: lines = stdout.splitlines() try: if "Package" in lines[0] and "Status:" in lines[1]: return "\n".join(lines[0:2]) except IndexError: pass return stdout + stderr return '' def checkDiskImage(dmgpath): mountpoints = mountdmg(dmgpath) if not mountpoints: return # search mounted diskimage for all flat packages. mountpoint = mountpoints[0] printed_dmg_path = False for dirpath, dirnames, filenames in os.walk(mountpoint): for name in filenames: if name.endswith('.pkg'): filepath = os.path.join(dirpath, name) status = checkSignature(filepath) if status: if not printed_dmg_path: printed_dmg_path = True print "%s:" % dmgpath print status unmountdmg(mountpoint) def main(): usage = ('%prog /path/to/check') p = optparse.OptionParser(usage=usage) options, arguments = p.parse_args() if len(arguments) == 0 or len(arguments) > 1: print >> sys.stderr, "Wrong number of parameters!" p.print_usage() exit() path_to_check = arguments[0] if not os.path.exists(path_to_check): print >> sys.stderr, "%s doesn't exist!" % path_to_check if os.path.isfile(path_to_check) and path_to_check.endswith(".dmg"): checkDiskImage(path_to_check) if os.path.isdir(path_to_check): for dirpath, dirnames, filenames in os.walk(path_to_check): for name in filenames: filepath = os.path.join(dirpath, name) if not name.startswith('._') and name.endswith('.pkg'): status = checkSignature(filepath) if status: print "%s:" % filepath print status if not name.startswith('._') and name.endswith('.dmg'): checkDiskImage(filepath) if __name__ == '__main__': main()
过时法结合DeployStudio:
下面的脚本是我在处理reimage过程中遇到证书过期造成Post安装中止问题时使用的,它主要是先变更系统时间,然后执行每个Post安装脚本,修订系统时间后,自动重启:
#!/bin/bash systemsetup -setusingnetworktime off systemsetup -setdate 03:22:12 for eachFile in `ls -w /etc/deploystudio/bin/ds_install_packages_*.sh`; do echo "$eachFile" $eachFile done systemsetup -setusingnetworktime on # shutdown -r now exit 0
结束:
不过,给安装软件以数字证书签名的方式,也已经被广泛的采用,比如Linux和Windows等系统中。在App Store中的软件都已经采用,而且在iOS设备和系统中,也是用来防止盗版的方法。