7272import tempfile
7373import traceback
7474
75+ _MACOS = sys .platform == "darwin"
7576
7677# LINT.IfChange
7778_DEFAULT_TIMEOUT = 900
@@ -287,6 +288,12 @@ def generate_arg_parser():
287288If specified, during signing, only search for the signing identity in the
288289keychain file specified. This is equivalent to the --keychain argument on
289290/usr/bin/codesign itself.
291+ ''' )
292+ sign_parser .add_argument (
293+ '--certificates' ,
294+ help = '''
295+ If specified, during signing, only search for the signing identity in `.cer`
296+ files in the directory specified.
290297''' )
291298 sign_parser .set_defaults (func = _sign_bundle )
292299
@@ -295,13 +302,36 @@ def generate_arg_parser():
295302
296303def _parse_provisioning_profile (provisioning_profile_path ):
297304 """Reads and parses a provisioning profile."""
298- plist_xml = subprocess .check_output ([
299- 'security' ,
300- 'cms' ,
301- '-D' ,
302- '-i' ,
303- provisioning_profile_path ,
304- ])
305+ if _MACOS :
306+ plist_xml = subprocess .check_output ([
307+ 'security' ,
308+ 'cms' ,
309+ '-D' ,
310+ '-i' ,
311+ provisioning_profile_path ,
312+ ])
313+ else :
314+ # We call it this way to silence the "Verification successful" message for
315+ # the non-error case
316+ try :
317+ plist_xml = subprocess .run (
318+ [
319+ 'openssl' ,
320+ 'smime' ,
321+ '-inform' ,
322+ 'der' ,
323+ '-verify' ,
324+ '-noverify' ,
325+ '-in' ,
326+ provisioning_profile_path ,
327+ ],
328+ check = True ,
329+ stderr = subprocess .PIPE ,
330+ stdout = subprocess .PIPE ,
331+ ).stdout
332+ except subprocess .CalledProcessError as e :
333+ print (e .stderr , file = sys .stderr )
334+ raise e
305335 return plistlib .loads (plist_xml )
306336
307337
@@ -310,39 +340,71 @@ def _generate_sha1(data):
310340 return hashlib .sha1 (data ).hexdigest ().upper ()
311341
312342
313- def _find_codesign_identities (signing_keychain = None ):
343+ def extract_identity_hash (cer_path ):
344+ try :
345+ der_cert_cmd = ['openssl' , 'x509' , '-in' , cer_path , '-outform' , 'DER' ]
346+ der_cert = subprocess .check_output (der_cert_cmd , stderr = subprocess .STDOUT )
347+
348+ sha1_hash = _generate_sha1 (der_cert )
349+
350+ subject_cmd = ['openssl' , 'x509' , '-in' , cer_path , '-noout' , '-subject' ]
351+ subject_output = subprocess .check_output (subject_cmd , stderr = subprocess .STDOUT ).decode ().strip ()
352+
353+ return sha1_hash , subject_output
354+ except subprocess .CalledProcessError as e :
355+ raise OSError (f'Failed to extract certificate from { cer_path } : { e .output .decode ()} ' )
356+
357+
358+ def _find_codesign_identities (signing_keychain = None , certificates_directory_path = None ):
314359 """Finds the code signing identities in a specified keychain."""
315360 ids = {}
316- execute_command = [
317- 'security' ,
318- 'find-identity' ,
319- '-v' ,
320- '-p' ,
321- 'codesigning' ,
322- ]
323- if signing_keychain :
324- execute_command .extend ([signing_keychain ])
325- output = _execute_and_filter_output (execute_command )
326- output = output .strip ()
327- for line in output .splitlines ():
328- m = _SECURITY_FIND_IDENTITY_OUTPUT_REGEX .search (line )
329- if m :
330- ids [m .groupdict ()['hash' ]] = line
361+ if _MACOS :
362+ execute_command = [
363+ 'security' ,
364+ 'find-identity' ,
365+ '-v' ,
366+ '-p' ,
367+ 'codesigning' ,
368+ ]
369+ if signing_keychain :
370+ execute_command .extend ([signing_keychain ])
371+ output = _execute_and_filter_output (execute_command )
372+ output = output .strip ()
373+ for line in output .splitlines ():
374+ m = _SECURITY_FIND_IDENTITY_OUTPUT_REGEX .search (line )
375+ if m :
376+ ids [m .groupdict ()['hash' ]] = (line , None )
377+ else :
378+ if not certificates_directory_path :
379+ raise OSError ('--certificates is required when finding identities on non-macOS platforms.' )
380+
381+ cer_paths = glob .glob (os .path .join (certificates_directory_path , "*.cer" ))
382+ if not cer_paths :
383+ raise OSError (f'No .cer files found in { certificates_directory_path } .' )
384+
385+ for cer_path in cer_paths :
386+ hash , subject = extract_identity_hash (cer_path )
387+ ids [hash ] = (subject , cer_path )
331388 return ids
332389
333390
334391def _resolve_codesign_identity (
335- provisioning_profile_path , codesign_identity , signing_keychain
336- ):
392+ provisioning_profile_path ,
393+ codesign_identity ,
394+ signing_keychain ,
395+ certificates_directory_path ):
337396 """Finds the best identity on the system given a profile and identity."""
338397 mpf = _parse_provisioning_profile (provisioning_profile_path )
339- ids_codesign = _find_codesign_identities (signing_keychain )
398+ ids_codesign = _find_codesign_identities (signing_keychain , certificates_directory_path )
340399 best_codesign_identity = codesign_identity
341400 for id_mpf in _get_identities_from_provisioning_profile (mpf ):
342401 if id_mpf in ids_codesign .keys ():
343- best_codesign_identity = id_mpf
402+ subject , cer_path = ids_codesign [id_mpf ]
403+ # If we have a certificate path, use that instead of the identity hash
404+ # On Linux the `codesign` implementation will use the certifcate directly
405+ best_codesign_identity = cer_path if cer_path else id_mpf
344406 # Return early if the specified codesign_identity matches a valid entity.
345- if codesign_identity in ids_codesign [ id_mpf ] :
407+ if not codesign_identity or codesign_identity in subject :
346408 break
347409 return best_codesign_identity
348410
@@ -409,9 +471,12 @@ def _invoke_codesign(codesign_path, identity, entitlements_path,
409471 ])
410472 cmd .append (full_path_to_sign )
411473
412- # Just like Xcode, ensure CODESIGN_ALLOCATE is set to point to the correct
413- # version.
414- custom_env = {'CODESIGN_ALLOCATE' : _find_codesign_allocate ()}
474+ if _MACOS :
475+ # Just like Xcode, ensure CODESIGN_ALLOCATE is set to point to the correct
476+ # version.
477+ custom_env = {'CODESIGN_ALLOCATE' : _find_codesign_allocate ()}
478+ else :
479+ custom_env = None
415480 _execute_and_filter_output (
416481 cmd ,
417482 filtering = _filter_codesign_tool_output ,
@@ -420,11 +485,15 @@ def _invoke_codesign(codesign_path, identity, entitlements_path,
420485
421486
422487def _fetch_preferred_signing_identity (
423- manifest , provisioning_profile_file_path , signing_keychain
424- ):
488+ certificates_directory_path ,
489+ manifest ,
490+ provisioning_profile_file_path ,
491+ signing_keychain ):
425492 """Returns the preferred signing identity.
426493
427494 Args:
495+ certificates_directory_path: If not None, this will only search for the
496+ signing identity in `.cer` files in the directory specified.
428497 manifest: The contents of the manifest in this dossier.
429498 provisioning_profile_file_path: Directory of the provisioning profile to be
430499 used for signing.
@@ -446,7 +515,10 @@ def _fetch_preferred_signing_identity(
446515 # signing, which doesn't need to be validated against a provisioning profile.
447516 if codesign_identity != '-' and provisioning_profile_file_path :
448517 codesign_identity = _resolve_codesign_identity (
449- provisioning_profile_file_path , codesign_identity , signing_keychain
518+ provisioning_profile_file_path ,
519+ codesign_identity ,
520+ signing_keychain ,
521+ certificates_directory_path ,
450522 )
451523 return codesign_identity
452524
@@ -499,8 +571,14 @@ def _extract_archive(*, app_bundle_subdir, working_dir, unsigned_archive_path):
499571 Raises:
500572 OSError: when app bundle is not found in extracted archive.
501573 """
502- subprocess .check_call (
503- ['ditto' , '-x' , '-k' , unsigned_archive_path , working_dir ])
574+ if sys .platform == "darwin" :
575+ subprocess .check_call (
576+ ['ditto' , '-x' , '-k' , unsigned_archive_path , working_dir ],
577+ )
578+ else :
579+ subprocess .check_call (
580+ ['unzip' , '-q' '-X' , unsigned_archive_path , '-d' , working_dir ],
581+ )
504582
505583 extracted_bundles = glob .glob (
506584 os .path .join (working_dir , app_bundle_subdir , 'Payload' , '*.app' ))
@@ -540,8 +618,20 @@ def _package_ipa(*, app_bundle_subdir, working_dir, output_ipa):
540618 if entry .name not in IPA_ALLOWED_SUBDIRS :
541619 raise OSError (f'Disallowed IPA base directory detected: { entry .path } ' )
542620
543- subprocess .check_call (
544- ['ditto' , '-c' , '-k' , '--norsrc' , '--noextattr' , bundle_path , output_ipa ])
621+ if _MACOS :
622+ subprocess .check_call ([
623+ 'ditto' ,
624+ '-c' ,
625+ '-k' ,
626+ '--norsrc' ,
627+ '--noextattr' ,
628+ bundle_path ,
629+ output_ipa ,
630+ ])
631+ else :
632+ if os .path .exists (output_ipa ):
633+ os .remove (output_ipa )
634+ subprocess .check_call (['zip' , '-X' , '-r' , output_ipa , '.' ], cwd = bundle_path )
545635
546636
547637def _sign_bundle_with_manifest (
@@ -551,6 +641,7 @@ def _sign_bundle_with_manifest(
551641 codesign_path ,
552642 allowed_entitlements ,
553643 signing_keychain ,
644+ certificates_directory_path ,
554645 override_codesign_identity = None ,
555646 executor = concurrent .futures .ThreadPoolExecutor ()):
556647 """Signs a bundle with a dossier.
@@ -570,6 +661,8 @@ def _sign_bundle_with_manifest(
570661 signing_keychain: If not None, this will first search for the signing
571662 identity in the keychain file specified, forwarding the path directly to
572663 the /usr/bin/codesign invocation via its --keychain argument.
664+ certificates_directory_path: If not None, this will only search for the
665+ signing identity in `.cer` files in the directory specified.
573666 override_codesign_identity: If set, this will override the identity
574667 specified in the manifest. This is primarily useful when signing an
575668 embedded bundle, as all bundles must use the same codesigning identity,
@@ -588,7 +681,10 @@ def _sign_bundle_with_manifest(
588681 provisioning_profile_filename )
589682 if not codesign_identity :
590683 codesign_identity = _fetch_preferred_signing_identity (
591- manifest , provisioning_profile_file_path , signing_keychain
684+ certificates_directory_path ,
685+ manifest ,
686+ provisioning_profile_file_path ,
687+ signing_keychain ,
592688 )
593689 if not codesign_identity :
594690 raise SystemExit (
@@ -618,7 +714,7 @@ def _sign_bundle_with_manifest(
618714 # submit each embedded manifest to sign concurrently
619715 codesign_futures = _sign_embedded_bundles_with_manifest (
620716 manifest , root_bundle_path , dossier_directory_path , codesign_path ,
621- allowed_entitlements , signing_keychain , codesign_identity , executor )
717+ allowed_entitlements , signing_keychain , certificates_directory_path , codesign_identity , executor )
622718 _wait_signing_futures (codesign_futures )
623719
624720 if provisioning_profile_file_path :
@@ -686,6 +782,7 @@ def _sign_embedded_bundles_with_manifest(
686782 codesign_path ,
687783 allowed_entitlements ,
688784 signing_keychain ,
785+ certificates_directory_path ,
689786 codesign_identity ,
690787 executor ):
691788 """Signs embedded bundles/dylibs/frameworks concurrently and returns futures.
@@ -702,6 +799,8 @@ def _sign_embedded_bundles_with_manifest(
702799 signing_keychain: If not None, this will only search for the signing
703800 identity in the keychain file specified, forwarding the path directly to
704801 the /usr/bin/codesign invocation via its --keychain argument.
802+ certificates_directory_path: If not None, this will only search for the
803+ signing identity in `.cer` files in the directory specified.
705804 codesign_identity: The codesign identity to use for codesigning.
706805 executor: Asynchronous jobs Executor from concurrent.futures.
707806
@@ -727,6 +826,7 @@ def _sign_embedded_bundles_with_manifest(
727826 embedded_bundle_path , embedded_manifest ,
728827 dossier_directory_path , codesign_path ,
729828 allowed_entitlements , signing_keychain ,
829+ certificates_directory_path ,
730830 codesign_identity , executor ))
731831
732832 if os .path .exists (framework_dir ):
@@ -871,6 +971,7 @@ def _sign_archived_bundle(
871971 dossier_directory_path ,
872972 output_ipa ,
873973 signing_keychain ,
974+ certificates_directory_path ,
874975 working_dir ,
875976 unsigned_archive_path ):
876977 """Signs the bundle and packages it as an IPA to output_artifact.
@@ -887,6 +988,8 @@ def _sign_archived_bundle(
887988 signing_keychain: If not None, this will only search for the signing
888989 identity in the keychain file specified, forwarding the path directly to
889990 the /usr/bin/codesign invocation via its --keychain argument.
991+ certificates_directory_path: If not None, this will only search for the
992+ signing identity in `.cer` files in the directory specified.
890993 working_dir: String, the path to unzip the archive file into.
891994 unsigned_archive_path: String, the full path to a unsigned archive.
892995 """
@@ -898,7 +1001,8 @@ def _sign_archived_bundle(
8981001 manifest = read_manifest_from_dossier (dossier_directory_path )
8991002 _sign_bundle_with_manifest (extracted_bundle , manifest ,
9001003 dossier_directory_path , codesign_path ,
901- allowed_entitlements , signing_keychain )
1004+ allowed_entitlements , signing_keychain ,
1005+ certificates_directory_path )
9021006 _package_ipa (
9031007 app_bundle_subdir = app_bundle_subdir ,
9041008 working_dir = working_dir ,
@@ -926,6 +1030,7 @@ def _sign_bundle(parsed_args):
9261030 codesign_path = parsed_args .codesign
9271031 allowed_entitlements = parsed_args .allow_entitlement
9281032 signing_keychain = parsed_args .keychain
1033+ certificates_directory_path = parsed_args .certificates
9291034 output_artifact = parsed_args .output_artifact
9301035
9311036 if not os .path .exists (input_fullpath ):
@@ -957,6 +1062,7 @@ def _sign_bundle(parsed_args):
9571062 _sign_archived_bundle (
9581063 allowed_entitlements = allowed_entitlements ,
9591064 app_bundle_subdir = 'bundle' ,
1065+ certificates_directory_path = certificates_directory_path ,
9601066 codesign_path = codesign_path ,
9611067 dossier_directory_path = dossier_directory_path ,
9621068 output_ipa = output_artifact ,
@@ -978,7 +1084,8 @@ def _sign_bundle(parsed_args):
9781084 manifest = read_manifest_from_dossier (dossier_directory .path )
9791085 _sign_bundle_with_manifest (input_fullpath , manifest ,
9801086 dossier_directory .path , codesign_path ,
981- allowed_entitlements , signing_keychain )
1087+ allowed_entitlements , signing_keychain ,
1088+ certificates_directory_path )
9821089 elif input_path_suffix == '.ipa' :
9831090 _check_common_archived_bundle_args (
9841091 output_artifact = output_artifact ,
@@ -989,6 +1096,7 @@ def _sign_bundle(parsed_args):
9891096 _sign_archived_bundle (
9901097 allowed_entitlements = allowed_entitlements ,
9911098 app_bundle_subdir = '' ,
1099+ certificates_directory_path = certificates_directory_path ,
9921100 codesign_path = codesign_path ,
9931101 dossier_directory_path = dossier_directory .path ,
9941102 output_ipa = output_artifact ,
0 commit comments