Skip to content

Commit ce475db

Browse files
✨ feat: Add sha256 and sha512 checksums to remote mode (#1200)
Co-authored-by: Aman Sharma <[email protected]>
1 parent 9fcb067 commit ce475db

File tree

5 files changed

+129
-27
lines changed

5 files changed

+129
-27
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ mvn -f pom.lockfile.xml
6969
- `includeMavenPlugins` (`-DincludeMavenPlugins=true`) will include the maven plugins in the lockfile. This is useful if you want to validate the Maven plugins as well.
7070
- `allowValidationFailure` (`-DallowValidationFailure=true`, default=false) allow validation failures, printing a warning instead of an error. This is useful if you want to only validate the Maven lockfile, but do not need to fail the build in case the lockfile is not valid. Use with caution, you loose all guarantees.
7171
- `includeEnvironment` (`-DincludeEnvironment=true`) will include the environment metadata in the lockfile. This is useful if you want to have warnings when the environment changes.
72-
- `checksumAlgorithm` (`-DchecksumAlgorithm=sha256`) will set the checksum algorithm used to generate the lockfile. The default depends on your checksum mode.
72+
- `checksumAlgorithm` (`-DchecksumAlgorithm=sha256`) will set the checksum algorithm used to generate the lockfile. If not explicitly provided it will use SHA-256.
7373
- `checksumMode` will set the checksum mode used to generate the lockfile. See [Checksum Modes](/maven_plugin/src/main/java/io/github/chains_project/maven_lockfile/checksum/ChecksumModes.java) for more information.
7474
- `skip` (`-Dskip=true`) will skip the execution of the plugin. This is useful if you would like to disable the plugin for a specific module.
7575
- `lockfileName` (`-DlockfileName=my-lockfile.json` default="lockfile.json") will set the name of the lockfile file to be generated/read.

maven_plugin/src/main/java/io/github/chains_project/maven_lockfile/checksum/ChecksumModes.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
*/
77
public enum ChecksumModes {
88
/**
9-
* Downloads the checksum from the maven repository.
9+
* Downloads the checksum from the maven repository. Supports md5, sha1, sha256 and sha512. If the requested
10+
* checksum is not found in remote repository, the artifact will be downloaded and checksum will be calculated
11+
* on the downloaded artifact. The download will be verified with the sha1 checksum if it available in the remote
12+
* repository.
1013
*/
1114
REMOTE("remote"),
1215
/**
13-
* Calculates the checksum from the downloaded artifact.
16+
* Calculates the checksum from the downloaded artifact in the local m2 folder.
1417
*/
1518
LOCAL("local");
1619

maven_plugin/src/main/java/io/github/chains_project/maven_lockfile/checksum/RemoteChecksumCalculator.java

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package io.github.chains_project.maven_lockfile.checksum;
22

3+
import com.google.common.io.BaseEncoding;
34
import io.github.chains_project.maven_lockfile.data.ResolvedUrl;
45
import java.net.URI;
56
import java.net.http.HttpClient;
67
import java.net.http.HttpRequest;
78
import java.net.http.HttpResponse;
9+
import java.security.MessageDigest;
10+
import java.util.Locale;
811
import java.util.Optional;
912
import org.apache.log4j.Logger;
1013
import org.apache.maven.artifact.Artifact;
@@ -23,8 +26,12 @@ public RemoteChecksumCalculator(
2326
ProjectBuildingRequest artifactBuildingRequest,
2427
ProjectBuildingRequest pluginBuildingRequest) {
2528
super(checksumAlgorithm);
26-
if (!(checksumAlgorithm.equals("sha1") || checksumAlgorithm.equals("md5"))) {
27-
throw new IllegalArgumentException("Invalid checksum algorithm maven central only supports sha1 or md5");
29+
if (!(checksumAlgorithm.equals("md5")
30+
|| checksumAlgorithm.equals("sha1")
31+
|| checksumAlgorithm.equals("sha256")
32+
|| checksumAlgorithm.equals("sha512"))) {
33+
throw new IllegalArgumentException(
34+
"Invalid checksum algorithm maven central only supports md5, sha1, sha256 or sha512.");
2835
}
2936

3037
this.artifactBuildingRequest = artifactBuildingRequest;
@@ -42,21 +49,81 @@ private Optional<String> calculateChecksumInternal(Artifact artifact, ProjectBui
4249
}
4350
String filename = artifactId + "-" + version + "." + extension;
4451

52+
BaseEncoding baseEncoding = BaseEncoding.base16();
53+
HttpClient client = HttpClient.newBuilder()
54+
.followRedirects(HttpClient.Redirect.ALWAYS)
55+
.build();
56+
4557
for (ArtifactRepository repository : buildingRequest.getRemoteRepositories()) {
46-
String url = repository.getUrl().replaceAll("/$", "") + "/" + groupId + "/" + artifactId + "/" + version
47-
+ "/" + filename + "." + checksumAlgorithm;
58+
String artifactUrl = repository.getUrl().replaceAll("/$", "") + "/" + groupId + "/" + artifactId + "/"
59+
+ version + "/" + filename;
60+
String checksumUrl = artifactUrl + "." + checksumAlgorithm;
4861

49-
LOGGER.debug("Checking: " + url);
62+
LOGGER.debug("Checking: " + checksumUrl);
5063

51-
HttpClient client = HttpClient.newBuilder()
52-
.followRedirects(HttpClient.Redirect.ALWAYS)
53-
.build();
54-
HttpRequest request =
55-
HttpRequest.newBuilder().uri(URI.create(url)).build();
56-
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
64+
HttpRequest checksumRequest =
65+
HttpRequest.newBuilder().uri(URI.create(checksumUrl)).build();
66+
HttpResponse<String> checksumResponse =
67+
client.send(checksumRequest, HttpResponse.BodyHandlers.ofString());
5768

58-
if (response.statusCode() >= 200 && response.statusCode() < 300) {
59-
return Optional.of(response.body().strip());
69+
if (checksumResponse.statusCode() >= 200 && checksumResponse.statusCode() < 300) {
70+
return Optional.of(checksumResponse.body().strip());
71+
}
72+
73+
if (checksumResponse.statusCode() == 404) {
74+
HttpRequest artifactRequest = HttpRequest.newBuilder()
75+
.uri(URI.create(artifactUrl))
76+
.build();
77+
HttpResponse<byte[]> artifactResponse =
78+
client.send(artifactRequest, HttpResponse.BodyHandlers.ofByteArray());
79+
80+
if (artifactResponse.statusCode() < 200 || artifactResponse.statusCode() >= 300) {
81+
continue;
82+
}
83+
84+
LOGGER.info("Unable to find " + checksumAlgorithm + " checksum for " + artifact.getGroupId() + ":"
85+
+ artifactId + ":" + version + " on remote. Downloading and calculating locally.");
86+
87+
// Fallback to and verify downloaded artifact with sha1
88+
HttpRequest artifactVerificationRequest = HttpRequest.newBuilder()
89+
.uri(URI.create(artifactUrl + ".sha1"))
90+
.build();
91+
HttpResponse<String> artifactVerificationResponse =
92+
client.send(artifactVerificationRequest, HttpResponse.BodyHandlers.ofString());
93+
94+
// Extract first part of string to handle sha1sum format, `hash_in_hex /path/to/file`.
95+
// For example provided by:
96+
// https://repo.maven.apache.org/maven2/com/martiansoftware/jsap/2.1/jsap-2.1.jar.sha1
97+
// https://repo.maven.apache.org/maven2/javax/inject/javax.inject/1/javax.inject-1.jar.sha1
98+
String artifactVerification =
99+
artifactVerificationResponse.body().strip();
100+
int spaceIndex = artifactVerification.indexOf(" ");
101+
artifactVerification =
102+
spaceIndex == -1 ? artifactVerification : artifactVerification.substring(0, spaceIndex);
103+
104+
if (artifactVerificationResponse.statusCode() >= 200
105+
&& artifactVerificationResponse.statusCode() < 300) {
106+
MessageDigest verificationMessageDigest = MessageDigest.getInstance("sha1");
107+
String sha1 = baseEncoding
108+
.encode(verificationMessageDigest.digest(artifactResponse.body()))
109+
.toLowerCase(Locale.ROOT);
110+
111+
if (!sha1.equals(artifactVerification)) {
112+
LOGGER.error("Invalid sha1 checksum for: " + artifactUrl);
113+
throw new RuntimeException("Invalid sha1 checksum for '" + artifact.getGroupId() + ":"
114+
+ artifactId + ":" + version + "'. Checksum found at '" + artifactUrl
115+
+ ".sha1' does not match calculated checksum of downloaded file. Remote checksum = '"
116+
+ artifactVerification + "'. Locally calculated checksum = '" + sha1 + "'.");
117+
}
118+
} else {
119+
LOGGER.warn("Unable to find sha1 to verify download of: " + artifactUrl);
120+
}
121+
122+
MessageDigest messageDigest = MessageDigest.getInstance(checksumAlgorithm);
123+
String checksum = baseEncoding
124+
.encode(messageDigest.digest(artifactResponse.body()))
125+
.toLowerCase(Locale.ROOT);
126+
return Optional.of(checksum);
60127
}
61128
}
62129

@@ -80,15 +147,16 @@ private Optional<ResolvedUrl> getResolvedFieldInternal(Artifact artifact, Projec
80147
}
81148
String filename = artifactId + "-" + version + "." + extension;
82149

150+
HttpClient client = HttpClient.newBuilder()
151+
.followRedirects(HttpClient.Redirect.ALWAYS)
152+
.build();
153+
83154
for (ArtifactRepository repository : buildingRequest.getRemoteRepositories()) {
84155
String url = repository.getUrl().replaceAll("/$", "") + "/" + groupId + "/" + artifactId + "/" + version
85156
+ "/" + filename;
86157

87158
LOGGER.debug("Checking: " + url);
88159

89-
HttpClient client = HttpClient.newBuilder()
90-
.followRedirects(HttpClient.Redirect.ALWAYS)
91-
.build();
92160
HttpRequest request = HttpRequest.newBuilder()
93161
.uri(URI.create(url))
94162
.method("HEAD", HttpRequest.BodyPublishers.noBody())
@@ -121,7 +189,7 @@ public String calculatePluginChecksum(Artifact artifact) {
121189

122190
@Override
123191
public String getDefaultChecksumAlgorithm() {
124-
return "sha1";
192+
return "SHA-256";
125193
}
126194

127195
@Override

maven_plugin/src/test/java/it/IntegrationTestsIT.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,10 +384,41 @@ public void remoteRepositoryShouldResolve(MavenExecutionResult result) throws Ex
384384

385385
@MavenTest
386386
public void checksumModeRemote(MavenExecutionResult result) throws Exception {
387-
// contract: if checksum mode is remote, maven_lockfile should be able to download .
387+
// contract: if checksum mode is remote, maven-lockfile should be able to download and verify sha256 from maven
388+
// central and if sha256 is not available, it should be able to .
388389
assertThat(result).isSuccessful();
389-
var lockfileExists = fileExists(result, "lockfile.json");
390-
assertThat(lockfileExists).isTrue();
390+
var lockfilePath = findFile(result, "lockfile.json");
391+
assertThat(lockfilePath).exists();
392+
var lockfile = LockFile.readLockFile(lockfilePath);
393+
394+
// Verify: atlassian-bandana:0.2.0 is hosted on packages.atlassian.com which doesn't provide sha256, sha256 has
395+
// to be calculated
396+
var dep1Checksum = lockfile.getDependencies().stream()
397+
.filter(dependency -> dependency
398+
.getChecksum()
399+
.equals("12357e6d5c5eb6b5ed80bbb98f4ef7b70fcb08520a9f306c4af086c37d6ebc11"))
400+
.findAny();
401+
assertThat(dep1Checksum).isNotNull();
402+
result.getMavenLog();
403+
404+
// Verify: jsap:2.1 is hosted on repo.maven.apache.org which doesn't provide sha256, and who's sha1 has a
405+
// different format (providing `checksum path` instead of `checksum`). Sha1 should still succeed as the
406+
// `checksum` is verified aganist up until the first space, thus excluding the path of the file when the
407+
// sha1 was generated. Sha256 has to be calculated.
408+
var dep2Checksum = lockfile.getDependencies().stream()
409+
.filter(dependency -> dependency
410+
.getChecksum()
411+
.equals("331746fa62cfbc3368260c5a2e660936ad11be612308c120a044e120361d474e"))
412+
.findAny();
413+
assertThat(dep2Checksum).isNotNull();
414+
415+
// Verify: spoon-core:11.1.0 is hosted on maven central and directly provides sha256 checksums
416+
var dep3Checksum = lockfile.getDependencies().stream()
417+
.filter(dependency -> dependency
418+
.getChecksum()
419+
.equals("a8ae41ae0a1578a7ef9ce4f8d562813a99e6cc015e8cb3b0482b5470d53f1c6b"))
420+
.findAny();
421+
assertThat(dep3Checksum).isNotNull();
391422
}
392423

393424
@MavenTest
@@ -397,7 +428,7 @@ public void resolvedFieldShouldResolve(MavenExecutionResult result) throws Excep
397428
Path lockFilePath = findFile(result, "lockfile.json");
398429
assertThat(lockFilePath).exists();
399430
var lockFile = LockFile.readLockFile(lockFilePath);
400-
var atlassinResolved = lockFile.getDependencies().stream()
431+
var atlassianResolved = lockFile.getDependencies().stream()
401432
.filter(
402433
dependency -> dependency
403434
.getResolved()
@@ -413,7 +444,7 @@ public void resolvedFieldShouldResolve(MavenExecutionResult result) throws Excep
413444
ResolvedUrl.of(
414445
"https://repo.maven.apache.org/maven2/fr/inria/gforge/spoon/spoon-core/10.3.0/spoon-core-10.3.0.jar")))
415446
.findAny();
416-
assertThat(atlassinResolved).isNotNull();
447+
assertThat(atlassianResolved).isNotNull();
417448
assertThat(mavenCentralResolved).isNotNull();
418449
}
419450
}

maven_plugin/src/test/resources-its/it/IntegrationTestsIT/checksumModeRemote/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<dependency>
3030
<groupId>fr.inria.gforge.spoon</groupId>
3131
<artifactId>spoon-core</artifactId>
32-
<version>10.3.0</version>
32+
<version>11.1.0</version>
3333
</dependency>
3434
<dependency>
3535
<groupId>atlassian-bandana</groupId>
@@ -56,7 +56,7 @@
5656
</executions>
5757
<configuration>
5858
<checksumMode>remote</checksumMode>
59-
<checksumAlgorithm>sha1</checksumAlgorithm>
59+
<checksumAlgorithm>sha256</checksumAlgorithm>
6060
</configuration>
6161
</plugin>
6262
</plugins>

0 commit comments

Comments
 (0)