Alexander "PapaTutuWawa
caef031d48
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
109 lines
6.6 KiB
Markdown
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
|
|
}
|
|
}
|
|
}
|
|
```
|