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 want bundletool 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.