Skip to content

Conversation

mlisikbf
Copy link

@mlisikbf mlisikbf commented Sep 30, 2025

Summary

Introduces aab signing using the existing yarn sign:android command, with the same list of arguments, just provide a path to an .aab file. signAndroid will check file extension to decide on:

  • the assets path for jsbundle placement
  • what signer to use
  • when to perform zipalign (before signing for app bundles vs after signing for apks)

Includes naming changes, updates to messages and docs where appropriate.

An alternative would be for end-users to invoke jarsigner themselves, extract and modify the bundle as needed.

Test plan

  • on this branch, create a symlink for the platform package: (cd packages/platform-android && npm link)
  • create an empty rock project with npm rock create (select android, skip plugins)
  • link the package with npm link @rock-js/platform-android
  • create a release keystore with: npm rock create-keystore:android - use default values, for simplicity use fake-pass when prompted for password
  • create a release aab with: npx rock build:android --aab --local --variant release (note: using --local to opt out of cache as it does not seem to catch js changes for release variants)
  • make changes in App.tsx that will be easy to identify (eg replace <WelcomeScreen/> with custom jsx)
  • create a bundle with npx rock bundle --platform android --entry-file index.js --bundle-output output.bundle
  • re-sign aab with npx rock sign:android ./android/app/build/outputs/bundle/release/app-release.aab --keystore ./android/app/release.keystore --keystore-password fake-pass --key-password fake-pass --key-alias rock-alias --jsbundle ./output.bundle
  • to create an apk from a bundle file, use bundletool -- brew install bundletool then bundletool build-apks --bundle=./android/app/build/outputs/bundle/release/app-release.aab --output=output/apks.apks --mode=universal --local-testing --ks=./android/app/release.keystore --ks-key-alias=rock-alias --ks-pass=pass:fake-pass --key-pass=pass:fake-pass
  • extract apks: unzip output/apks.apks -d output and install on a connected device with adb install output/universal.apk
  • once ran, the app should show UI changes made after first bundle was created

revert changes and verify apk signing:

  • npx rock build:android --local --variant release
  • make UI changes to App.tsx
  • then:
npx rock bundle --platform android --entry-file index.js --bundle-output output.bundle
npx rock sign:android ./android/app/build/outputs/apk/release/app-release.apk --keystore ./android/app/release.keystore --keystore-password fake-pass --key-password fake-pass --key-alias rock-alias --jsbundle ./output.bundle
adb install ./android/app/build/outputs/apk/release/app-release.apk

closes #588


Note

Generalizes Android signing to handle both APK and AAB, updating args, signing/align flow, bundle replacement paths, and docs.

  • Platform Android (@rock-js/platform-android)
    • AAB Support in sign:android: Now signs both APK and AAB based on file extension.
      • Uses apksigner for APK and jarsigner for AAB; adjusts zipalign order accordingly.
      • Replaces JS bundle in assets (APK) or base/assets (AAB).
      • Introduces generic binaryPath throughout (apk -> binaryPath/path) and updates messages.
      • New helpers: alignArchiveFile, signAab, signApk, isAab, and password handling for jarsigner.
  • Docs
    • Updates website/src/docs/cli.md for sign:android to accept APK or AAB and reflect new binaryPath argument and output descriptions.
  • Release
    • Changeset: minor bump for @rock-js/platform-android.

Written by Cursor Bugbot for commit 321b308. This will update automatically on new commits. Configure here.

Copy link

changeset-bot bot commented Sep 30, 2025

🦋 Changeset detected

Latest commit: 321b308

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@rock-js/platform-android Minor
@rock-js/plugin-brownfield-android Minor
@rock-js/config Minor
@rock-js/platform-apple-helpers Minor
@rock-js/platform-ios Minor
@rock-js/plugin-brownfield-ios Minor
@rock-js/plugin-metro Minor
@rock-js/plugin-repack Minor
@rock-js/provider-github Minor
@rock-js/provider-s3 Minor
@rock-js/test-helpers Minor
@rock-js/tools Minor
@rock-js/welcome-screen Minor
rock Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

vercel bot commented Sep 30, 2025

@mlisikbf is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Comment on lines 339 to 341
function isAab(filePath: string): boolean {
return path.extname(filePath).toLowerCase() === '.aab';
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opted to switch on path extension rather than adding a separate --aab flag, as that should be enough to distinguish and the flag seemed superfluous.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense 👍🏼

Copy link
Contributor

@thymikee thymikee Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please run prettier here and there (this one is missing trailing newline)

cursor[bot]

This comment was marked as outdated.

@mlisikbf mlisikbf requested a review from thymikee October 14, 2025 14:50
@thymikee
Copy link
Contributor

It's on my list, will come back to this shortly!

Copy link
Contributor

@artus9033 artus9033 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, thanks for the contribution! Just the few comments to be resolved + I think we should decide to go with apksigner instead of jarsigner to make it more secure & follow Android docs' recommendations.

];

