How to Use Gradle verification-metadata.xml for Dependency Security
Purpose
When I read about abandoned JitPack namespaces being hijacked to serve malicious code, I realized my Gradle projects were vulnerable. Any dependency from a compromised repository could inject malware into my build.
This post demonstrates how to use Gradle’s verification-metadata.xml to lock dependency checksums and prevent supply chain attacks.
Environment
- Gradle 8.5+
- Java 17
- GitHub Actions for CI/CD
The Problem
Abandoned open-source package namespaces can be hijacked by attackers. They take over the original repository and publish malicious code with the same version numbers. Even if you’re careful about updating dependencies, transitive dependencies can be compromised through hijacked namespaces.
The core risk is:
- Direct dependencies you explicitly declare
- Transitive dependencies pulled in by your dependencies
- Any dependency from hijackable repositories (like JitPack)
The Solution: Gradle Dependency Verification
Gradle’s verification metadata system creates cryptographic checksums (SHA-256) for all dependencies. When enabled, Gradle verifies that every downloaded JAR matches the expected checksum, blocking compromised dependencies.
I set this up in my project with one command:
./gradlew --write-verification-metadata sha256This generates verification-metadata.xml in the project root with checksums for every dependency.
verification-metadata.xml Structure
Here’s what the generated file looks like:
<?xml version="1.1" encoding="UTF-8"?><verification-metadata xmlns="https://schema.gradle.org/dependency-verification"> <configuration> <verify-metadata>true</verify-metadata> <verify-signatures>true</verify-signatures> <trusted-artifacts> <trusted-group>org.apache.logging.log4j</trusted-group> </trusted-artifacts> </configuration> <components> <component group="com.example" name="my-dependency" version="1.0.0"> <artifact name="my-dependency-1.0.0.jar"> <sha256 value="a1b2c3d4e5f6789..."/> </artifact> </component> </components></verification-metadata>I can explain the key parts:
verify-metadata: Enables checksum verificationverify-signatures: Enables PGP signature verification (optional but recommended)trusted-artifacts: Groups you trust without verificationcomponents: The actual dependency checksums
Finding Hidden JitPack Dependencies
Before generating verification metadata, I needed to find all JitPack dependencies including transitive ones. I used the dependencyInsight task:
# Find specific JitPack dependencies./gradlew dependencyInsight --dependency com.github.some-org --configuration runtimeClasspath
# Show full dependency tree./gradlew dependencies --configuration runtimeClasspathThis revealed transitive JitPack dependencies I didn’t know about. The dependencyInsight task is your friend for finding dependencies that might not be obvious.
Configuring Gradle for Dependency Verification
I added this configuration to my build.gradle:
dependencyVerification { failOnStaleMetadata = true failOnAnyHashMismatch = true
trustedKeys { // Add PGP keys for trusted repositories key("ORG-ALIAS", "-----BEGIN PGP PUBLIC KEY BLOCK-----...") }
verifySha256sums = true}The critical settings:
failOnStaleMetadata: Build fails if verification metadata is missing for a dependencyfailOnAnyHashMismatch: Build fails if checksums don’t match
CI/CD Integration
The most important part is running this verification in CI. I configured GitHub Actions to fail builds if dependencies change without updating the metadata:
name: Build with dependency verificationon: [push, pull_request]
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Verify dependencies run: | ./gradlew --write-verification-metadata sha256 ./gradlew build env: GRADLE_OPTS: "-Dorg.gradle.dependency.verification.failOnStaleMetadata=true"This ensures:
- Dependencies are verified on every build
- New or changed dependencies fail the build
- The metadata file stays current with dependency updates
How It Works
When I run ./gradlew build with verification enabled:
- Gradle reads
verification-metadata.xml - For each dependency, it downloads the artifact
- It calculates the SHA-256 checksum of the downloaded artifact
- It compares the checksum to the stored value
- If they don’t match, the build fails immediately
The same process works for:
- Direct dependencies in your
build.gradle - Transitive dependencies from any repository
- Plugins and their dependencies
What Happens When Dependencies Change
When I update a dependency:
./gradlew --write-verification-metadata sha256Gradle regenerates the metadata with new checksums. I commit the updated file along with my dependency change. If an attacker tries to serve a malicious artifact from a hijacked namespace, the build fails because the checksum won’t match.
Why This Matters
I think the key reason dependency verification is essential:
- Prevents supply chain attacks at the build level: Even if a repository is compromised, malicious code can’t enter your build
- Creates immutable dependency snapshots: You know exactly what code you’re building with
- Provides early warning: If a dependency changes unexpectedly, you know immediately
- Works with any repository: Maven, Gradle, JitPack, private repos - it doesn’t matter
Common Mistakes
I made several mistakes when setting this up:
- Not updating metadata after dependency updates: The build would fail because the checksums were stale
- Only running locally without CI integration: Dependencies could change in CI without detection
- Ignoring transitive dependencies: I initially only verified direct dependencies
- Not using
dependencyInsight: I missed hidden JitPack dependencies
The solution was to integrate verification into CI from the start and use dependencyInsight to find all dependencies before generating metadata.
Summary
In this post, I showed how to use Gradle’s verification-metadata.xml to lock dependency checksums and prevent supply chain attacks. The key point is that cryptographic verification creates an immutable record of expected dependencies, blocking any compromised artifacts from entering your build.
Generate the metadata with ./gradlew --write-verification-metadata sha256, configure CI to fail on stale metadata, and use dependencyInsight to find hidden dependencies. This gives you protection against namespace hijacking and other supply chain attacks.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Gradle Dependency Verification Documentation
- 👨💻 Reddit: Check your build.gradle for old JitPack dependencies
- 👨💻 OWASP Dependency Check
- 👨💻 JitPack.io
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments