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:
- Download the
maven-wrapper.jar
andapache-maven-3.9.6-bin.zip
- Download and verify their detached signatures
- Calculate their SHA-256 digests
- Execute
mvn wrapper:wrapper -DwrapperSha256Sum=<digest> -DdistributionSha256Sum=<digest>
More information on the Maven wrapper plugin can be found here: https://maven.apache.org/wrapper/.