Inside Code Transparency: The JWT File
I am starting to spend a bit of time poking around the implementation of code transparency, with an eye towards filling in some of the gaps that I wrote about in my initial thoughts post.
This time, let’s add code transparency to an App Bundle and see what that really means.
Adding Code Transparency
The first thing that you will need is a suitable Java keystore. This should not
be the one that you use for any purpose other than code transparency. It also
needs to have a 3072-bit key size (or higher, presumably). This means that
the Android Studio keystore UI will not work,
and you will need to create the keystore the old-fashioned way:
using keytool
.
You will also need an up-to-date copy of bundletool
.
And, of course, you will need an App Bundle for your app.
From there, you can use the add-transparency
command to
add code transparency to a copy of the App Bundle:
bundletool add-transparency \
--bundle=/path/to/your/AppBundle.aab \
--output=/path/to/your/AppBundleWithCT.aab \
--ks=/path/to/your/keystore.jks \
--ks-key-alias=WhateverAliasYouUsed
In a nutshell:
-
--bundle
points to the App Bundle that you created (e.g., from Studio) -
--output
points to where you wantbundletool
to write the augmented App Bundle -
--ks
points to your keystore -
--ks-key-alias
is the alias of the key inside that keystore that you wish to use
You will be prompted for the keystore password at the command line, or there are
ways to use a --ks-pass
command-line option to supply it.
This may take several seconds or longer, depending on the size of your App Bundle,
the power of your machine running bundletool
, the current phase of the moon, etc.
Examining the Augmented App Bundle
App Bundle .aab
files are really ZIP archives, so you can examine them using your
favorite ZIP utility. In an App Bundle with code transparency, you will find a
file at:
/BUNDLE-METADATA/com.android.tools.build.bundletool/code_transparency_signed.jwt
The BUNDLE-METADATA/
directory “is what it says on the tin”: it is metadata about
the contents of the App Bundle. Akin to JAR metadata contents, the contents of
BUNDLE-METADATA/
appear to be namespaced by use, so com.android.tools.build.bundletool
will contain metadata related to bundletool
. code_transparency_signed.jwt
is the actual code transparency file.
Decoding the JWT
That file is a JSON Web Token (JWT). It will be a very long encoded string. For example, a new project from Android Studio 4.2.2 resulted in a 3460-character code transparency JWT, looking a bit like:
eyJhbGciOiJSUzI1NiIsIng1YyI6WyJNSUlFMVRDQ0FyMmdBd0lCQWdJRVMwekdQVEFOQ...
(with a few thousand additional characters in place of the ...
)
So, we need to decode it by one means or another. JWT is used by lots of systems, and so you may already have some tools for decoding its contents. Otherwise, this Web site offers online decoding, and Linux developers can add a bash function to decode at the command line.
(macOS and Windows developers: I’m sure you have something good to use too!)
Note that these tools merely decode the JWT, allowing us to see what is inside of them. They do not validate that the JWT has not been modified — that is a separate step.
Examining the JWT
That scrap project — based on the “Empty Activity” template FWIW — gives us the following JWT payload, after decoding:
{
"codeRelatedFile": [
{
"path": "base/dex/classes.dex",
"sha256": "c8a57ffe798c896f1b2c5f33862cbde817bb233c291217e3511245e1e9b91c82"
},
{
"path": "base/dex/classes2.dex",
"sha256": "192a3e51f14682fb41b91b99e916b068e818ad598d7d5659ea9c7e26c201de15"
},
{
"path": "base/dex/classes3.dex",
"sha256": "08cae05a2180b2249079bbabedaf3a8ac20bef1e4a3d4f8ea319ea4c2f42a396"
}
]
}
There is no specification for this payload, something that we will need to rectify
at some point. But, inside the codeRelatedFile
array, we have individual
JSON objects, each having:
-
path
: a relative path, from the base of the.aab
ZIP contents, of a “code-related file”, such as a DEX file -
sha256
: the SHA-256 hash of the contents of the identified file
DEX files get this shorthand JSON syntax. Native libraries have a couple of additional properties:
{
"path": "base/lib/x86_64/libflipper.so",
"type": "NATIVE_LIBRARY",
"apkPath": "lib/x86_64/libflipper.so",
"sha256": "3f4d0a1a03b7825cb5350453c579d39f9f3369f885b8906dfdf1748510186664"
},
As it turns out, there is a protobuf .proto
file in the bundletool
project
that appears to describe this JSON structure. NATIVE_LIBRARY
is an enum
value, where DEX
is the other value (and presumably is the default value if nothing
is provided). apkPath
is documented as “Path to file in the APK”; it is not
quite clear to me why this is needed for native libraries but not DEX files.
I still have not yet torn into the bundletool
implementation, but the verification
process probably is something like:
-
Confirm that the JWT is signed by the expected signing key (with that chore largely being up to us)
-
Iterate over the JWT payload entries, find the corresponding DEX or
.so
file in the APKs installed for this app, and validate the SHA-256 hashes -
Iterate over the DEX and
.so
files of the APKs installed for this app and confirm that everything there was represented in the JWT (so there has not been a code insertion attack)
I will continue blogging about code transparency in the coming weeks and months, as I try to make sense of how we can cover what Google has not: actually using this to ensure that our apps are not being manipulated.