Permalink
Find file
executable file 230 lines (192 sloc) 8 KB
#!/usr/bin/python
"""signtool.py [options] file [file ...]
If no include patterns are specified, all files will be considered. -i/-x only
have effect when signing entire directories."""
from collections import defaultdict
import os
import sys
import site
# Modify our search path to find our modules
site.addsitedir(os.path.join(os.path.dirname(__file__), "../../lib/python"))
from signing.client import remote_signfile, buildValidatingOpener
from util.archives import packtar, unpacktar
from util.paths import findfiles
import logging
log = logging.getLogger(__name__)
import pefile
def is_authenticode_signed(filename):
"""Returns True if the file is signed with authenticode"""
p = None
try:
p = pefile.PE(filename)
# Look for a 'IMAGE_DIRECTORY_ENTRY_SECURITY' entry in the optinal data
# directory
for d in p.OPTIONAL_HEADER.DATA_DIRECTORY:
if d.name == 'IMAGE_DIRECTORY_ENTRY_SECURITY' and d.VirtualAddress != 0:
return True
return False
except:
log.exception("Problem parsing file")
return False
finally:
if p:
p.close()
def main():
allowed_formats = ("sha2signcode", "sha2signcodestub", "signcode",
"osslsigncode", "gpg", "mar", "mar_sha384", "dmg",
"dmgv2", "jar", "emevoucher")
from optparse import OptionParser
import random
parser = OptionParser(__doc__)
parser.set_defaults(
hosts=[],
cert=None,
log_level=logging.INFO,
output_dir=None,
output_file=None,
formats=[],
includes=[],
excludes=[],
nsscmd=None,
tokenfile=None,
noncefile=None,
cachedir=None,
)
parser.add_option(
"-H", "--host", dest="hosts", action="append", help="format[:format]:hostname[:port]")
parser.add_option("-c", "--server-cert", dest="cert")
parser.add_option("-t", "--token-file", dest="tokenfile",
help="file where token is stored")
parser.add_option("-n", "--nonce-file", dest="noncefile",
help="file where nonce is stored")
parser.add_option("-d", "--output-dir", dest="output_dir",
help="output directory; if not set then files are replaced with signed copies")
parser.add_option("-o", "--output-file", dest="output_file",
help="output file; if not set then files are replaced with signed copies. This can only be used when signing a single file")
parser.add_option("-f", "--formats", dest="formats", action="append",
help="signing formats (one or more of %s)" % ", ".join(allowed_formats))
parser.add_option("-q", "--quiet", dest="log_level", action="store_const",
const=logging.WARN)
parser.add_option(
"-v", "--verbose", dest="log_level", action="store_const",
const=logging.DEBUG)
parser.add_option("-i", "--include", dest="includes", action="append",
help="add to include patterns")
parser.add_option("-x", "--exclude", dest="excludes", action="append",
help="add to exclude patterns")
parser.add_option("--nsscmd", dest="nsscmd",
help="command to re-sign nss libraries, if required")
parser.add_option("--cachedir", dest="cachedir",
help="local cache directory")
# TODO: Concurrency?
# TODO: Different certs per server?
options, args = parser.parse_args()
logging.basicConfig(
level=options.log_level, format="%(asctime)s - %(message)s")
if not options.hosts:
parser.error("at least one host is required")
if not options.cert:
parser.error("certificate is required")
if not os.path.exists(options.cert):
parser.error("certificate not found")
if not options.tokenfile:
parser.error("token file is required")
if not options.noncefile:
parser.error("nonce file is required")
# Covert nsscmd to win32 path if required
if sys.platform == 'win32' and options.nsscmd:
nsscmd = options.nsscmd.strip()
if nsscmd.startswith("/"):
drive = nsscmd[1]
options.nsscmd = "%s:%s" % (drive, nsscmd[2:])
# Handle format
formats = []
for fmt in options.formats:
if "," in fmt:
for fmt in fmt.split(","):
if fmt not in allowed_formats:
parser.error("invalid format: %s" % fmt)
formats.append(fmt)
elif fmt not in allowed_formats:
parser.error("invalid format: %s" % fmt)
else:
formats.append(fmt)
# bug 1164456
# GPG signing must happen last because it will be invalid if done prior to
# any format that modifies the file in-place.
if "gpg" in formats:
formats.remove("gpg")
formats.append("gpg")
if options.output_file and (len(args) > 1 or os.path.isdir(args[0])):
parser.error(
"-o / --output-file can only be used when signing a single file")
if options.output_dir:
if os.path.exists(options.output_dir):
if not os.path.isdir(options.output_dir):
parser.error(
"output_dir (%s) must be a directory", options.output_dir)
else:
os.makedirs(options.output_dir)
if not options.includes:
# Do everything!
options.includes.append("*")
if not formats:
parser.error("no formats specified")
format_urls = defaultdict(list)
for h in options.hosts:
# The last two parts of a host is the actual hostname:port. Any parts
# before that are formats - there could be 0..n formats so this is
# tricky to split.
parts = h.split(":")
h = parts[-2:]
fmts = parts[:-2]
# If no formats are specified, the host is assumed to support all of them.
if not fmts:
fmts = formats
for f in fmts:
format_urls[f].append("https://%s" % ":".join(h))
missing_fmt_hosts = set(formats) - set(format_urls.keys())
if missing_fmt_hosts:
parser.error("no hosts capable of signing formats: %s" % " ".join(missing_fmt_hosts))
log.debug("in %s", os.getcwd())
buildValidatingOpener(options.cert)
token = open(options.tokenfile, 'rb').read()
for fmt in formats:
urls = format_urls[fmt]
random.shuffle(urls)
# The only difference between dmg and dmgv2 are the servers they use.
# The server side code only understands "dmg" as a format, so we need
# to translate this now that we've chosen our URLs
if fmt == "dmgv2":
fmt = "dmg"
log.debug("doing %s signing", fmt)
log.debug("possible hosts are %s" % urls)
files = []
# We want to package the ".app" file in a tar for mac signing.
if fmt == "dmg":
for fd in args:
packtar(fd + '.tar.gz', [fd], os.getcwd())
files.append(fd + '.tar.gz')
# For other platforms we sign all of the files individually.
else:
files = findfiles(args, options.includes, options.excludes)
for f in files:
log.debug("%s", f)
log.debug("checking %s for signature...", f)
if fmt in ('sha2signcode', 'sha2signcodestub', 'signcode', 'osslsigncode') and is_authenticode_signed(f):
log.info("Skipping %s because it looks like it's already signed", f)
continue
if options.output_dir:
dest = os.path.join(options.output_dir, os.path.basename(f))
else:
dest = None
if not remote_signfile(options, urls, f, fmt, token, dest):
log.error("Failed to sign %s with %s", f, fmt)
sys.exit(1)
if fmt == "dmg":
for fd in args:
log.debug("unpacking %s", fd)
unpacktar(fd + '.tar.gz', os.getcwd())
os.unlink(fd + '.tar.gz')
if __name__ == '__main__':
main()