fix(pushapk): Actually check apk signatures - #1514
Draft
hneiva wants to merge 5 commits into
Draft
Conversation
The non-Google branch of _get_channel_publish_config never copied `certificate_alias` into its result, while all three Google paths did, so a samsung publish config had no alias at all and jarsigner._pluck_configuration would raise KeyError on it. Nothing noticed, because its only caller is jarsigner.verify(), which never runs. The alias identifies the certificate the incoming artifact was signed with, which the upstream signing task decides, so it is a property of the artifact rather than of the destination store and belongs at the app level for every store alike. Huawei support, currently in flight, needs no further change.
_pluck_configuration indexed `publish_config["certificate_alias"]`, which raises KeyError when the key is absent, and passed the value through untouched when it was None. A None alias ends up in the jarsigner argv, where subprocess reports it as `TypeError: expected str, bytes or os.PathLike object, not NoneType`. Raise ConfigValidationError instead, naming the store and pointing at `skip_check_signature`, so a product configured without an alias says so rather than failing inside subprocess.
Both the deployed image and the test image installed `default-jre-headless`, which ships keytool but not jarsigner: jarsigner lives in the JDK. Verified against debian:bookworm, where default-jre-headless provides only /usr/bin/keytool while default-jdk-headless provides both. This did not matter while signature verification was dead code, but the script does shell out to jarsigner, and the README already says a JDK is required. Install default-jdk-headless in both images so the binary is actually present before verification is switched on.
…ified The fixture APKs date from 2017 and are signed with SHA1withDSA and a 1024-bit DSA key. Modern JDKs disable both, so jarsigner treats them as unsigned and `-verify -strict` exits 16. Their manifests also list entries that were stripped out of the archives, which `-strict` rejects on its own. No alias juggling can make them verify. Generate a throwaway RSA-2048 keypair per test instead and re-sign the fixtures with it under the alias the product config expects, after dropping the stale META-INF so jarsigner writes a fresh manifest. The generated configs now declare SHA-256 to match, which is what worker.yml uses in production anyway. test_main_with_samsung_store signed for "nightly" while the fenix release app it targets declares "fenix-production"; it now uses the right alias. Signing needs a real keytool and jarsigner, so the class is skipped when they are missing. Detection runs the binaries rather than looking them up, because macOS ships stubs on PATH that exit non-zero with "Unable to locate a Java Runtime".
`skip_check_signature` is a product-level option, but async_main read it from `publish_config`, the per-app/channel/store dict that get_publish_config() builds. That function has never emitted the key on any of its paths, so the lookup always missed and always fell back to the default. jarsigner.verify() and manifest.verify() were dead code for every product in every environment. The bug predates the initial monorepo import. The default was wrong too. `True` means skip, and the log line in the skipped branch claims the product is configured with the option, but that branch is what ran when the key was absent, which is every product in worker.yml. Reading the right dict alone changes nothing, since nothing sets the option, so both have to move together: read `product_config`, and default to verifying. mozillavpn dev/fake-prod is the one config that cannot verify, because there is no dep signing for it yet and init_worker.sh imports no certificate, so it opts out explicitly. No test covered either branch of the condition, which is why this survived. There is one now; it fails against the old code with `assert 0 == 2`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pushapkscript has never verified an APK or AAB signature, for any product, in any environment.
skip_check_signaturewas read from the wrong config dict, so thejarsignerandMANIFEST.MFchecks inscript.pywere dead code. This predates the initial monorepo import.Switching them on needed four more fixes, one of which would otherwise have broken production.
The bug
script.py:skip_check_signatureis declared at the product level of the config schema, but this reads it frompublish_config, the per-app/channel/store dict built byget_publish_config(). That function never emits the key, so the lookup always missed and always fell back to the default.Two bugs, not one:
Truemeans skip, and theelsebranch claims the product is configured with the option, but that branch is what ran when the key was absent, which is every product inworker.yml.Fixing only (1) changes nothing, since nothing sets the option. Both had to move together. No test covered either branch, which is why this survived.
What else was blocking it
jarsignerwas not installed anywhere. Both the deployed image and the test image installeddefault-jre-headless, which shipskeytoolbut notjarsigner; jarsigner is JDK only. Verified ondebian:bookworm, where the JRE package provides just/usr/bin/keytoolwhiledefault-jdk-headlessprovides both. Turning verification on without this would have failed every task in production.Samsung and Huawei had no
certificate_alias. The non-Google branch of_get_channel_publish_confignever copied it into its result, sojarsigner._pluck_configurationraisedKeyErroron it.mozillavpndev/fake-prod has no alias at all, andinit_worker.shimports no certificate for it ("no dep signing for mozillavpn yet"). ANonealias reached the jarsigner argv as aTypeErrorfrom subprocess.The test fixtures cannot be verified. The APKs date from 2017 and use SHA1withDSA with a 1024-bit key. Modern JDKs disable both, so jarsigner reports them as unsigned and
-verify -strictexits 16. Their manifests also list entries that were stripped out of the archives, which-strictrejects on its own.Commits
set certificate_alias for non-Google target storesOne line, matching what the Google paths already did. The alias identifies the certificate the incoming artifact was signed with, which the upstream signing task decides, so it is the same whichever store the artifact goes to. Huawei support, currently in flight, needs no further change.fail loudly when no certificate_alias is configuredConfigValidationErrornaming the store and pointing atskip_check_signature, instead of aKeyErroror aTypeErrorfrom inside subprocess.install the JDK, so jarsigner existsdefault-jdk-headlessin both the deployed and the test image.sign the integration fixtures so they can be verifiedGenerate a throwaway RSA-2048 keypair per test and re-sign each fixture under the alias its config expects, after dropping the staleMETA-INFso jarsigner writes a fresh manifest. The generated configs now declare SHA-256, which is whatworker.ymluses in production. Also fixestest_main_with_samsung_store, which signed fornightlywhile the fenix release app it targets declaresfenix-production.verify APK signatures, which never actually ranReadproduct_config, default to verifying.mozillavpnnon-prod opts out explicitly, as the one config that genuinely cannot verify.Testing
default-jdk-headlesscontainer, with the integration tests genuinely runningjarsigner -verify -strictagainst freshly signed APKs.test_script.pytest fails against the old code withassert 0 == 2, so it is a real regression test.worker.ymlcombination for a missing alias.mozillavpndev/fake-prod is the only one, and it opts out.Risk
This enables a check that has been dead since before the monorepo import, so production artifacts have never been through
jarsigner -verify -stricton a modern JDK. Autograph signs with PKCS#7 and SHA-256, so it should hold, but the fixtures show that weak-algorithm rejection is not hypothetical.Worth watching the first dev/fake-prod task before this reaches prod.
skip_check_signature: trueon a product is the escape hatch if some artifact turns out not to verify.