Relentless Coding

A Developer’s Blog

Maven Wrapper Integrity Validation

Maven is modular. It uses plugins to achieve its goals. These plugins are downloaded when they are invoked.

For example, executing mvn wrapper:wrapper will make Maven first look in your local repository (~/.m2/repository), and, if it cannot find it, download the wrapper plugin from the repository defined in your settings.xml. (If no custom repository is defined, the default Maven “central” repository will be used.)

How does mvn know what to download, though, based only on a prefix (wrapper:)? Because the wrapper plugin used the Maven prefix rules. Maven looked in the current project, and not finding what it was looking for downloaded https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-metadata.xml. Now it found this:

<!-- ... snip... -->
<plugin>
  <name>Apache Maven Wrapper Plugin</name>
  <prefix>wrapper</prefix>
  <artifactId>maven-wrapper-plugin</artifactId>
</plugin>
<!-- ... snip... -->

Maven now knows it can download this plugin from $REPO/org/apache/maven/plugins/maven-wrapper-plugin/$LATEST_VERSION/maven-wrapper-plugin-$LATEST_VERSION.jar.

Recently, the wrapper plugin added SHA-256 checksums to verify the integrity of maven-wrapper.jar and the Maven distribution.

These SHA-256 sums are not easily discovered, however. I could not find a web source that listed them. Fortunately, signatures are uploaded together with the source code. The public signing keys can be found elsewhere.

So, we first download all the Apache Maven certificates (“keys”) and sign them locally to make them valid:

$ curl -o apache-keys.txt https://downloads.apache.org/maven/KEYS
$ gpg --import < apache-keys.txt
$ while IFS=: read -r type validity key_len pubkey_algo key_id rest; do
>   if [[ $type == pub ]] && ! [[ $validity =~ ^(e|r)$ ]]; then
>       gpg --quick-lsign-key "$key_id"
>   fi
> done < <(gpg --show-keys --with-colons apache-keys.txt 2>/dev/null)

(See for an explanation of the meaning of GnuPG’s colon-delimited fields https://github.com/gpg/gnupg/blob/master/doc/DETAILS.)

We’ve now locally certified all the imported Apache Maven certificates. (This basically means you’ve attested that the UIDs in them belong to the provided public keys. “Locally” just means they are not going to be part of any export of the certificates, for example when uploading to a key server.) GPG has set the validity of these certificates to “full”. We now accept signatures on files and other data from these certificates.

We download the file manually and verify it using the detached signature (file ending in .asc for “ASCII-armored binary”):

$ gpg --verify maven-wrapper-3.2.0.jar.asc maven-wrapper-3.2.0.jar
gpg: Signature made Thu Mar  9 00:07:07 2023 CET
gpg:                using RSA key 84789D24DF77A32433CE1F079EB80E92EB2135B1
gpg:                issuer "sjaranowski@apache.org"
gpg: Good signature from "Slawomir Jaranowski <sjaranowski@apache.org>" [full]
gpg:                 aka "Slawomir Jaranowski <s.jaranowski@gmail.com>" [full]

Now that we have some confidence that we downloaded a legitimate copy of the maven-wrapper.jar file, we can calculate its SHA-256 sum.

The distributionSha256Sum should be the SHA-256 checksum of the downloaded distribution, i.e. the actual Maven tool. According to the documentation, we can find the archives here: https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip. Again, we download the file manually, let GPG verify it and calculate the SHA-256 digest.

Now we’re ready to kick off the Maven wrapper plugin:

$ mvn wrapper:wrapper \
>   -DwrapperSha256Sum=e63a53cfb9c4d291ebe3c2b0edacb7622bbc480326beaa5a0456e412f52f066a \
>   -DdistributionSha256Sum=f00af914c785c9faed661f223000a92d1de9553f5c82d3b4362e66d9c031625f

This downloads the wrapper plugin which in its turn downloads the wrapper distribution (mvnw executable and .mvn directory containing the wrapper configuration). The configuration file now looks like:

$ sed '/^#/d' .mvn/wrapper/maven-wrapper.properties
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
distributionSha256Sum=f00af914c785c9faed661f223000a92d1de9553f5c82d3b4362e66d9c031625f
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
wrapperSha256Sum=f00a53cfb9c4d291ebe3c2b0edacb7622bbc480326beaa5a0456e412f52f066a

What is protected by the checksums is not the execution of the mvnw shell script, but the execution of what that script executes: maven-wrapper.jar. When executed, this maven-wrapper.jar downloads the actual Maven distribution (mvn) if necessary and verifies the downloaded file against the distributionSha256Sum in the config file. Now, if the wrapper SHA-256 sum is wrong, the shell script will throw an error. And if the just-downloaded distribution’s SHA-256 sum is wrong, the maven-wrapper.jar will throw an error.

So, updating the Maven wrapper comes down to:

  1. Download the maven-wrapper.jar and apache-maven-3.9.6-bin.zip
  2. Download and verify their detached signatures
  3. Calculate their SHA-256 digests
  4. Execute mvn wrapper:wrapper -DwrapperSha256Sum=<digest> -DdistributionSha256Sum=<digest>

More information on the Maven wrapper plugin can be found here: https://maven.apache.org/wrapper/.