Chapter 28Lesson 05~380 minutes

Checkpoint Lab — Dependency Verification, Checksums, Signatures, Repository Content Filtering, and Supply-Chain Security

Create one synthetic Maven/Gradle supply-chain matrix, prove an integrity failure and a repository-scope failure with local artifacts, record exact wrapper/dependency/plugin identities, and finish with an incident-ready verification checklist for CI.

CheckpointMaven + GradleChecksum mismatchRepository scopeControl matrix

Learning objectives

  • Build a documented Maven/Gradle control matrix for dependency, repository, plugin, and Wrapper trust.
  • Record exact dependency/plugin/Wrapper identities and independent checksum evidence.
  • Demonstrate one harmless integrity failure and one repository-scope failure using local artifacts.
  • Prove repair in strict, isolated state without disabling verification or deleting normal caches.
  • Produce an incident-ready verification checklist and bridge to CI/CD enforcement in Chapter 29.

Checkpoint boundary. Everything is synthetic and project-local. The checkpoint must never use production credentials, production publish targets, normal user caches, or a shared signing key.

1. Acceptance contract

The checkpoint passes only if you can show both what should resolve and what must fail. A green build alone is insufficient.

Invariant Required evidence
Build-tool identity Gradle Wrapper version/URL/distribution SHA-256 + Wrapper JAR independent verification record; Maven Wrapper identity if used.
Dependency identity Exact coordinate dev.academy.internal:policy-lib:1.0.0 + trusted JAR SHA-256.
Repository ownership Internal group is exclusive to trusted-repo; alternate repo cannot satisfy it.
Gradle verification Reviewed gradle/verification-metadata.xml; strict build passes for trusted bytes.
Integrity incident Same-coordinate changed JAR fails strict verification; expected/actual digests preserved.
Maven lane Checksum policy and independent checksum/signature evidence are documented without claiming a Gradle-equivalent core ledger.
Cleanup Only disposable lab/cache/key state is removed.

2. Setup and preflight

Create the lab and record the environment before any mutation.

set -euo pipefail
mkdir ch28-checkpoint
cd ch28-checkpoint
export GRADLE_USER_HOME="$PWD/.gradle-user-home"

java -version 2>&1 | tee environment-java.txt
javac -version | tee environment-javac.txt
./gradlew --version | tee environment-gradle.txt
cat gradle/wrapper/gradle-wrapper.properties | tee environment-gradle-wrapper.txt
sha256sum gradle/wrapper/gradle-wrapper.jar | tee environment-wrapper-jar.actual.sha256

# Optional Maven lane if the trusted wrapper is already available:
# ./mvnw --version | tee environment-maven.txt

3. Predict before execution

Write the predictions before running the experiment:

P1. With trusted-repo present, dev.academy.internal:policy-lib:1.0.0 resolves only there.
P2. After verification metadata is accepted, unchanged trusted bytes pass strict verification.
P3. Replacing those bytes under the same coordinate produces a non-zero verification failure.
P4. If trusted-repo cannot serve the coordinate, alternate-repo still cannot satisfy the exclusive internal group.
P5. Restoring trusted bytes and repository state returns the exact original SHA-256 and a green strict build.

4. Create the control matrix

Keep this table with the checkpoint evidence and mark each row PASS/FAIL after execution.

Boundary Gradle control Maven control / boundary Evidence
Version identity Pinned coordinate + prior Chapter 20 governance. Pinned dependency/plugin versions / dependencyManagement policy. Dependency report/tree.
Repository origin Settings repositories + exclusiveContent. Mirrors/repositories + repository-manager routing when strict namespace ownership is required. Repository config + scope-failure test.
Artifact integrity verification-metadata.xml SHA-256, strict mode. Repository checksum policy + independent SHA-256/SHA-512 verification. Expected/actual digest.
Authenticity Trusted PGP keys + signatures when independently anchored. External GPG/repository-manager/tooling verification where required. Fingerprint + signature result.
Build tool Distribution SHA-256 + Wrapper JAR checksum/PGP. Wrapper 3.3.4 distribution/JAR SHA-256 properties as applicable. Wrapper files + independent release digest.
Plugins Pinned plugin IDs/versions + plugin repositories + dependency verification. Pinned build plugins/extensions + governed plugin repositories. Plugin declarations/resolution evidence.
Incident response Strict failure + verification report + isolated rerun. Resolver logs/effective settings + isolated local repo + manager audit. Preserved incident bundle.

5. Create the synthetic repositories and consumer

Use the exact Lesson 2 fixture so the test is reproducible.

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
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(); }
}

6. Establish and independently record the baseline

Bootstrap SHA-256 metadata, then compare the generated artifact digest to the independently calculated trusted repository digest before treating the metadata as accepted.

./gradlew --write-verification-metadata sha256 clean compileJava \
  | tee baseline-bootstrap.log
cp gradle/verification-metadata.xml baseline-verification-metadata.xml
trusted_sha=$(cut -d' ' -f1 trusted-artifact.sha256)
printf 'trusted_sha=%s\n' "$trusted_sha" | tee baseline-accepted.sha256

grep -n 'policy-lib-1.0.0.jar' gradle/verification-metadata.xml
./gradlew --dependency-verification strict clean compileJava \
  | tee baseline-strict.log

7. Integrity failure: mutate bytes, not coordinates

