blog.polynom.me/android-yubikey-signing/index.html
2024-01-11 12:26:22 +00:00

153 lines
11 KiB
HTML

<!doctype html>
<html lang="en-gb">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://blog.polynom.me/css/index.css" rel="stylesheet" integrity="sha384-EJX4ZZbYMJeuoLRp93IbM/RYSzZmxw42TK7sgSRBEMChbBFK4NYUQEfsz3nBJQm8" />
<link rel="alternate" type="application/rss+xml" title="blog.polynom.me Atom feed" href="https://blog.polynom.me/atom.xml">
<meta property="og:description" content="" />
<meta property="og:title" content="Signing Android Apps Using a YubiKey (on NixOS)" />
<title>Signing Android Apps Using a YubiKey (on NixOS)</title>
</head>
<body>
<div class="flex flex-col p-2 md:p-8 items-start md:w-4/5 mx-auto">
<!-- Header -->
<div class="flex flex-row self-center">
<img class="w-12 h-12 md:w-24 md:h-24 rounded-lg" src="https://blog.polynom.me/img/avatar.jpg" integrity="sha384-uiNteVXosQ2+o/izp41L1G9VwuwYDYCOPxzFWks058DMUhW7KfQXcipM7WqgSgEZ" alt="Profile picture"/>
<div class="ml-4 self-center">
<a class="self-center text-2xl font-bold" href="/">PapaTutuWawa's Blog</a>
<ul class="list-none">
<li class="inline mr-8"><a href="/">Posts</a></li>
<li class="inline mr-8"><a href="https://blog.polynom.me/atom.xml">RSS</a></li>
<li class="inline mr-8"><a href="https://polynom.me">About</a></li>
</ul>
</div>
</div>
<!-- Container for posts -->
<div class="mx-auto mt-4 w-full md:max-w-prose">
<h1 class="text-indigo-400 text-3xl">Signing Android Apps Using a YubiKey (on NixOS)</h1>
<span class="text-md mt-2">Posted on 2023-07-24</span>
<!-- Actual article -->
<article class="prose lg:prose-lg text-white mt-4">
<p>In my spare time, I currently develop two Android apps using <em>Flutter</em>: <a href="https://codeberg.org/PapaTutuWawa/anitrack">AniTrack</a>, a
simple anime and manga tracker based on my own needs, and <a href="https://moxxy.org">Moxxy</a>, 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: &quot;Wouldn't it be cool if I could keep
the key-pair on a separate device which does the signing for me?&quot;. The consequence
of this thought is that I bought a <em>YubiKey 5c</em>. However, as always, using it for my
purposes did not go without issues.</p>
<span id="continue-reading"></span>
<p>The first issue is that the official <a href="https://developer.android.com/build/building-cmdline#deploy_from_bundle"><em>Android</em> documentation</a>
says to use the <code>apksigner</code> tool for creating the signature. <a href="https://developers.yubico.com/PIV/Guides/Android_code_signing.html">The <em>YubiKey</em> documentation</a>, however,
uses <code>jarsigner</code>. While I, at first, did not think much of it, <em>Android</em> has
<a href="https://source.android.com/docs/security/features/apksigning/">different versions of the signature algorithm</a>: <code>v1</code> (what <code>jarsigner</code> does), <code>v2</code>, <code>v3</code>, <code>v3.1</code> and
<code>v4</code>. While it seems like it would be no problem to just use <code>v1</code> signatures, <em>Flutter</em>, by default,
generates <code>v1</code> and <code>v2</code> signatures, so I thought that I should keep it like that.</p>
<p>So, the solution is to just use <code>apksigner</code> instead of <code>jarsigner</code>, like <a href="https://geoffreymetais.github.io/code/key-signing/">another person on the Internet</a> did.
But that did not work for me. Running <code>apksigner</code> like that makes it complain that <code>apksigner</code> cannot
access the required <code>sun.security.pkcs11.SunPKCS11</code> Java class.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>&gt; /nix/store/ib27l0593bi4ybff06ndhpb8gyhx5zfv-android-sdk-env/share/android-sdk/build-tools/34.0.0/apksigner sign \
</span><span> --ks NONE \
</span><span> --ks-pass &quot;pass:&lt;YubiKey PIN&gt;&quot; \
</span><span> --provider-class sun.security.pkcs11.SunPKCS11 \
</span><span> --provider-arg ./provider.cfg \
</span><span> --ks-type PKCS11 \
</span><span> --min-sdk-version 24 \
</span><span> --max-sdk-version 34 \
</span><span> --in unsigned.apk \
</span><span> --out signed.apk
</span><span>
</span><span>Exception in thread &quot;main&quot; 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
</span><span> at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
</span><span> at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
</span><span> at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:489)
</span><span> at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
</span><span> at com.android.apksigner.ApkSignerTool$ProviderInstallSpec.installProvider(ApkSignerTool.java:1233)
</span><span> at com.android.apksigner.ApkSignerTool$ProviderInstallSpec.access$200(ApkSignerTool.java:1201)
</span><span> at com.android.apksigner.ApkSignerTool.sign(ApkSignerTool.java:343)
</span><span> at com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:92)
</span></code></pre>
<p>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 <em>YubiKey</em>. After a lot of trial and error, I found out that I can force Java to export certain classes
using the <code>--add-exports</code> flag. Since <code>apksigner</code> complained that the security classes are not exported to its
unnamed class, I had to specify <code>--add-exports sun.security.pkcs11.SunPKCS11=ALL-UNNAMED</code>.</p>
<h2 id="my-setup">My Setup</h2>
<p>TL;DR: I wrapped this entire setup (minus the Gradle config as that's a per-project thing) into a fancy <a href="https://codeberg.org/PapaTutuWawa/bits-and-bytes/src/branch/master/src/flutter/build.sh">script</a>.</p>
<p>My provider configuration for the signature is exactly like the one provided in <a href="https://geoffreymetais.github.io/code/key-signing/#set-up-your-own-management-key">previously mentioned blog post</a>,
with the difference that I cannot use the specified path to the <code>opensc-pkcs11.so</code> 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 <code>opensc</code> and then
use its <code>lib/opensc-pkcs11.so</code> path (<code>/nix/store/h2bn9iz4zqzmkmmjw9b43v30vhgillw4-opensc-0.22.0</code> in this case) for testing or, as
used in <a href="https://codeberg.org/PapaTutuWawa/anitrack/src/branch/master/flake.nix">AniTrack</a>, let Nix figure out the path by building
the config file from within my Nix Flake:</p>
<pre data-lang="nix" style="background-color:#2b303b;color:#c0c5ce;" class="language-nix "><code class="language-nix" data-lang="nix"><span>{
</span><span> </span><span style="color:#65737e;"># ...
</span><span> </span><span style="color:#d08770;">providerArg </span><span>= </span><span style="color:#bf616a;">pkgs</span><span>.</span><span style="color:#bf616a;">writeText </span><span>&quot;</span><span style="color:#a3be8c;">provider-arg.cfg</span><span>&quot; &#39;&#39;
</span><span style="color:#a3be8c;"> name = OpenSC-PKCS11
</span><span style="color:#a3be8c;"> description = SunPKCS11 via OpenSC
</span><span style="color:#a3be8c;"> library = </span><span style="font-style:italic;color:#ab7967;">${</span><span style="font-style:italic;color:#bf616a;">pkgs</span><span style="font-style:italic;color:#c0c5ce;">.</span><span style="font-style:italic;color:#bf616a;">opensc</span><span style="font-style:italic;color:#ab7967;">}</span><span style="color:#a3be8c;">/lib/opensc-pkcs11.so
</span><span style="color:#a3be8c;"> slotListIndex = 0
</span><span style="color:#a3be8c;"> </span><span>&#39;&#39;;
</span><span> </span><span style="color:#65737e;"># ...
</span><span>}
</span></code></pre>
<p>Next, to force Java to export the <code>sun.security.pkcs11.SunPKCS11</code> class to <code>apksigner</code>'s unnamed class, I added <code>--add-exports sun.security.pkcs11.SunPKCS11</code>
to the Java command line. There are two ways of doing this:</p>
<ol>
<li>Since <code>apksigner</code> is just a wrapper script around calling <code>apksigner.jar</code>, we could patch the wrapper script to include this parameter.</li>
<li>Use the wrapper script's built-in mechanism to pass arguments to the <code>java</code> command.</li>
</ol>
<p>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 <code>apksigner</code>'s way of specifying Java arguments (<code>-J</code>) is much easier. However, there is a little trick to it: When you pass <code>-Jsomething</code> to <code>apksigner</code>,
the wrapper scripts transforms it to <code>java -something</code>. As such, we cannot pass <code>-Jadd-exports sun.security.pkcs11.SunPKCS11</code> because it would get transformed
to <code>java -add-exports sun.security.[...]</code>, 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: <code>-J&quot;-add-exports sun.security.pkcs11.SunPKCS11&quot;</code>. This makes the wrapper append <code>--add-exports sun.security.pkcs11.SunPKCS11</code> to the
Java command line, ultimately allowing me to sign unsigned Android APKs with the key-pair on my <em>YubiKey</em>.</p>
<p>Since signing a signed APK makes little sense, we also need to tell Gradle to <em>not</em> sign the APK. In the case of Flutter apps, I modified the <code>android/app/build.gradle</code>
file to use a null signing config:</p>
<pre data-lang="gradle" style="background-color:#2b303b;color:#c0c5ce;" class="language-gradle "><code class="language-gradle" data-lang="gradle"><span>android {
</span><span> </span><span style="color:#65737e;">// ...
</span><span> buildTypes {
</span><span> release {
</span><span> </span><span style="color:#65737e;">// This prevents Gradle from signing release builds.
</span><span> </span><span style="color:#65737e;">// I don&#39;t care what happens to debug builds as I&#39;m not distributing them.
</span><span> signingConfig </span><span style="color:#d08770;">null
</span><span> }
</span><span> }
</span><span>}
</span></code></pre>
</article>
<!-- Common post footer -->
<div class="mt-6">
<span class="prose lg:prose-lg text-md text-white">
If you have any questions or comments, then feel free to send me an email (Preferably with GPG encryption)
to papatutuwawa [at] polynom.me or reach out to me on the Fediverse at <a href="https://social.polynom.me&#x2F;papatutuwawa">@papatutuwawa@social.polynom.me</a>.
</span>
</div>
</div>
</div>
</body>
</html>