try {
await spawn('jarsigner', jarsignerArgs);
Copy link
Contributor

@artus9033 artus9033 Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be better to use apksigner actually. Jarsigner is not recommended in the docs and it was not created for signing APKs, therefore e.g. it:

  • does not know that APKs for Android <= 17 cannot be signed with SHA-256 digests ([major] - I also don't see the code that would adjust that), while apktool detects & adjusts that automatically
  • [major] there are security concerns that come from jarsigner, which constitues Android's v1 signing scheme: it only signs parts of the ZIP (APK / AAB), e.g. ZIP metadata is not signed

Copy link
Author

@mlisikbf mlisikbf Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting. I assumed apksigner won't work based on bundletool docs explicitly calling out not to use it. Testing locally, it works and only requires a min-sdk-version param, and using bundletool I can get apks out of the resigned bundle that pass apksigner verify (and install & run fine).

Will need to test it in our pipeline (with play store test track submission), and come back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to hear that! Sure, let's wait for the result - thanks for checking this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the min-sdk-version is required, you can take default version from the template (currently 24) and add a --min-sdk-version flag where users will be able to overwrite it when necessary. Btw can you check if the tool fails when sdk mismatches? that would be ideal as we could point users to that flag to avoid confusion.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this indicates we shouldn't use apksigner, or just that it's API is tailored for use with apks.

I've tested a few combinations with --min-sdk-version, and in each one, I can successfully produce apks from the bundle locally using bundletool -- and by uploading the bundle to play console and downloading an apk from there. It would seem that the value of --min-sdk-version is irrelevant when signing an aab:

  • project with min sdk 34, --min-sdk-version lower - 0, 24
  • project with min sdk 24, --min-sdk-version higher - 34, 36
  • project with min sdk 34, --min-sdk-version higher - 36

All tested on a device with sdk 34 / Android 14, using bundletool build-apks --connected-device and bundletool install-apks to test locally.

In apksigner docs, there's this note on --min-sdk-version:

Higher values allow the tool to use stronger security parameters when signing the app but limit the APK's availability to devices running more recent versions of Android

In our case, I can install on lower version when setting --min-sdk-version high for aab signing, because at a later point - when prepared by play store or built by bundletool, the individual apks will be signed again (and will read sdk versions from manifest)

Updated the PR to remove jarsigner-related bits. This reduces the change set to extension-agnostic naming, different asset path and --min-sdk-version added for aabs.

Added a --min-sdk-version arg to the command. Set it to default to 36 for aabs, just so that stronger security parameters are used for signing. User can alternatively provide an override.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking!
I'm looking at apksigner help around this flag:

--max-sdk-version     Highest API Level on which this APK's signatures will be
                      verified. By default, the highest possible value is used.

maybe we don't need to expose it? We can add it later if someone requests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one you link is max not min -- but it did cross my mind that it might be better no to expose --min-sdk-version and just have a default for aabs. Will remove it just to keep the PR focused on aabs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, let's do that.

// 6. Align archive after signing if aab
if (isAab(outputPath)) {
await alignArchive()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The post-signing alignment for AABs also overwrites the signed outputPath with the unsigned tempArchivePath, losing the signature

This bot's comment is actually incorrect, we can ignore that (LLM trash as usual). The issue is exactly why apksigner is preferred over jarsigner, because jarsigner means Android v1 signature. apksigner uses v2,3 or 4 signing which is whole-file signing and therefore there is no way to tamper with the file after signing and therefore signing is needed after alignment. However, if we move to apksigner (as I suggested in a comment below), we would then have to keep in mind the (only) right way would be align-then-sign.

also, can you send me some materials on why AAB needs aligning after signing, while APK does it before?

It's not that they need it that way, it results from the tool choice. Jarsigner (legacy, which constitutes the old Android singing scheme v1) only signs parts of the ZIP (APK / AAB), and the signatures don't cover ZIP metadata. Zipalign stores the actual alignment information in the ZIP metdata (not covered by the signatures) and this is why the signing should occur after alignment.

Apksigner signs according to v2+ Android signing schemes which treat the whole file as a blob (including ZIP metadata). Therefore, realigning means invalidating the signature.

Some information on that can be found here: https://source.android.com/docs/security/features/apksigning#v1

return [];
}

return ['--min-sdk-version', minSdkVersion || '36'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minSdkVersion is 24. 36 is the compileSdkVersion

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree to keep 36 given it does not appear we need to match actual min sdk?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it fail if you don’t have sdk 36 or silently pass? If I read the man correctly it will set the latest available so I wonder if we even need to set that flag.

@thymikee
Copy link
Contributor

Minor tweaks and we're good to merge this today

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sign:android command for android app bundles

3 participants