Keep the original artifact, replace it with a deterministic changed JAR, then force re-resolution.

artifact=trusted-repo/dev/academy/internal/policy-lib/1.0.0/policy-lib-1.0.0.jar
cp "$artifact" original-policy-lib-1.0.0.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 "$artifact" -C fixture-classes .
sha256sum "$artifact" | tee incident-actual.sha256

set +e
./gradlew --refresh-dependencies --dependency-verification strict clean compileJava \
  > incident-integrity.log 2>&1
integrity_rc=$?
set -e
printf 'integrity_rc=%s\n' "$integrity_rc" | tee incident-integrity.rc
test "$integrity_rc" -ne 0

Do not regenerate verification metadata here. That would convert the incident into policy.

8. Repair integrity and prove exact identity

Restore the reviewed bytes and verify both SHA-256 and strict build behavior.

cp original-policy-lib-1.0.0.jar "$artifact"
sha256sum -c trusted-artifact.sha256
./gradlew --refresh-dependencies --dependency-verification strict clean compileJava \
  | tee repaired-integrity.log

9. Repository-scope failure: make trusted origin unavailable

The alternate repository still contains the same coordinate, but exclusivity must make it ineligible.

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 > incident-scope.log 2>&1
scope_rc=$?
set -e
printf 'scope_rc=%s\n' "$scope_rc" | tee incident-scope.rc
test "$scope_rc" -ne 0

grep -Ei 'policy-lib|could not find|resolve' incident-scope.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 repaired-scope.log

10. Add Maven evidence without overstating Maven core guarantees

Produce repository sidecar digests and an optional isolated Maven consumer. checksumPolicy=fail is useful transport-integrity behavior, while Maven Resolver's default remote-external algorithm order is commonly SHA-1,MD5 unless configured otherwise; independently recorded SHA-256/SHA-512/fingerprint evidence remains a separate review artifact.

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" | tee maven-sha256-check.txt
sha512sum -c "$artifact.sha512" | tee maven-sha512-check.txt

# Optional when trusted Maven Wrapper is available:
# mkdir -p maven-consumer && cat > maven-consumer/pom.xml <<'POM'
# ... use the POM shown in Lesson 2 ...
# POM
# ./mvnw -f maven-consumer/pom.xml -Dmaven.repo.local="$PWD/.maven-user-repo" dependency:tree
<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>

11. Complete the wrapper identity record

Compare the checked-in Wrapper JAR and configured distribution checksum to independently published Gradle data. If Maven Wrapper is used, record distributionSha256Sum and applicable wrapperSha256Sum from a separately verified Apache source.

Gradle wrapper record
  requested_version: 9.7.1
  distribution_url: services.gradle.org/distributions/gradle-9.7.1-bin.zip
  distribution_sha256: independently verified official value
  wrapper_jar_sha256: independently verified official value
  reviewed_by: <name/change record>

Maven wrapper record (if used)
  wrapper_version: 3.3.4
  maven_version: 3.9.16
  distribution_sha256: independently verified Apache value
  wrapper_jar_sha256: only when wrapper JAR form is present
  reviewed_by: <name/change record>

12. Verification checklist

Evidence Pass condition
Exact identities Gradle/Maven/JDK/Wrapper/dependency/plugin versions are recorded; no dynamic selectors in production path.
Trusted artifact digest Trusted JAR digest equals baseline accepted SHA-256.
Strict verification Unchanged artifact passes; changed same-coordinate artifact fails.
Repository scope Alternate repository cannot satisfy dev.academy.internal when trusted repo is unavailable.
Wrapper trust Expected Wrapper/distribution digests come from independent release data, not the checkout itself.
Maven boundary Checksum policy and independent checksum/signature evidence are documented accurately.
No bypass No accepted fix disables verification, ignores checksum errors, broadens repositories, or auto-accepts mismatched bytes.
Cleanup No normal home caches, global keyrings, real credentials, or production repositories were mutated.

13. Minimal incident handoff template

If either negative test happened unexpectedly in a real build, preserve this record before remediation:

incident_id:
first_seen_utc:
coordinate_or_plugin:
expected_version:
expected_digest_or_key:
actual_digest_or_key:
repository_name_and_url:
wrapper_version_and_digest:
jdk_version:
verification_mode:
cache_scope:
credentials_exposed_or_suspected:
containment_action:
independent_source_used_for_revalidation:
clean_room_rebuild_result:
release_artifacts_potentially_affected:

14. Cleanup and rollback

After retaining the study evidence you want, delete only the checkpoint directory. If you created a disposable GnuPG home, it is inside that directory and disappears with it.

cd ..
rm -rf ch28-checkpoint

15. What Chapter 28 adds—and the bridge to Chapter 29

You now have a trust model that survives beyond a developer workstation: exact wrapper identity, scoped repositories, immutable coordinates, checksum/signature verification, plugin trust, and evidence-preserving incident handling. Chapter 29 takes those invariants into CI/CD: ephemeral agents, cache policy, test sharding, artifact promotion, and build-once/promote-the-same-bytes workflows.

Knowledge check

Why is the integrity failure required to keep the same version?

What exactly proves the repository-scope control?

What should never be the source of the expected Wrapper checksum?

Why does the Maven lane record independent digests in addition to checksumPolicy?

What is the Chapter 29 bridge?

Official references and version notes

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.