11#!/usr/bin/env python
22"""Functions that interface with rcodesign"""
33import asyncio
4+ from collections import namedtuple
45import logging
6+ import os
57import re
8+ from glob import glob
9+ from shutil import copy2
10+
11+ from scriptworker_client .aio import download_file , raise_future_exceptions , retry_async
12+ from scriptworker_client .exceptions import DownloadError
613from signingscript .exceptions import SigningScriptError
714
815log = logging .getLogger (__name__ )
@@ -41,13 +48,14 @@ async def _execute_command(command):
4148 stderr = (await proc .stderr .readline ()).decode ("utf-8" ).rstrip ()
4249 if stderr :
4350 # Unfortunately a lot of outputs from rcodesign come out to stderr
44- log .warn (stderr )
51+ log .warning (stderr )
4552 output_lines .append (stderr )
4653
4754 exitcode = await proc .wait ()
4855 log .info ("exitcode {}" .format (exitcode ))
4956 return exitcode , output_lines
5057
58+
5159def find_submission_id (logs ):
5260 """Given notarization logs, find and return the submission id
5361 Args:
@@ -128,6 +136,7 @@ async def rcodesign_check_result(logs):
128136 raise RCodesignError ("Notarization failed!" )
129137 return
130138
139+
131140async def rcodesign_staple (path ):
132141 """Staples a given app
133142 Args:
@@ -146,3 +155,129 @@ async def rcodesign_staple(path):
146155 if exitcode > 0 :
147156 raise RCodesignError (f"Error stapling notarization. Exit code { exitcode } " )
148157 return
158+
159+
160+ def _create_empty_entitlements_file (dest ):
161+ contents = """<?xml version="1.0" encoding="UTF-8"?>
162+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
163+ <plist version="1.0">
164+ <dict>
165+ </dict>
166+ </plist>
167+ """ .lstrip ()
168+ with open (dest , "wt" ) as fd :
169+ fd .writelines (contents )
170+
171+
172+ async def _download_entitlements (hardened_sign_config , workdir ):
173+ """Download entitlements listed in the hardened signing config
174+ Args:
175+ hardened_sign_config (list): hardened signing configs
176+ workdir (str): current work directory where entitlements will be saved
177+
178+ Returns:
179+ Map of url -> local file location
180+ """
181+ empty_file = os .path .join (workdir , "0-empty.xml" )
182+ _create_empty_entitlements_file (empty_file )
183+ # rcodesign requires us to specify an "empty" entitlements file
184+ url_map = {None : empty_file }
185+
186+ # Unique urls to be downloaded
187+ urls_to_download = set ([i ["entitlements" ] for i in hardened_sign_config if "entitlements" in i ])
188+ # If nothing found, skip
189+ if not urls_to_download :
190+ log .warn ("No entitlements urls provided! Skipping download." )
191+ return url_map
192+
193+ futures = []
194+ for index , url in enumerate (urls_to_download , start = 1 ):
195+ # Prefix filename with an index in case filenames are the same
196+ filename = "{}-{}" .format (index , url .split ("/" )[- 1 ])
197+ dest = os .path .join (workdir , filename )
198+ url_map [url ] = dest
199+ log .info (f"Downloading resource: { filename } from { url } " )
200+ futures .append (
201+ asyncio .ensure_future (
202+ retry_async (
203+ download_file ,
204+ retry_exceptions = (DownloadError , TimeoutError ),
205+ args = (url , dest ),
206+ attempts = 5 ,
207+ )
208+ )
209+ )
210+ await raise_future_exceptions (futures )
211+ return url_map
212+
213+
214+ EntitlementEntry = namedtuple (
215+ "EntitlementEntry" ,
216+ ["file" , "entitlement" , "runtime" ],
217+ )
218+
219+ def _get_entitlements_args (hardened_sign_config , path , entitlements_map ):
220+ """Builds the list of entitlements based on files in path
221+
222+ Args:
223+ hardened_sign_config (list): hardened signing configuration
224+ path (str): path to app
225+ """
226+ entries = []
227+
228+ for config in hardened_sign_config :
229+ entitlement_path = entitlements_map .get (config .get ("entitlements" ))
230+ for path_glob in config ["globs" ]:
231+ separator = ""
232+ if not path_glob .startswith ("/" ):
233+ separator = "/"
234+ # Join incoming glob with root of app path
235+ full_path_glob = path + separator + path_glob
236+ for binary_path in glob (full_path_glob , recursive = True ):
237+ # Get relative path
238+ relative_path = os .path .relpath (binary_path , path )
239+ # Append "<binary path>:<entitlement>" to list of args
240+ entries .append (
241+ EntitlementEntry (
242+ file = relative_path ,
243+ entitlement = entitlement_path ,
244+ runtime = config .get ("runtime" ),
245+ )
246+ )
247+
248+ return entries
249+
250+
251+ async def rcodesign_sign (workdir , path , creds_path , creds_pass_path , hardened_sign_config = []):
252+ """Signs a given app
253+ Args:
254+ workdir (str): Path to work directory
255+ path (str): Path to be signed
256+ creds_path (str): Path to credentials file
257+ creds_pass_path (str): Path to credentials password file
258+ hardened_sign_config (list): Hardened signing configuration
259+
260+ Returns:
261+ (Tuple) exit code, log lines
262+ """
263+ # TODO: Validate and sanitize input
264+ command = [
265+ "rcodesign" ,
266+ "sign" ,
267+ "--code-signature-flags=runtime" ,
268+ f"--p12-file={ creds_path } " ,
269+ f"--p12-password-file={ creds_pass_path } " ,
270+ ]
271+
272+ entitlements_map = await _download_entitlements (hardened_sign_config , workdir )
273+ file_entitlements = _get_entitlements_args (hardened_sign_config , path , entitlements_map )
274+
275+ for entry in file_entitlements :
276+ if entry .runtime :
277+ flags_arg = f"--code-signature-flags=runtime:{ entry .file } "
278+ command .append (flags_arg )
279+ entitlement_arg = f"--entitlements-xml-path={ entry .file } :{ entry .entitlement } "
280+ command .append (entitlement_arg )
281+
282+ command .append (path )
283+ await _execute_command (command )
0 commit comments