blog.polynom.me/content/2023-07-24-Android-Yubikey-Signing.md
Alexander "PapaTutuWawa caef031d48
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Initial commit
2024-01-05 18:10:44 +01:00

109 lines
6.6 KiB
Markdown

+++
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.
<!-- more -->
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:<YubiKey PIN>" \
--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
}
}
}
```