Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mumble-voip/mumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavide Beatrici <git@davidebeatrici.dev>2022-01-18 03:25:14 +0300
committerDavide Beatrici <git@davidebeatrici.dev>2022-01-18 03:25:14 +0300
commite4c3959f3625e7b1ad45bded4a792c9b184ead62 (patch)
treea9ad6b8f00d2a1c12f461845d8243b55c0706aa2 /scripts
parent5574cc8e2462349ae953a9d0a4ed2dbe7dfeeba9 (diff)
MAINT: Add script to sign macOS binaries and packages
Right now we are using "osxdist.py" to generate a DMG package from scratch. The script also takes care of signing the content. However, our "mumble-releng" repository has had a dedicated script called "sign-dmg.py" since 2013. In fact, that's what we have always been using to sign the app bundle. As we're planning to eventually ditch "osxdist.py" in favor of CPack's generator, we decided to complete "sign-dmg.py". The script now: - Takes care of signing all binaries in the bundle in addition to it, effectively replacing "osxdist.py". - Supports single binaries, replacing "sign-mach-o.py" (which only called codesign() from "sign-dmg.py"). - Supports PKG packages. - Provides a "--config" option to specify the path to the configuration file. Default: "$HOME/.sign_macOS.cfg". - Provides a "--entitlements" option to specify the path to the plist file containing the requested capabilities. This is mandatory for notarization to succeed. More specifically, the "--options runtime" codesign parameter is. The parameter is not passed if this option is not used because it would cause Mumble to crash upon audio input use. The name is changed to "sign_macOS.py" as the script does not only handle DMG packages anymore.
Diffstat (limited to 'scripts')
-rw-r--r--scripts/sign_macOS.py332
1 files changed, 332 insertions, 0 deletions
diff --git a/scripts/sign_macOS.py b/scripts/sign_macOS.py
new file mode 100644
index 000000000..406092ebe
--- /dev/null
+++ b/scripts/sign_macOS.py
@@ -0,0 +1,332 @@
+#!/usr/bin/env python3
+#
+# Copyright 2013-2022 The Mumble Developers. All rights reserved.
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file at the root of the
+# Mumble source tree or at <https://www.mumble.info/LICENSE>.
+#
+# About the tool
+# --------------
+# sign_macOS.py is a tool that takes a Mumble .DMG file and digitally signs its content
+# (executables, codecs, plugins, installers).
+#
+# The file(s) may already be signed. In this case, the tool will simply replace all the existing signatures.
+#
+# Using the tool
+# --------------
+# To sign Mumble-1.4.0.unsigned.dmg, one would do the following:
+#
+# $ ./sign_macOS.py -i Mumble-1.4.0.unsigned.dmg -o Mumble-1.4.0.dmg
+#
+# Configuration
+# -------------
+# The signing behavior of the tool can be configured via a configuration file.
+# By default the tool will read its configuration from $HOME/.sign_macOS.cfg.
+#
+# The configuration file uses JSON. For example, to sign using a particular
+# set of Developer ID certificates in your login keychain, you could use
+# something like this:
+#
+# --->8---
+# {
+# # The keychain needs the .keychain extension explicitly typed out.
+# "keychain": "login.keychain",
+# # Your Application certificate.
+# "developer-id-app": "Developer ID Application: John Appleseed",
+# # Your Installer certificate.
+# "developer-id-installer": "Developer ID Installer: John Appleseed"
+# }
+# --->8---
+
+import argparse
+import io
+import json
+import os
+import pathlib
+import plistlib
+import shutil
+import string
+import subprocess
+import sys
+import tempfile
+
+# Specify a set of codesign requirements for Mumble binaries.
+#
+# We require an Apple CA and a Developer ID leaf certificate (the one we're signing with).
+# The 'designated' line is specifically tuned to work on older versions
+# of macOS that aren't Developer ID-aware without breakage.
+#
+# We also explicitly require all shared libraries to be codesigned by Apple.
+# We can reasonably do that because Mumble is statically built on macOS.
+requirements = '''designated => anchor apple generic and identifier "${identifier}" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "${subject_OU}")
+library => anchor apple'''
+
+class Signer:
+ def __init__(self, cfgFile):
+ with open(cfgFile) as file:
+ content = file.read()
+ self.cfg = json.loads(content)
+
+ @staticmethod
+ def cmd(args, cwd = None):
+ '''
+ Executes the requested program and throws an exception if the program returns a non-zero return code.
+ '''
+ ret = subprocess.Popen(args, cwd = cwd).wait()
+ if ret != 0:
+ raise Exception('command "%s" exited with exit status %i' % (args[0], ret))
+
+ def certificateSubjectOU(self):
+ '''
+ Extracts the subject OU from the Application DeveloperID certificate that is specified in the script's configuration.
+ '''
+ findCert = subprocess.Popen(('/usr/bin/security', 'find-certificate',
+ '-c', self.cfg['developer-id-app'],
+ '-p', self.cfg['keychain']),
+ stdout = subprocess.PIPE, text = True)
+ pem, _ = findCert.communicate()
+
+ openssl = subprocess.Popen(('/usr/bin/openssl', 'x509', '-subject', '-noout'),
+ stdout = subprocess.PIPE, stdin = subprocess.PIPE, text = True)
+ subject, _ = openssl.communicate(pem)
+
+ start = subject.split('/OU=', 1)[1]
+ end = start.index('/')
+
+ return start[:end]
+
+ @staticmethod
+ def lookupFileIdentifier(file):
+ '''
+ Looks up a bundle identifier suitable for use when signing the app bundle or binary.
+ '''
+ try:
+ d = plistlib.load(os.path.join(file, 'Contents', 'Info.plist'))
+ return d['CFBundleIdentifier']
+ except:
+ return os.path.basename(file)
+
+ def codesign(self, files, entitlements = None):
+ '''
+ Calls the codesign executable on the signable object(s) in the specified directory.
+ '''
+ OU = self.certificateSubjectOU()
+
+ if hasattr(files, 'isalpha'):
+ files = (files,)
+
+ for file in files:
+ identifier = self.lookupFileIdentifier(file)
+ reqs = string.Template(requirements).substitute({
+ 'identifier': identifier,
+ 'subject_OU': OU,
+ })
+
+ print("Identifier:", identifier)
+ print("Subject_OU:", OU)
+ print("Reqs:", reqs)
+
+ args = ['-vvvv', '--force',
+ '-r=' + reqs,
+ '--identifier', identifier,
+ '--keychain', self.cfg['keychain'],
+ '--sign', self.cfg['developer-id-app']]
+
+ if entitlements:
+ args += ['--options', 'runtime',
+ '--entitlements', entitlements]
+
+ self.cmd(['codesign'] + args + [file])
+
+ return 0
+
+ def prodsign(self, inFile, outFile):
+ '''
+ Calls the productsign executable to sign the specified product, outputting it signed.
+ '''
+ self.cmd(['productsign',
+ '--keychain', self.cfg['keychain'],
+ '--sign', self.cfg['developer-id-installer'],
+ inFile, outFile])
+
+ @staticmethod
+ def volNameForMountedDMG(mountPoint):
+ diskutil = subprocess.Popen(['diskutil', 'info', '-plist', mountPoint], stdout = subprocess.PIPE)
+ plist, _ = diskutil.communicate()
+ fileLikePlist = io.BytesIO(plist)
+ diskInfo = plistlib.load(fileLikePlist)
+ return diskInfo['VolumeName']
+
+ @staticmethod
+ def extractDMG(file, workDir):
+ '''
+ Extracts the specified DMG into the "content" subdirectory of workDir.
+ It returns the volume name of the extracted DMG.
+ '''
+ mountDir = os.path.join(workDir, 'mount')
+ os.mkdir(mountDir)
+ Signer.cmd(['hdiutil', 'mount', '-readonly', '-mountpoint', mountDir, file])
+ volName = Signer.volNameForMountedDMG(mountDir)
+ contentDir = os.path.join(workDir, 'content')
+ os.mkdir(contentDir)
+ for file in os.listdir(mountDir):
+ if file == '.Trashes':
+ continue
+
+ src = os.path.join(mountDir, file)
+ dst = os.path.join(contentDir, file)
+
+ if os.path.islink(src):
+ target = os.readlink(src)
+ os.symlink(target, dst)
+ elif os.path.isdir(src):
+ shutil.copytree(src, dst, True)
+ else:
+ shutil.copy(src, dst)
+
+ Signer.cmd(['umount', mountDir])
+
+ return volName
+
+ @staticmethod
+ def extractPKG(file, workDir):
+ '''
+ Expands the specified PKG into workDir.
+ '''
+ Signer.cmd(['pkgutil', '--expand-full', file, workDir])
+
+ def signApp(self, workDir, entitlements = None):
+ '''
+ Signs the app bundle in the "content" subdirectory of workDir.
+ '''
+ app = os.path.join(workDir, 'content', 'Mumble.app')
+
+ binaries = []
+ binariesDir = os.path.join(app, 'Contents', 'MacOS')
+ for binary in os.listdir(binariesDir):
+ binaries.append(os.path.join(binariesDir, binary))
+
+ codecs = []
+ codecsDir = os.path.join(app, 'Contents', 'Codecs')
+ for codec in os.listdir(codecsDir):
+ codecs.append(os.path.join(codecsDir, codec))
+
+ plugins = []
+ pluginsDir = os.path.join(app, 'Contents', 'Plugins')
+ for plugin in os.listdir(pluginsDir):
+ plugins.append(os.path.join(pluginsDir, plugin))
+
+ self.codesign(binaries)
+ self.codesign(codecs)
+ self.codesign(plugins)
+
+ overlayInst = os.path.join(app, 'Contents', 'Resources', 'MumbleOverlay.pkg')
+ os.rename(overlayInst, overlayInst + '.intermediate')
+ self.prodsign(overlayInst + '.intermediate', overlayInst)
+ os.remove(overlayInst + '.intermediate')
+
+ self.codesign(app, entitlements)
+
+ def signOverlay(self, workDir):
+ '''
+ Extracts "MumbleOverlay.pkg" (which is extracted from the app bundle),
+ signs the relevant binaries and then repacks it.
+ '''
+ app = os.path.join(workDir, 'content', 'Mumble.app')
+
+ overlayPKG = os.path.join(app, 'Contents', 'Resources', 'MumbleOverlay.pkg')
+ overlayDir = os.path.join(workDir, 'MumbleOverlay')
+
+ self.extractPKG(overlayPKG, overlayDir)
+
+ binaries = []
+ binariesDir = os.path.join(overlayDir, 'net.sourceforge.mumble.OverlayScriptingAddition.pkg',
+ 'Payload', 'MumbleOverlay.osax',
+ 'Contents', 'MacOS')
+ for binary in os.listdir(binariesDir):
+ binaries.append(os.path.join(binariesDir, binary))
+
+ self.codesign(binaries)
+
+ self.makePKG(overlayDir, overlayPKG)
+
+ @staticmethod
+ def makeDMG(workDir, volName, outFile):
+ '''
+ Makes a new DMG for the Mumble app that resides in the workDir's content subdirectory.
+ '''
+ Signer.cmd(['hdiutil', 'create', '-srcfolder', os.path.join(workDir, 'content'),
+ '-format', 'UDBZ', '-size', '1g', '-volname', volName, outFile])
+
+ @staticmethod
+ def makePKG(workDir, file):
+ '''
+ Flattens workDir's content into a PKG file.
+ '''
+ Signer.cmd(['pkgutil', '--flatten-full', workDir, file])
+
+def main():
+ p = argparse.ArgumentParser(usage='sign_macOS.py --input=<in.dmg> --output=<out.dmg> [--keep-tree]')
+ p.add_argument('-c', '--config', help = 'Configuration file', default = os.path.join(pathlib.Path.home(), '.sign_macOS.cfg'))
+ p.add_argument('-e', '--entitlements', help = 'Entitlements file')
+ p.add_argument('-i', '--input', help = 'Input file')
+ p.add_argument('-o', '--output', help = 'Output file')
+ p.add_argument('-kt', '--keep-tree', action = 'store_true', dest = 'keepTree',
+ help = 'Keep the working tree after signing')
+ args = p.parse_args()
+
+ if not args.input:
+ print('No input specified')
+ sys.exit(1)
+
+ if not args.output:
+ print('No output specified')
+ sys.exit(1)
+
+ signer = Signer(args.config)
+
+ inFile = os.path.abspath(args.input)
+ outFile = os.path.abspath(args.output)
+ workDir = tempfile.mkdtemp()
+
+ print('Input: ' + inFile)
+ print('Output: ' + outFile)
+ print('Working dir: ' + workDir + '\n')
+
+ if inFile.lower().endswith('.dmg'):
+ volName = signer.extractDMG(inFile, workDir)
+
+ signer.signOverlay(workDir)
+ signer.signApp(workDir, args.entitlements)
+
+ signer.makeDMG(workDir, volName, outFile)
+
+ signer.codesign(outFile)
+ elif inFile.lower().endswith('.pkg'):
+ signer.extractPKG(inFile, workDir)
+
+ files = []
+ for file in os.listdir(workDir):
+ files.append(os.path.join(workDir, file))
+
+ signer.codesign(files)
+
+ signer.makePKG(workDir, outFile)
+
+ os.rename(outFile, outFile + '.intermediate')
+ signer.prodsign(outFile + '.intermediate', outFile)
+ os.remove(outFile + '.intermediate')
+ else:
+ shutil.copy(inFile, outFile)
+ signer.codesign(outFile)
+
+ print('')
+
+ if not args.keepTree:
+ shutil.rmtree(workDir, ignore_errors = True)
+ print('Working tree removed\n')
+
+ print('Signed file available at ' + args.output)
+
+if __name__ == '__main__':
+ main()