Dependency Verification, Checksums, Signatures, Repository Content Filtering, and Supply-Chain Security: Guided Hands-On Workflow and Core Operations
Build a disposable local supply-chain lab: create a deterministic internal artifact, bind its group to one repository, bootstrap and review Gradle verification metadata, verify Wrapper integrity, inspect Maven checksum/signature controls, then trigger and explain a harmless checksum failure.
Learning objectives
- Create two disposable Maven-layout repositories containing the same coordinate but different bytes.
- Configure Gradle exclusive content so an internal group can only resolve from the trusted repository.
- Bootstrap and review SHA-256 dependency verification metadata and interpret strict verification.
- Verify Gradle Wrapper distribution/JAR integrity without trusting the checkout as its own source of truth.
- Inspect Maven checksum/signature mechanics and simulate a harmless checksum mismatch locally.
Lab boundary. Run only in a disposable Chapter 28
directory. The fixture uses synthetic group
dev.academy.internal and project-relative repositories.
Never point these mutation steps at a production artifact
repository, shared cache, normal ~/.gradle, or normal
~/.m2.
1. Preflight and identity record
The lab assumes the trusted project Wrapper from Chapter 15 is already present and pinned to Gradle 9.7.1. JDK 21 is used to build the synthetic JAR and Java 17 is the bytecode target. Maven 3.9.16 is optional for the Maven consumer inspection; the integrity experiments themselves do not require Maven.
set -euo pipefail
mkdir ch28-lab
cd ch28-lab
export GRADLE_USER_HOME="$PWD/.lab-gradle-home"
java -version 2>&1 | tee java-version.txt
javac -version | tee javac-version.txt
# After copying the trusted wrapper files into this disposable lab:
./gradlew --version | tee gradle-version.txt
cat gradle/wrapper/gradle-wrapper.properties | tee wrapper-properties.txt
2. Create trusted and alternate repositories
Both repositories expose the exact same Maven coordinate. The trusted repository contains a deterministic Java 17 JAR; the alternate repository contains different bytes. This creates a harmless dependency-confusion fixture without any network service.
set -euo pipefail
mkdir -p trusted-repo/dev/academy/internal/policy-lib/1.0.0
mkdir -p alternate-repo/dev/academy/internal/policy-lib/1.0.0
mkdir -p fixture-src/dev/academy/internal fixture-classes
cat > fixture-src/dev/academy/internal/PolicyLib.java <<'JAVA'
package dev.academy.internal;
public final class PolicyLib {
private PolicyLib() {}
public static String message() { return "trusted-v1"; }
}
JAVA
javac --release 17 -d fixture-classes fixture-src/dev/academy/internal/PolicyLib.java
jar --create --date=2026-01-01T00:00:00Z \
--file trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar \
-C fixture-classes .
cat > trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.pom <<'POM'
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy.internal</groupId>
<artifactId>policy-lib</artifactId>
<version>1.0.0</version>
</project>
POM
# Create an alternate repository entry with the same coordinate but different bytes.
printf 'not-the-trusted-jar\n' > alternate-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar
cp trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.pom \
alternate-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.pom
sha256sum trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar \
| tee trusted-artifact.sha256
sha256sum alternate-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar \
| tee alternate-artifact.sha256
The two SHA-256 values must differ. Preserve them as evidence; do not delete or overwrite either artifact yet.
3. Create the consuming Gradle project
Repository scope lives in settings.gradle.kts, not
inside individual subprojects.
FAIL_ON_PROJECT_REPOS prevents later build scripts from
silently adding a broad repository and bypassing the settings-level
policy.
pluginManagement {
repositories {
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
// Public repository is deliberately listed first.
maven {
name = "alternateRepo"
url = uri("alternate-repo")
}
// The internal namespace is exclusive to this repository.
exclusiveContent {
forRepository {
maven {
name = "academyInternal"
url = uri("trusted-repo")
metadataSources { mavenPom(); artifact() }
}
}
filter {
includeGroup("dev.academy.internal")
}
}
}
}
rootProject.name = "chapter28-supply-chain"
plugins {
java
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(17)
}
dependencies {
implementation("dev.academy.internal:policy-lib:1.0.0")
}
package dev.academy.app;
import dev.academy.internal.PolicyLib;
public final class App {
private App() {}
public static String message() { return PolicyLib.message(); }
}
mkdir -p src/main/java/dev/academy/app
cat > src/main/java/dev/academy/app/App.java <<'JAVA'
package dev.academy.app;
import dev.academy.internal.PolicyLib;
public final class App {
private App() {}
public static String message() { return PolicyLib.message(); }
}
JAVA
./gradlew dependencies --configuration compileClasspath | tee dependency-baseline.txt
./gradlew dependencyInsight \
--dependency dev.academy.internal:policy-lib \
--configuration compileClasspath | tee dependency-insight.txt
4. Bootstrap verification metadata—then stop and review
The first metadata generation is not a security verdict. It records the artifacts the current resolver selected. Run it only after verifying repository scope and coordinates, then inspect the diff before committing anything.
./gradlew --write-verification-metadata sha256 clean compileJava \
| tee verification-bootstrap.log
test -f gradle/verification-metadata.xml
grep -n 'dev.academy.internal' gradle/verification-metadata.xml
grep -n 'policy-lib-1.0.0.jar' gradle/verification-metadata.xml
cp gradle/verification-metadata.xml verification-metadata.review-copy.xml
Compare the generated digest with
trusted-artifact.sha256. The coordinate should not
refer to alternate-repo. For real third-party
dependencies, a second trusted channel—not the local cache—must
validate the digest or key.
5. Prove strict verification on an unchanged build
Once verification metadata exists, strict mode is the default. An unchanged clean build should verify successfully.
./gradlew clean compileJava | tee strict-success.log
sha256sum trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar \
| tee strict-trusted.sha256
Do not switch to --dependency-verification off merely
to make a failure disappear. Lenient mode can be useful during a
controlled metadata update because it collects errors, but it is
inherently weaker and should not become the release gate.
6. Replace bytes under the same coordinate and preserve the failure
This simulates a repository compromise or mutable release:
1.0.0 keeps its coordinate but changes bytes.
cp trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar trusted-v1.jar
cat > fixture-src/dev/academy/internal/PolicyLib.java <<'JAVA'
package dev.academy.internal;
public final class PolicyLib {
private PolicyLib() {}
public static String message() { return "changed-same-coordinate"; }
}
JAVA
rm -rf fixture-classes && mkdir fixture-classes
javac --release 17 -d fixture-classes fixture-src/dev/academy/internal/PolicyLib.java
jar --create --date=2026-01-01T00:00:00Z \
--file trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar \
-C fixture-classes .
sha256sum trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar \
| tee changed-artifact.sha256
set +e
./gradlew --refresh-dependencies clean compileJava > checksum-mismatch.log 2>&1
rc=$?
set -e
printf 'mismatch rc=%s\n' "$rc"
test "$rc" -ne 0
grep -Ei 'verification|checksum|policy-lib' checksum-mismatch.log | head -40
The correct repair is investigation plus restoration or an
intentional reviewed version upgrade—not replacing the expected
checksum with the attacker-controlled digest. Keep
checksum-mismatch.log, old/new digests, and both JARs
until the incident is understood.
7. Restore trusted bytes and re-verify
Restore the original immutable artifact, not merely the cache.
cp trusted-v1.jar trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar
./gradlew --refresh-dependencies clean compileJava | tee restored.log
sha256sum -c trusted-artifact.sha256
8. Prove the alternate repository cannot satisfy the protected group
Now test repository scope independently of checksum verification. Temporarily make the trusted repository unable to serve the coordinate while leaving the alternate copy available. Because the group is exclusive, resolution must fail instead of falling through.
mv trusted-repo/dev/academy/internal/policy-lib/1.0.0 \
trusted-repo/dev/academy/internal/policy-lib/1.0.0.off
set +e
./gradlew --refresh-dependencies clean compileJava > repository-scope-failure.log 2>&1
rc=$?
set -e
printf 'scope rc=%s\n' "$rc"
test "$rc" -ne 0
grep -Ei 'policy-lib|could not find|resolve' repository-scope-failure.log | head -40
mv trusted-repo/dev/academy/internal/policy-lib/1.0.0.off \
trusted-repo/dev/academy/internal/policy-lib/1.0.0
./gradlew --refresh-dependencies clean compileJava | tee repository-scope-restored.log
If the alternate repository satisfies the coordinate, the settings
policy is wrong. Check whether exclusiveContent was
replaced with a non-exclusive content filter or whether a project
repository escaped settings governance.
9. Verify the Gradle Wrapper from independent release data
Wrapper verification is outside dependency verification. The 9.7.1
distribution checksum should be pinned in
gradle-wrapper.properties. Verify the Wrapper JAR
against Gradle’s separately published checksum/signature; do not
derive the expected value from the checked-in JAR.
grep '^distributionUrl=' gradle/wrapper/gradle-wrapper.properties
grep '^distributionSha256Sum=' gradle/wrapper/gradle-wrapper.properties
sha256sum gradle/wrapper/gradle-wrapper.jar | tee wrapper-jar.actual.sha256
# When network access is allowed, obtain expected Wrapper JAR checksum independently:
# curl -fsSLo wrapper-jar.expected.sha256 \
# https://services.gradle.org/distributions/gradle-9.7.1-wrapper.jar.sha256
# Compare expected value to the actual committed JAR before executing an untrusted wrapper change.
10. Maven checksum/signature inspection without pretending it is Gradle verification
Create SHA-256/SHA-512 sidecars for the synthetic artifact and
verify them directly. This teaches the underlying repository
evidence. Maven Resolver supports SHA-256 and SHA-512, but its
default remote-external checksum algorithm order is commonly
SHA-1,MD5 unless configured otherwise.
checksumPolicy=fail governs missing or mismatched
expected checksums; direct SHA-256/SHA-512 comparison here remains
independent review evidence, while signature/fingerprint policy is a
separate control.
artifact=trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar
sha256sum "$artifact" > "$artifact.sha256"
sha512sum "$artifact" > "$artifact.sha512"
sha256sum -c "$artifact.sha256"
sha512sum -c "$artifact.sha512"
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy.consumer</groupId>
<artifactId>maven-check</artifactId>
<version>1.0.0</version>
<repositories>
<repository>
<id>academy-internal</id>
<url>file://${project.basedir}/../trusted-repo</url>
<releases>
<enabled>true</enabled>
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.academy.internal</groupId>
<artifactId>policy-lib</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>
If Maven is installed or its trusted Wrapper is available, use an
isolated local repository such as
-Dmaven.repo.local="$PWD/.lab-m2". Do not use the
normal user repository as evidence that a clean consumer can resolve
securely.
11. Optional local signature mechanics with a disposable key
This optional exercise demonstrates detached signatures only. A key generated inside the same lab is not independent proof of publisher identity; it simply lets you practice verification and fingerprint recording.
export GNUPGHOME="$PWD/.lab-gnupg"
mkdir -m 700 "$GNUPGHOME"
gpg --batch --passphrase '' --quick-gen-key \
'Chapter28 Synthetic <chapter28@example.invalid>' ed25519 sign 1d
fingerprint=$(gpg --batch --with-colons --fingerprint 'Chapter28 Synthetic' \
| awk -F: '$1=="fpr" {print $10; exit}')
printf 'synthetic signing fingerprint=%s\n' "$fingerprint" | tee signing-fingerprint.txt
artifact=trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar
gpg --batch --yes --armor --detach-sign "$artifact"
gpg --verify "$artifact.asc" "$artifact"
Production trust would require the expected fingerprint to be anchored through an organization-approved, independent channel. Never commit the disposable private key.
12. Challenge: choose the missing control
A team has a lockfile, immutable dependency versions, and dependency verification, but internal coordinates can still be looked up in Maven Central before the internal repository. What is the narrowest missing control?
Expected reasoning: add repository exclusivity/routing for the internal namespace. A checksum protects bytes only after selection; it does not make an unintended repository ineligible.
13. Cleanup only disposable state
Keep logs only if you want them as study evidence, then remove the lab directory.
cd ..
rm -rf ch28-lab
Never delete normal ~/.gradle, ~/.m2,
global keyrings, or production repository state as a routine
response to a verification failure.
Knowledge check
What should you do immediately after generating verification metadata?
Review the repository/coordinate selection and compare digests/keys against independent trusted evidence before accepting the file.
Why does the checksum mismatch experiment keep the coordinate at 1.0.0?
It models mutable or compromised bytes under an apparently immutable release identity.
Why remove the trusted artifact for the repository-scope test?
It proves Gradle refuses to fall through to the alternate repository for an exclusive group.
Is a disposable self-generated PGP key proof of real publisher provenance?
No. It demonstrates mechanics; production provenance requires independently anchored key ownership/fingerprint trust.
Why isolate Gradle/Maven cache state in the lab?
It prevents warm normal-user caches from hiding repository and verification behavior and makes cleanup safe.
Official references and version notes
- Gradle 9.7.1 release notes — pinned Gradle baseline and current dependency-verification diagnostics.
- Verifying Dependencies — checksums, signatures, trusted keys, strict/lenient/off modes, bootstrapping, reports, and scope.
-
Filtering Repository Content
— repository filters and
exclusiveContentsemantics. -
Gradle Wrapper
—
distributionSha256Sum, Wrapper JAR verification, PGP verification, and upgrade workflow. - Securing Gradle Builds — dependency, repository, cache, CI, and Wrapper attack surfaces.
- Maven release history — Maven 3.9.16 GA baseline; Maven 4 remains pre-GA at this chapter timestamp.
- Maven settings reference — repositories, mirrors, servers, and repository policies.
- Maven Resolver RepositoryPolicy — checksum fail/warn/ignore behavior.
- Apache Maven Wrapper — Wrapper 3.3.4 and SHA-256 verification properties.
- Apache Maven downloads — release checksums/signatures and KEYS verification guidance.
Version-sensitive behavior was rechecked against primary documentation on 2026-08-24. Mandatory labs are local/free and use synthetic artifacts only. No production repository, credential, signing key, shared cache, or hosted CI service is required.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.