Dependency verification

Compromised Gradle dependencies pose a security risk. A malicious actor could potentially inject a modified dependency into the build process, for example, through a man-in-the-middle attack during dependency resolution.

If a build dependency (a library) has been compromised, it can affect the way your application executes on a device. If a plugin dependency has been compromised, it can change the way your build works, or even run external commands on the build machine.

To mitigate this, you can enable Dependency verification in your build.

Library checksums and signatures

Library authors can provide two pieces of metadata that can help verify the authenticity of the dependencies you are downloading. You define a file named gradle/verification-metadata.xml to specify which values you approve. It can contain:

  • Checksums - a hash of an artifact that you can use to verify that the artifact was not corrupted during transport. If the checksum was retrieved from a trusted source, it tells you that the artifact has not changed, reducing man-in-the-middle attacks.

    The downside is that, because checksums are calculated from the artifacts, they change with every release, requiring you to update gradle/verification-metadata.xml whenever you upgrade them.

  • Signatures - allows dependency users to specify a public key for a given artifact in order to validate that this artifact was built and signed by the library author who is the authenticated owner of that public key. This is more work for the library author, but as long as their private key itself has not been compromised, the signature tells you the library is legitimate.

    If the library author signs each version of an artifact with the same key, you don't need to update gradle/verification-metadata.xml when you upgrade them.

Enable dependency verification

Gradle dependency verification compares checksums and signatures during your build.

Create a gradle/verification-metadata.xml file that contains the following:

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata
    xmlns="https://schema.gradle.org/dependency-verification"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
    <configuration>
        <!-- verify .pom and .module files -->
        <verify-metadata>true</verify-metadata>
        <!-- verify .asc PGP files that come with the artifacts -->
        <verify-signatures>true</verify-signatures>
        <!-- use human readable keyring format -->
        <keyring-format>armored</keyring-format>
        <!-- read keys in a local file, fewer requests to network -->
        <key-servers enabled="false">
            <key-server uri="https://keyserver.ubuntu.com"/>
            <key-server uri="https://keys.openpgp.org"/>
        </key-servers>
    </configuration>
    <components>
    </components>
</verification-metadata>

This serves as a starting point and will be updated shortly.

Run ./gradlew assembleDebug to see how this changes the build. You'll see messages such as

* What went wrong:
Error resolving plugin [id: 'com.android.application', version: '8.7.3', apply: false]
> Dependency verification failed for configuration 'detachedConfiguration1'
  One artifact failed verification: com.android.application.gradle.plugin-8.7.3.pom ...
  This can indicate that a dependency has been compromised ...
  
  Open this report for more details: .../dependency-verification-report.html

Gradle is telling you that you are pulling in versions of dependencies that you have not explicitly approved.

Bootstrap the checksum and signature data

You can bootstrap the initial set trusted keys and components. This process gathers the current signatures and checksums for all libraries used by your project.

Generate the initial metadata by running

./gradlew --write-verification-metadata pgp,sha256 --export-keys help

This command tells Gradle to build a list of PGP keys and fallback checksums for all the dependencies that are used in this project. You will see a change to your verification-metadata.xml with a number of entries like:

<trusted-key id="8461EFA0E74ABAE010DE66994EB27DB2A3B88B8B">
    <trusting group="androidx.activity"/>
</trusted-key>

This tells Gradle that if it sees a dependency from maven group androidx.activity, it will ensure that the accompanying .asc files (signatures stored in the repository) match that key.

Bootstrapping will also generate gradle/verification-keyring.keys that contains the public PGP keys used by your build. Check in both of these files into your version tracking system. Any future changes that modify either verification-metadata.xml or verification-keyring.keys should be carefully reviewed.

Strip versions from trusted keys

Signing keys rarely change between releases of a library. The generated data in the gradle/verification-metadata.xml file contains version details, meaning You would need to re-add key information for each new dependency version.

To avoid this, and specify that the key applies to all versions of a library, remove the version specifications.

In the Android Studio editor, use Edit > Find > Replace... using regular expression to replace all version specifications for trusted keys.

  • from: <trusted-key(.*) version=\".*\"/>
  • to: <trusted-key$1/>

Android Studio synchronization

So far, your command-line build works, but if you try to synchronize in Android Studio you'll see errors like

A build operation failed.
    Dependency verification failed for configuration ':app:detachedConfiguration3'
One artifact failed verification: gradle-8.10.2-src.zip (gradle:gradle:8.10.2) from repository Gradle distributions
If the artifacts are trustworthy, you will need to update the gradle/verification-metadata.xml file. For more on how to do this, please refer to https://docs.gradle.org/8.10.2/userguide/dependency_verification.html#sec:troubleshooting-verification in the Gradle documentation.

Android Studio wants to download the Gradle sources (along with other sources and docs). The easiest way to correct this is to trust all source and javadocs. Add <trusted-artifacts> in gradle/verification-metadata.xml:

<verification-metadata ...>
   <configuration>
      <trusted-artifacts>
         <trust file=".*-javadoc[.]jar" regex="true"/>
         <trust file=".*-sources[.]jar" regex="true"/>
         <trust group="gradle" name="gradle"/>
      </trusted-artifacts>
      ...
  </configuration>
</verification-metadata>

Your build will now work properly from the command line and Android Studio.