Skip to content

Commit b4a1d1e

Browse files
Support Linux with dossier_codesigning_reader.py (#2539)
On Linux there is no keychain or the `security` command. To codesign on Linux you’ll have to use something like https://gregoryszorc.com/docs/apple-codesign/0.17.0/apple_codesign_rcodesign.html, with a wrapper that translates `codesign` arguments to `rcodesign` arguments. Using that method requires using `.cer` files instead of identity strings (which are part of the subject of a certificate). The change made here assumes you are using that method, which has worked for us in our internal project. --------- Signed-off-by: Brentley Jones <[email protected]>
1 parent ccddf99 commit b4a1d1e

File tree

2 files changed

+159
-42
lines changed

2 files changed

+159
-42
lines changed

tools/dossier_codesigningtool/dossier_codesigning_reader.py

Lines changed: 150 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import tempfile
7373
import traceback
7474

75+
_MACOS = sys.platform == "darwin"
7576

7677
# LINT.IfChange
7778
_DEFAULT_TIMEOUT = 900
@@ -287,6 +288,12 @@ def generate_arg_parser():
287288
If specified, during signing, only search for the signing identity in the
288289
keychain 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

296303
def _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

334391
def _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

422487
def _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

547637
def _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

Comments
 (0)