Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
Merge 8b4c26a into 654ff45
Browse files Browse the repository at this point in the history
  • Loading branch information
JohanLorenzo committed Nov 23, 2018
2 parents 654ff45 + 8b4c26a commit c6a8d8c
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 64 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [0.9.0] - 2018-11-23

### Changed
* Digest algorithm is not checked by jarsigner anymore. Instead, pushapkscript parses `META-INF/MANIFEST.MF`. This allows several digests to be used. Otherwise jarsigner inconsistently reports one of the digests.


## [0.8.0] - 2018-06-22

Expand All @@ -14,12 +19,14 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Changed
* `google_play_track` in task payload can now be a random string. Value is enforced by mozapkpublisher.


## [0.7.0] - 2018-04-27

### Added
* Support for Firefox Focus
* Support for Google Play's new internal track.


## [0.6.0] - 2018-04-20

### Removed
Expand Down
31 changes: 0 additions & 31 deletions pushapkscript/jarsigner.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import logging
import re
import subprocess

from pushapkscript.exceptions import SignatureError
from pushapkscript.task import extract_android_product_from_scopes

log = logging.getLogger(__name__)

DIGEST_ALGORITHM_REGEX = re.compile(r'\s*Digest algorithm: (\S+)$', re.MULTILINE)

_DIGEST_ALGORITHM_PER_ANDROID_PRODUCT = {
'aurora': 'SHA1',
'beta': 'SHA1',
'release': 'SHA1',
'dep': 'SHA1',

'focus': 'SHA-256',
}


def verify(context, apk_path):
binary_path, keystore_path, certificate_aliases, android_product = _pluck_configuration(context)
Expand All @@ -35,7 +23,6 @@ def verify(context, apk_path):
_check_certificate_via_return_code(
completed_process.returncode, command_output, binary_path, apk_path, certificate_alias, keystore_path
)
_check_digest_algorithm(command_output, apk_path, android_product)


def _check_certificate_via_return_code(return_code, command_output, binary_path, apk_path, certificate_alias, keystore_path):
Expand All @@ -50,24 +37,6 @@ def _check_certificate_via_return_code(return_code, command_output, binary_path,
log.info('The signature of "{}" comes from the correct alias "{}"'.format(apk_path, certificate_alias))


def _check_digest_algorithm(command_output, apk_path, android_product):
# This prevents https://bugzilla.mozilla.org/show_bug.cgi?id=1332916
match_result = DIGEST_ALGORITHM_REGEX.search(command_output)
if match_result is None:
log.critical(command_output)
raise SignatureError('Could not find what digest algorithm was used to sign this APK')

digest_algorithm = match_result.group(1)
expected_digest_algorithm = _DIGEST_ALGORITHM_PER_ANDROID_PRODUCT[android_product]
if digest_algorithm != expected_digest_algorithm:
log.critical(command_output)
raise SignatureError(
'Wrong digest algorithm: "{}" digest is expected, but "{}" was found'.format(expected_digest_algorithm, digest_algorithm)
)

log.info('The signature of "{}" contains the correct digest algorithm'.format(apk_path))


def _pluck_configuration(context):
keystore_path = context.config['jarsigner_key_store']
# Uses jarsigner in PATH if config doesn't provide it
Expand Down
72 changes: 72 additions & 0 deletions pushapkscript/manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import io
import logging
import re

from zipfile import ZipFile

from pushapkscript.exceptions import SignatureError
from pushapkscript.task import extract_android_product_from_scopes


log = logging.getLogger(__name__)

_NAME_MARKER = 'Name: '
_DIGEST_MARKER_PATTERN = re.compile(r'\S+-Digest: ')
_DIGEST_ALGORITHM_PER_ANDROID_PRODUCT = {
'aurora': 'SHA1',
'beta': 'SHA1',
'release': 'SHA1',
'dep': 'SHA1',

'focus': 'SHA-256',
}


def verify(context, apk_path):
# This prevents https://bugzilla.mozilla.org/show_bug.cgi?id=1332916
android_product = extract_android_product_from_scopes(context)
expected_digest_algorithm = _DIGEST_ALGORITHM_PER_ANDROID_PRODUCT[android_product]
if not _does_apk_have_expected_digest(apk_path, expected_digest_algorithm):
raise SignatureError(
'Wrong digest algorithm: "{}" digest is expected, but it was not found'.format(expected_digest_algorithm)
)

log.info('The signature of "{}" contains the correct digest algorithm ({})'.format(
apk_path, expected_digest_algorithm
))


def _does_apk_have_expected_digest(apk_path, expected_digest):
with ZipFile(apk_path) as zip:
with zip.open('META-INF/MANIFEST.MF') as f:
# readline doesn't return a py3 str
manifest_lines = [line for line in io.TextIOWrapper(f, 'utf-8')]

manifest = _parse_manifest_lines(manifest_lines)
return _is_digest_present(expected_digest, manifest)


def _parse_manifest_lines(manifest_lines):
manifest = {}
current_file = None
for line in manifest_lines:
line = line.rstrip('\n') # remove trailing EOL
# XXX Headers at the top aren't parsed
if line.startswith(_NAME_MARKER):
current_file = line[len(_NAME_MARKER):]
elif current_file:
if line.startswith(' '):
current_file = current_file + line.lstrip()
elif _DIGEST_MARKER_PATTERN.match(line):
digest, hash = line.split(': ')
manifest.setdefault(current_file, {})[digest] = hash

return manifest


def _is_digest_present(expected_digest, parsed_manifest):
if not parsed_manifest:
return False

expected_digest = '{}-Digest'.format(expected_digest)
return all(expected_digest in entry for entry in parsed_manifest.values())
6 changes: 4 additions & 2 deletions pushapkscript/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from scriptworker import client, artifacts

from pushapkscript import googleplay, jarsigner, task
from pushapkscript import googleplay, jarsigner, task, manifest


log = logging.getLogger(__name__)
Expand All @@ -27,7 +27,9 @@ async def async_main(context):
]

