blob: 9eacc1433a49a116933ea65b1c65079e992b59dd [file] [log] [blame]
import os
import time
import tempfile
import shlex
import shutil
import fnmatch
import re
# TODO: Use util.command
from subprocess import Popen, PIPE, STDOUT, check_call, call
from util.archives import unpacktar, tar_dir, MAR, SEVENZIP
from release.info import fileInfo
import logging
log = logging.getLogger(__name__)
MAC_DESIGNATED_REQUIREMENTS = """\
=designated => ( (anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] ) or (anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = "%(subject_ou)s"))
"""
def signfile(filename, keydir, fake=False, passphrase=None, timestamp=True):
"""Perform authenticode signing on the given file with keys in keydir.
If passphrase is set, it will be sent as stdin to the process.
If fake is True, then don't actually sign anything, just sleep for a
second to simulate signing time.
If timestamp is True, then a signed timestamp will be included with the
signature."""
if fake:
time.sleep(1)
return
basename = os.path.basename(filename)
dirname = os.path.dirname(filename)
stdout = tempfile.TemporaryFile()
command = ['signcode',
'-spc', '%s/MozAuthenticode.spc' % keydir,
'-v', '%s/MozAuthenticode.pvk' % keydir,
]
if timestamp:
command.extend(
['-t', 'http://timestamp.verisign.com/scripts/timestamp.dll'])
command.extend([
'-i', 'http://www.mozilla.com',
'-a', 'sha1',
# Try 5 times, and wait 60 seconds between tries
'-tr', '5',
'-tw', '60',
basename])
try:
log.debug("Running %s", command)
proc = Popen(
command, cwd=dirname, stdout=stdout, stderr=STDOUT, stdin=PIPE)
if passphrase:
proc.stdin.write(passphrase)
proc.stdin.close()
if proc.wait() != 0:
raise ValueError("signcode didn't return with 0")
stdout.seek(0)
data = stdout.read()
# Make sure that the command output "Succeeded". Sometimes signcode
# returns with 0, but doesn't output "Succeeded", which in the past has
# meant that the file has been signed, but is missing a timestmap.
if data.strip() != "Succeeded" and "Success" not in data:
raise ValueError("signcode didn't report success")
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
def osslsigncode_signfile(inputfile, outputfile, keydir, fake=False, passphrase=None, timestamp=None):
"""Perform Authenticode signing on "inputfile", writing a signed version
to "outputfile". See signcode_signfile for a description of other
arguments.
See https://bugzilla.mozilla.org/show_bug.cgi?id=711210#c15 for background
on why we want both methods.
"""
if fake:
time.sleep(1)
return
stdout = tempfile.TemporaryFile()
args = [
'-certs', '%s/MozAuthenticode.spc' % keydir,
'-key', '%s/MozAuthenticode.pvk' % keydir,
'-i', 'http://www.mozilla.com',
'-h', 'sha1',
'-in', inputfile,
'-out', outputfile,
]
if timestamp:
args.extend(['-t', 'http://timestamp.verisign.com/scripts/timestamp.dll'])
try:
import pexpect
proc = pexpect.spawn('osslsigncode', args)
# We use logfile_read because we only want stdout/stderr, _not_ stdin.
proc.logfile_read = stdout
proc.expect('Enter PEM pass phrase')
proc.sendline(passphrase)
proc.wait()
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
def gpg_signfile(filename, sigfile, gpgdir, fake=False, passphrase=None):
"""Sign the given file with the default key from gpgdir. The signature is
written to sigfile.
If fake is True, generate a fake signature and sleep for a bit.
If passphrase is set, it will be passed to gpg on stdin
"""
if fake:
open(sigfile, "wb").write("""
-----BEGIN FAKE SIGNATURE-----
Version: 1.2.3.4
I am ur signature!
-----END FAKE SIGNATURE-----""")
time.sleep(1)
return
command = ['gpg', '--homedir', gpgdir, '-bsa', '-o', sigfile, '-q',
'--batch']
if passphrase:
command.extend(['--passphrase-fd', '0'])
command.append(filename)
log.info('Running %s', command)
stdout = tempfile.TemporaryFile()
try:
proc = Popen(command, stdout=stdout, stderr=STDOUT, stdin=PIPE)
if passphrase:
proc.stdin.write(passphrase)
proc.stdin.close()
if proc.wait() != 0:
raise ValueError("gpg didn't return 0")
stdout.seek(0)
data = stdout.read()
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
def emevoucher_signfile(inputfile, outputfile, key, chain, fake=False, passphrase=None):
"""Perform SMIME signing on "inputfile", writing a signed version
to "outputfile", using passed in "key". This is necessary for the EME voucher.
If fake is True, generate a fake signature and sleep for a bit.
If passphrase is set, it will be passed to gpg on stdin
"""
if fake:
time.sleep(1)
return
stdout = tempfile.TemporaryFile()
args = ['smime', '-sign', '-in', inputfile,
'-out', outputfile, '-signer', key,
'-certfile', chain,
'-md', 'sha256', '-binary', '-nodetach',
'-outform', 'DER']
try:
import pexpect
proc = pexpect.spawn("openssl", args)
# We use logfile_read because we only want stdout/stderr, _not_ stdin.
proc.logfile_read = stdout
proc.expect('Enter pass phrase')
proc.sendline(passphrase)
if proc.wait() != 0:
raise ValueError("openssl didn't return 0")
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
def mar_signfile(inputfile, outputfile, mar_cmd, fake=False, passphrase=None):
# Now sign it
if isinstance(mar_cmd, basestring):
mar_cmd = shlex.split(mar_cmd)
else:
mar_cmd = mar_cmd[:]
command = mar_cmd + [inputfile, outputfile]
log.info('Running %s', command)
stdout = tempfile.TemporaryFile()
try:
proc = Popen(command, stdout=stdout, stderr=STDOUT, stdin=PIPE)
if passphrase:
proc.stdin.write(passphrase)
proc.stdin.write("\n")
proc.stdin.close()
if proc.wait() != 0:
raise ValueError("mar didn't return 0")
stdout.seek(0)
data = stdout.read()
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
def jar_unsignfile(filename):
# Find all the files in META-INF
command = ['unzip', '-l', filename]
stdout = tempfile.TemporaryFile()
log.debug("running %s", command)
proc = Popen(command, stdout=stdout, stderr=STDOUT, stdin=PIPE)
if proc.wait() != 0:
stdout.seek(0)
data = stdout.read()
log.error("unzip output: %s", data)
raise ValueError("Couldn't list zip contents")
stdout.seek(0)
data = stdout.read()
meta_files = re.findall(r'\b(META-INF/.*)$', data, re.I + re.M)
if not meta_files:
# Nothing to do
return
# Now delete them
command = ['zip', filename, '-d'] + meta_files
stdout = tempfile.TemporaryFile()
log.debug("running %s", command)
proc = Popen(command, stdout=stdout, stderr=STDOUT, stdin=PIPE)
# Zip returns with 0 in normal operatoin
# it returns with 12 if it has nothing to do
if proc.wait() not in (0, 12):
stdout.seek(0)
data = stdout.read()
log.error("zip output: %s", data)
raise ValueError("Couldn't remove previous signature")
def jar_signfile(filename, keystore, keyname, fake=False, passphrase=None):
"""Sign a jar file
"""
# unsign first
jar_unsignfile(filename)
command = ["jarsigner", "-keystore", keystore, filename]
if keyname:
command.append(keyname)
stdout = tempfile.TemporaryFile()
try:
log.debug("running %s", command)
proc = Popen(command, stdout=stdout, stderr=STDOUT, stdin=PIPE)
if passphrase:
passphrases = passphrase.split(' ')
for p in passphrases:
proc.stdin.write(p)
proc.stdin.write("\n")
proc.stdin.close()
if proc.wait() != 0:
stdout.seek(0)
data = stdout.read()
log.error("jarsigner output: %s", data)
raise ValueError("jarsigner didn't return 0")
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
def get_bundle_executable(appdir):
"""Return the CFBundleIdentifier from a Mac application."""
import plistlib
return plistlib.readPlist(os.path.join(appdir, 'Contents', 'Info.plist'))['CFBundleExecutable']
def dmg_signfile(filename, keychain, signing_identity, subject_ou, fake=False):
"""Sign a mac .app folder, including handling subapps within.
"""
# Only some directories contain files that get explicitly signed by
# codesign. Others (such as .app/Contents/Resources) have their checksums
# embedded into the CodeResources file that codesign creates when the .app
# is signed at the end.
SIGN_DIRS = ("MacOS", "Library")
log.debug("Signing %s", filename)
app = os.path.basename(filename)
appdir = os.path.dirname(filename)
app_executable = get_bundle_executable(filename)
stdout = tempfile.TemporaryFile()
sign_command = ['codesign',
'-s', signing_identity, '-fv',
'--keychain', keychain,
'--requirement', MAC_DESIGNATED_REQUIREMENTS % locals()]
# We scan "Contents" here to make it easier to find inner apps
# (by looking for things that end with ".app")
contents_d = os.path.join(filename, "Contents")
for top, dirs, files in os.walk(contents_d):
for d in dirs:
abs_d = os.path.join(top, d)
# We only want to sign individual files in certain directories.
# Removing items from "dirs" prevents walk() from descending into
# those directories.
# See SIGN_DIRS above for further explanation.
if top == contents_d and d not in SIGN_DIRS:
log.debug("Skipping %s because it's not in SIGN_DIRS.", abs_d)
dirs.remove(d)
continue
# Make sure to sign any inner apps, too
if d.endswith(".app"):
dmg_signfile(abs_d, keychain, signing_identity, subject_ou, fake)
# None of the individual files in Contents (eg, Info.plist) need
# explicit signing. They get taken care of during the overall .app
# signing, so there's no point in iterating over files if we're still
# in the root directory.
if top == contents_d:
log.debug("Skipping file iteration in %s because it's the root directory.", top)
continue
for f in files:
abs_f = os.path.join(top, f)
# If we find any ".app" after the app we're currently signing
# this means it's an inner app, which was handled above.
# We shouldn't do any further processing here.
if top[len(filename):].count(".app") > 0:
log.debug("Skipping %s because it's part an inner app.", abs_f)
continue
# We also need to skip the main executable, because this gets
# signed with the outer package, which happens below.
if f == app_executable:
log.debug("Skipping %s because it's the main executable.", abs_f)
continue
try:
dir_ = os.path.dirname(abs_f)
log.debug("Signing with command: %s %s", " ".join(sign_command), f)
check_call(sign_command + [f], cwd=dir_, stdout=stdout, stderr=STDOUT)
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
try:
log.debug("Signing with command: %s %s", " ".join(sign_command), app)
check_call(sign_command + [app], cwd=appdir, stdout=stdout, stderr=STDOUT)
except:
stdout.seek(0)
data = stdout.read()
log.exception(data)
raise
def dmg_signpackage(pkgfile, dstfile, keychain, mac_id, subject_ou, fake=False, passphrase=None):
""" Sign a mac build, putting results into `dstfile`.
pkgfile must be a tar, which gets unpacked, signed, and repacked.
"""
# Keep track of our output in a list here, and we can output everything
# when we're done This is to avoid interleaving the output from
# multiple processes.
from flufl.lock import Lock, TimeOutError, NotLockedError
from datetime import timedelta
import pexpect
# TODO: Is it even possible to do 'fake' signing?
logs = []
logs.append("Repacking %s to %s" % (pkgfile, dstfile))
# pexpect requires a string as input
unlock_command = 'security unlock-keychain ' + keychain
lock_command = ['security', 'lock-keychain', keychain]
lockfile = os.path.join(os.path.dirname(keychain), '.lock')
tmpdir = tempfile.mkdtemp()
try:
# Unpack it
logs.append("Unpacking %s to %s" % (pkgfile, tmpdir))
unpacktar(pkgfile, tmpdir)
for macdir in os.listdir(tmpdir):
macdir = os.path.join(tmpdir, macdir)
log.debug('Checking if we should sign %s', macdir)
if shouldSign(macdir, 'mac'):
log.debug('Need to sign %s', macdir)
try:
sign_lock = None
# Acquire a lock for the signing command, to ensure we don't have a
# race condition where one process locks the keychain immediately after another
# unlocks it.
log.debug("Try to acquire %s", lockfile)
sign_lock = Lock(lockfile)
# Put a 30 second timeout on waiting for the lock.
sign_lock.lock(timedelta(0, 30))
# Unlock the keychain so that we do not get a user-interaction prompt to use
# the keychain for signing. This operation requires a password.
child = pexpect.spawn(unlock_command)
child.expect('password to unlock .*')
child.sendline(passphrase)
# read output until child exits
child.read()
child.close()
if child.exitstatus != 0:
raise ValueError("keychain unlock failed")
# Sign the thing!
dmg_signfile(macdir, keychain, mac_id, subject_ou, fake)
except TimeOutError:
# timed out acquiring lock, give an error
log.exception("Timeout acquiring lock %s for codesign, is something broken? ", lockfile)
raise
except:
# catch any other locking error
log.exception("Error acquiring %s for codesign, is something broken?", lockfile)
raise
finally:
# Lock the keychain again, no matter what happens
# This command does not require a password
check_call(lock_command)
# Release the lock, if it was acquired
if sign_lock:
try:
sign_lock.unlock()
log.debug("Release %s", lockfile)
except NotLockedError:
log.debug("%s was already unlocked", lockfile)
# Repack it
logs.append("Packing %s" % dstfile)
tar_dir(dstfile, tmpdir)
except:
log.exception("Error signing %s", pkgfile)
raise
finally:
# Clean up after ourselves, and output our logs
shutil.rmtree(tmpdir)
log.info("\n ".join(logs))
def filterFiles(files, product):
""" Filter out files that we can't sign. Right now this means that
anything that isn't a win32 .exe or .mar file gets filtered out"""
for f in files[:]:
skip = False
try:
info = fileInfo(f, product)
if info['platform'] != 'win32':
skip = True
if info['contents'] not in ('complete', 'installer'):
skip = True
except ValueError:
skip = True
if skip:
files.remove(f)
if 'win32' in f:
if 'xpi' not in f:
log.info("Skipping %s", f)
return files
def sortFiles(files, product, firstLocale):
"""sort files into a specific ordering using the key function defined
within"""
# Utility function for sorting files
# Makes sure that .mar files follow their corresponding .exe files
def fileKey(f):
info = fileInfo(f, product)
locale = info['locale']
leading_path = info['leading_path']
if locale == firstLocale and not leading_path:
localeVal = 0
else:
localeVal = 1
if info['format'] == 'exe':
exeVal = 0
else:
exeVal = 1
return (localeVal, leading_path, locale, exeVal, f)
return sorted(files, key=fileKey)
def sums_are_equal(base_package, packages):
""" Check to make sure that the dictionaries of files:checksums are
exactly equal across a list of packages """
success = True
for signed_file in base_package.keys():
log.debug("comparing file %s", signed_file)
if not len([p for p in packages if p[signed_file] ==
packages[0][signed_file]]) == len(packages):
log.error("%s differs!", signed_file)
success = False
return success
def shouldSign(filename, platform='win32'):
"""Returns True if filename should be signed."""
# These should already be signed by Microsoft.
_dont_sign = [
'D3DCompiler_42.dll', 'd3dx9_42.dll',
'D3DCompiler_43.dll', 'd3dx9_43.dll',
'msvc*.dll',
]
ext = os.path.splitext(filename)[1]
b = os.path.basename(filename)
if platform == 'mac':
if b.endswith('.app'):
return True
elif platform in ('win32', 'win64'):
if ext in ('.dll', '.exe') and not any(fnmatch.fnmatch(b, p) for p in _dont_sign):
return True
else:
# We should never get here.
log.debug("Invalid Platform: %s", platform)
return False
def getChkFile(filename):
_special_files = ['freebl3.dll', 'softokn3.dll', 'nssdbm3.dll']
b = os.path.basename(filename)
if b in _special_files:
d = os.path.dirname(filename)
f = os.path.splitext(b)[0] + '.chk'
return os.path.join(d, f)
return None
def checkTools():
"""Returns True if all of the helper commands ($MAR, $SEVENZIP) are
runnable.
Raises a OSError if they can't be found.
"""
# Check that MAR and SEVENZIP are executable
null = open(os.devnull, "w")
try:
call([MAR, '-h'], stdout=null)
except OSError:
raise OSError("mar must be in your $PATH, or set via $MAR")
try:
call([SEVENZIP, '-h'], stdout=null)
except OSError:
raise OSError("7z must be in your $PATH, or set via $SEVENZIP")