1
1
#!/usr/bin/env python
2
2
"""Functions that interface with rcodesign"""
3
3
import asyncio
4
+ from collections import namedtuple
4
5
import logging
6
+ import os
5
7
import 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
6
13
from signingscript .exceptions import SigningScriptError
7
14
8
15
log = logging .getLogger (__name__ )
@@ -41,13 +48,14 @@ async def _execute_command(command):
41
48
stderr = (await proc .stderr .readline ()).decode ("utf-8" ).rstrip ()
42
49
if stderr :
43
50
# Unfortunately a lot of outputs from rcodesign come out to stderr
44
- log .warn (stderr )
51
+ log .warning (stderr )
45
52
output_lines .append (stderr )
46
53
47
54
exitcode = await proc .wait ()
48
55
log .info ("exitcode {}" .format (exitcode ))
49
56
return exitcode , output_lines
50
57
58
+
51
59
def find_submission_id (logs ):
52
60
"""Given notarization logs, find and return the submission id
53
61
Args:
@@ -128,6 +136,7 @@ async def rcodesign_check_result(logs):
128
136
raise RCodesignError ("Notarization failed!" )
129
137
return
130
138
139
+
131
140
async def rcodesign_staple (path ):
132
141
"""Staples a given app
133
142
Args:
@@ -146,3 +155,129 @@ async def rcodesign_staple(path):
146
155
if exitcode > 0 :
147
156
raise RCodesignError (f"Error stapling notarization. Exit code { exitcode } " )
148
157
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