log.info('Verifying APKs\' signatures...')
[jarsigner.verify(context, apk_path) for apk_path in all_apks_paths]
for apk_path in all_apks_paths:
jarsigner.verify(context, apk_path)
manifest.verify(context, apk_path)

if task.extract_android_product_from_scopes(context) == 'focus':
log.warn('Focus does not upload strings automatically. Skipping Google Play strings search.')
Expand Down
29 changes: 0 additions & 29 deletions pushapkscript/test/test_jarsigner.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,35 +70,6 @@ def test_raises_error_when_return_code_is_not_0(self):
with self.assertRaises(SignatureError):
jarsigner.verify(self.context, '/path/to/apk')

def test_raises_error_when_digest_is_not_sha1_for_fennec(self):
with patch('subprocess.run') as run:
run.return_value = MagicMock()
run.return_value.returncode = 0
run.return_value.stdout = 'Digest algorithm: SHA-256'

with self.assertRaises(SignatureError):
jarsigner.verify(self.context, '/path/to/apk')

def test_expects_sha256_for_focus_or_klar(self):
self.context.task['scopes'] = ['project:mobile:focus:releng:product:focus']
self.context.config['taskcluster_scope_prefix'] = 'project:mobile:focus:releng:product:'
self.context.config['jarsigner_certificate_aliases']['focus'] = 'focus'
with patch('subprocess.run') as run:
run.return_value = MagicMock()
run.return_value.returncode = 0
run.return_value.stdout = 'Digest algorithm: SHA-256'

jarsigner.verify(self.context, '/path/to/apk')

def test_raises_error_when_no_digest_algo_is_returned_by_jarsigner(self):
with patch('subprocess.run') as run:
run.return_value = MagicMock()
run.return_value.returncode = 0
run.return_value.stdout = 'Some random output'

with self.assertRaises(SignatureError):
jarsigner.verify(self.context, '/path/to/apk')

def test_pluck_configuration_sets_every_argument(self):
self.assertEqual(
jarsigner._pluck_configuration(self.context),
Expand Down
Loading

0 comments on commit c6a8d8c

Please sign in to comment.