+++ title = "Signing Android Apps Using a YubiKey (on NixOS)" date = "2023-07-24" template = "post.html" aliases = [ "/Android-Yubikey-Signing.html" ] +++ In my spare time, I currently develop two Android apps using *Flutter*: [AniTrack](https://codeberg.org/PapaTutuWawa/anitrack), a simple anime and manga tracker based on my own needs, and [Moxxy](https://moxxy.org), a modern XMPP client. While I don't provide release builds for AniTrack, I do for Moxxy. Those are signed using the key-pair that Flutter generates. I thought to myself: "Wouldn't it be cool if I could keep the key-pair on a separate device which does the signing for me?". The consequence of this thought is that I bought a *YubiKey 5c*. However, as always, using it for my purposes did not go without issues. The first issue is that the official [*Android* documentation](https://developer.android.com/build/building-cmdline#deploy_from_bundle) says to use the `apksigner` tool for creating the signature. [The *YubiKey* documentation](https://developers.yubico.com/PIV/Guides/Android_code_signing.html), however, uses `jarsigner`. While I, at first, did not think much of it, *Android* has [different versions of the signature algorithm](https://source.android.com/docs/security/features/apksigning/): `v1` (what `jarsigner` does), `v2`, `v3`, `v3.1` and `v4`. While it seems like it would be no problem to just use `v1` signatures, *Flutter*, by default, generates `v1` and `v2` signatures, so I thought that I should keep it like that. So, the solution is to just use `apksigner` instead of `jarsigner`, like [another person on the Internet](https://geoffreymetais.github.io/code/key-signing/) did. But that did not work for me. Running `apksigner` like that makes it complain that `apksigner` cannot access the required `sun.security.pkcs11.SunPKCS11` Java class. ``` > /nix/store/ib27l0593bi4ybff06ndhpb8gyhx5zfv-android-sdk-env/share/android-sdk/build-tools/34.0.0/apksigner sign \ --ks NONE \ --ks-pass "pass:" \ --provider-class sun.security.pkcs11.SunPKCS11 \ --provider-arg ./provider.cfg \ --ks-type PKCS11 \ --min-sdk-version 24 \ --max-sdk-version 34 \ --in unsigned.apk \ --out signed.apk Exception in thread "main" java.lang.IllegalAccessException: class com.android.apksigner.ApkSignerTool$ProviderInstallSpec cannot access class sun.security.pkcs11.SunPKCS11 (in module jdk.crypto.cryptoki) because module jdk.crypto.cryptoki does not export sun.security.pkcs11 to unnamed module @75640fdb at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392) at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674) at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:489) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) at com.android.apksigner.ApkSignerTool$ProviderInstallSpec.installProvider(ApkSignerTool.java:1233) at com.android.apksigner.ApkSignerTool$ProviderInstallSpec.access$200(ApkSignerTool.java:1201) at com.android.apksigner.ApkSignerTool.sign(ApkSignerTool.java:343) at com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:92) ``` It may only be an issue because I use NixOS, as I cannot find another instance of someone else having this issue. But I still want my APK signed using the key-pair on my *YubiKey*. After a lot of trial and error, I found out that I can force Java to export certain classes using the `--add-exports` flag. Since `apksigner` complained that the security classes are not exported to its unnamed class, I had to specify `--add-exports sun.security.pkcs11.SunPKCS11=ALL-UNNAMED`. ## My Setup TL;DR: I wrapped this entire setup (minus the Gradle config as that's a per-project thing) into a fancy [script](https://codeberg.org/PapaTutuWawa/bits-and-bytes/src/branch/master/src/flutter/build.sh). My provider configuration for the signature is exactly like the one provided in [previously mentioned blog post](https://geoffreymetais.github.io/code/key-signing/#set-up-your-own-management-key), with the difference that I cannot use the specified path to the `opensc-pkcs11.so` as I am on NixOS, where such paths are not used. So in my setup, I either use the Nix REPL to build the derivation for `opensc` and then use its `lib/opensc-pkcs11.so` path (`/nix/store/h2bn9iz4zqzmkmmjw9b43v30vhgillw4-opensc-0.22.0` in this case) for testing or, as used in [AniTrack](https://codeberg.org/PapaTutuWawa/anitrack/src/branch/master/flake.nix), let Nix figure out the path by building the config file from within my Nix Flake: ```nix { # ... providerArg = pkgs.writeText "provider-arg.cfg" '' name = OpenSC-PKCS11 description = SunPKCS11 via OpenSC library = ${pkgs.opensc}/lib/opensc-pkcs11.so slotListIndex = 0 ''; # ... } ``` Next, to force Java to export the `sun.security.pkcs11.SunPKCS11` class to `apksigner`'s unnamed class, I added `--add-exports sun.security.pkcs11.SunPKCS11` to the Java command line. There are two ways of doing this: 1. Since `apksigner` is just a wrapper script around calling `apksigner.jar`, we could patch the wrapper script to include this parameter. 2. Use the wrapper script's built-in mechanism to pass arguments to the `java` command. While option 1 would work, it would require, in my case, to override the derivation that builds my Android SDK environment, which I am not that fond of. Using `apksigner`'s way of specifying Java arguments (`-J`) is much easier. However, there is a little trick to it: When you pass `-Jsomething` to `apksigner`, the wrapper scripts transforms it to `java -something`. As such, we cannot pass `-Jadd-exports sun.security.pkcs11.SunPKCS11` because it would get transformed to `java -add-exports sun.security.[...]`, which is not what we want. To work around this, I quote the entire parameter to trick Bash into thinking that I'm passing a single argument: `-J"-add-exports sun.security.pkcs11.SunPKCS11"`. This makes the wrapper append `--add-exports sun.security.pkcs11.SunPKCS11` to the Java command line, ultimately allowing me to sign unsigned Android APKs with the key-pair on my *YubiKey*. Since signing a signed APK makes little sense, we also need to tell Gradle to *not* sign the APK. In the case of Flutter apps, I modified the `android/app/build.gradle` file to use a null signing config: ```gradle android { // ... buildTypes { release { // This prevents Gradle from signing release builds. // I don't care what happens to debug builds as I'm not distributing them. signingConfig null } } } ```