Mac:如何应对证书过期

前面两篇分别介绍了苹果软件安装包证书过期的问题和现实中所做成的麻烦,下面来说说如何应对它。

首先,让我们来回顾一下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设备和系统中,也是用来防止盗版的方法。


你可能感兴趣的:(mac)