You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2026/08/31 11:07:11 UTC
(maven-resolver) branch master updated: fix: deploy-side integrity must fail closed (#2084)
This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new c82abf7eb fix: deploy-side integrity must fail closed (#2084)
c82abf7eb is described below
commit c82abf7ebcf2ee34d89b1927158787f87647f6d2
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Aug 31 13:07:04 2026 +0200
fix: deploy-side integrity must fail closed (#2084)
## Summary
Fixes 4 findings from the maven-resolver security audit (scan-maven-resolver-20260811):
| Finding | Severity | Description |
|---------|----------|-------------|
| f022 | LOW | Checksum upload failures swallowed; deploy succeeds without published checksums |
| f023 | LOW | Provided/trusted checksums never cover metadata downloads |
| f029 | LOW | First matching checksum algorithm accepts, skipping stronger provided checksums |
| f030 | LOW | One pre-existing `.asc` silently disables signing of all deploy artifacts |
**Root cause:** On the publication and strongest-verification paths, failures degrade silently: checksums that fail to upload are logged and forgotten, one pre-existing signature disables signing wholesale, and trusted-checksums has structural coverage gaps.
**Fix:** Deploy must be atomic across artifact bytes and integrity metadata: propagate failures, cover metadata, compare all checksums, per-artifact signature skip.
---
.../connector/basic/BasicRepositoryConnector.java | 50 +++++++--
.../BasicRepositoryConnectorConfigurationKeys.java | 18 +++
.../aether/connector/basic/ChecksumValidator.java | 18 ++-
.../connector/basic/ChecksumValidatorTest.java | 48 ++++++++
.../gnupg/GnupgSignatureArtifactGenerator.java | 66 ++++++++---
.../generator/gnupg/GpgSignerFactoryTest.java | 26 +++++
.../SigstoreSignatureArtifactGenerator.java | 121 +++++++++++++--------
.../FileTrustedChecksumsSourceSupport.java | 37 +++++++
.../SparseDirectoryTrustedChecksumsSource.java | 77 ++++++++++++-
.../SummaryFileTrustedChecksumsSource.java | 75 +++++++++++--
.../TrustedToProvidedChecksumsSourceAdapter.java | 31 ++++++
.../FileTrustedChecksumsSourceTestSupport.java | 53 +++++++++
...rustedToProvidedChecksumsSourceAdapterTest.java | 52 +++++++++
.../spi/checksums/ProvidedChecksumsSource.java | 27 +++++
.../spi/checksums/TrustedChecksumsSource.java | 45 ++++++++
15 files changed, 657 insertions(+), 87 deletions(-)
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
index 399d363ab..0cb2028c4 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
@@ -69,11 +69,13 @@ import org.slf4j.LoggerFactory;
import static java.util.Objects.requireNonNull;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_DOWNSTREAM_THREADS;
+import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_FAIL_ON_CHECKSUM_UPLOAD_FAILURE;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_INCLUDED_CHECKSUMS;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_PARALLEL_PUT;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_PERSISTED_CHECKSUMS;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_THREADS;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_UPSTREAM_THREADS;
+import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_FAIL_ON_CHECKSUM_UPLOAD_FAILURE;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_INCLUDED_CHECKSUMS;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_PARALLEL_PUT;
import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_PERSISTED_CHECKSUMS;
@@ -111,6 +113,8 @@ final class BasicRepositoryConnector implements RepositoryConnector {
private final boolean persistedChecksums;
+ private final boolean failOnChecksumUploadFailure;
+
private final ConcurrentHashMap<Boolean, SmartExecutor> executors;
private final AtomicBoolean closed;
@@ -170,6 +174,11 @@ final class BasicRepositoryConnector implements RepositoryConnector {
CONFIG_PROP_PARALLEL_PUT);
persistedChecksums =
ConfigUtils.getBoolean(session, DEFAULT_PERSISTED_CHECKSUMS, CONFIG_PROP_PERSISTED_CHECKSUMS);
+ failOnChecksumUploadFailure = ConfigUtils.getBoolean(
+ session,
+ DEFAULT_FAIL_ON_CHECKSUM_UPLOAD_FAILURE,
+ CONFIG_PROP_FAIL_ON_CHECKSUM_UPLOAD_FAILURE + "." + repository.getId(),
+ CONFIG_PROP_FAIL_ON_CHECKSUM_UPLOAD_FAILURE);
}
/**
@@ -221,6 +230,17 @@ final class BasicRepositoryConnector implements RepositoryConnector {
boolean first = true;
for (MetadataDownload transfer : safeMetadataDownloads) {
+ Map<String, String> providedChecksums = Collections.emptyMap();
+ for (ProvidedChecksumsSource providedChecksumsSource : providedChecksumsSources.values()) {
+ Map<String, String> provided = providedChecksumsSource.getProvidedMetadataChecksums(
+ session, transfer, repository, checksumAlgorithmFactories);
+
+ if (provided != null) {
+ providedChecksums = provided;
+ break;
+ }
+ }
+
URI location = layout.getLocation(transfer.getMetadata(), false);
TransferResource resource = newTransferResource(location, transfer);
@@ -239,7 +259,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
checksumPolicy,
checksumAlgorithmFactories,
checksumLocations,
- null,
+ providedChecksums,
listener);
if (executor == null || first) {
task.run();
@@ -583,43 +603,49 @@ final class BasicRepositoryConnector implements RepositoryConnector {
* @param path source
* @param bytes transformed data from file or {@code null}
*/
- private void uploadChecksums(Path path, byte[] bytes) {
+ private void uploadChecksums(Path path, byte[] bytes) throws Exception {
if (checksumLocations.isEmpty()) {
return;
}
+ Map<String, String> sumsByAlgo;
try {
ArrayList<ChecksumAlgorithmFactory> algorithms = new ArrayList<>();
for (RepositoryLayout.ChecksumLocation checksumLocation : checksumLocations) {
algorithms.add(checksumLocation.getChecksumAlgorithmFactory());
}
- Map<String, String> sumsByAlgo;
if (bytes != null) {
sumsByAlgo = ChecksumAlgorithmHelper.calculate(bytes, algorithms);
} else {
sumsByAlgo = ChecksumAlgorithmHelper.calculate(path, algorithms);
}
-
- for (RepositoryLayout.ChecksumLocation checksumLocation : checksumLocations) {
- uploadChecksum(
- checksumLocation.getLocation(),
- sumsByAlgo.get(checksumLocation
- .getChecksumAlgorithmFactory()
- .getName()));
- }
} catch (IOException e) {
LOGGER.warn("Failed to upload checksums for {}", file, e);
throw new UncheckedIOException(e);
}
+
+ for (RepositoryLayout.ChecksumLocation checksumLocation : checksumLocations) {
+ uploadChecksum(
+ checksumLocation.getLocation(),
+ sumsByAlgo.get(
+ checksumLocation.getChecksumAlgorithmFactory().getName()));
+ }
}
- private void uploadChecksum(URI location, Object checksum) {
+ private void uploadChecksum(URI location, Object checksum) throws Exception {
try {
if (checksum instanceof Exception) {
throw (Exception) checksum;
}
transporter.put(new PutTask(location).setDataString((String) checksum));
} catch (Exception e) {
+ if (failOnChecksumUploadFailure) {
+ // Fail-closed (default): a deploy must be atomic across artifact bytes and integrity
+ // metadata. Swallowing this failure would report a successful deploy that published the
+ // artifact without checksums for consumers to verify.
+ LOGGER.error("Failed to upload checksum to {}", location, e);
+ throw e;
+ }
LOGGER.warn("Failed to upload checksum to {}", location, e);
}
}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorConfigurationKeys.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorConfigurationKeys.java
index 154595ced..23a6c761c 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorConfigurationKeys.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorConfigurationKeys.java
@@ -117,4 +117,22 @@ public final class BasicRepositoryConnectorConfigurationKeys {
public static final String CONFIG_PROP_INCLUDED_CHECKSUMS = CONFIG_PROPS_PREFIX + "includedChecksums";
public static final boolean DEFAULT_INCLUDED_CHECKSUMS = true;
+
+ /**
+ * Flag indicating whether a failed checksum upload should fail the whole PUT transfer. When enabled
+ * (default), a deploy is atomic across the artifact bytes and their integrity metadata: if a checksum
+ * cannot be calculated or its upload fails, the transfer fails, instead of the remote repository silently
+ * ending up with an artifact published without checksums for consumers to verify. When disabled, checksum
+ * upload failures are only logged at WARN level (legacy, fail-open behavior).
+ *
+ * @since 2.0.23
+ * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
+ * @configurationType {@link java.lang.Boolean}
+ * @configurationDefaultValue {@link #DEFAULT_FAIL_ON_CHECKSUM_UPLOAD_FAILURE}
+ * @configurationRepoIdSuffix Yes
+ */
+ public static final String CONFIG_PROP_FAIL_ON_CHECKSUM_UPLOAD_FAILURE =
+ CONFIG_PROPS_PREFIX + "failOnChecksumUploadFailure";
+
+ public static final boolean DEFAULT_FAIL_ON_CHECKSUM_UPLOAD_FAILURE = true;
}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
index 39b0cc3ee..7ab4dc67b 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
@@ -120,6 +120,8 @@ final class ChecksumValidator {
private boolean validateChecksums(Map<String, ?> actualChecksums, ChecksumKind kind, Map<String, ?> checksums)
throws ChecksumFailureException {
+ boolean accepted = false;
+ boolean rejected = false;
for (Map.Entry<String, ?> entry : checksums.entrySet()) {
String algo = entry.getKey();
Object calculated = actualChecksums.get(algo);
@@ -139,18 +141,24 @@ final class ChecksumValidator {
checksumExpectedValues.put(getChecksumPath(checksumAlgorithmFactory), expected);
if (!isEqualChecksum(expected, actual)) {
+ // ALL comparable checksums of this kind must match: a mismatch on any algorithm rejects the
+ // whole kind, so a match on a weaker algorithm (map iteration order is not strength-ordered)
+ // cannot mask a recorded mismatch on a stronger one. The policy decides whether the mismatch
+ // fails the transfer immediately (default) or is merely recorded.
+ rejected = true;
checksumPolicy.onChecksumMismatch(
checksumAlgorithmFactory.getName(),
kind,
ChecksumFailureException.mismatch(expected, kind.name(), actual));
} else if (checksumPolicy.onChecksumMatch(checksumAlgorithmFactory.getName(), kind)) {
- return true;
+ accepted = true;
}
}
- return false;
+ return accepted && !rejected;
}
private boolean validateExternalChecksums(Map<String, ?> actualChecksums) throws ChecksumFailureException {
+ boolean rejected = false;
for (ChecksumLocation checksumLocation : checksumLocations) {
ChecksumAlgorithmFactory factory = checksumLocation.getChecksumAlgorithmFactory();
Object calculated = actualChecksums.get(factory.getName());
@@ -182,11 +190,15 @@ final class ChecksumValidator {
checksumExpectedValues.put(checksumFile, expected);
if (!isEqualChecksum(expected, actual)) {
+ rejected = true;
checksumPolicy.onChecksumMismatch(
factory.getName(),
ChecksumKind.REMOTE_EXTERNAL,
ChecksumFailureException.mismatch(expected, ChecksumKind.REMOTE_EXTERNAL.name(), actual));
- } else if (checksumPolicy.onChecksumMatch(factory.getName(), ChecksumKind.REMOTE_EXTERNAL)) {
+ } else if (checksumPolicy.onChecksumMatch(factory.getName(), ChecksumKind.REMOTE_EXTERNAL)
+ && !rejected) {
+ // a fetched checksum that mismatched earlier already rejected this download: a later
+ // (possibly weaker) matching algorithm must not accept it
return true;
}
} catch (IOException e) {
diff --git a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
index ce6748c79..ad18ca861 100644
--- a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
+++ b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
@@ -254,6 +254,54 @@ public class ChecksumValidatorTest {
policy.assertCallbacks("mismatch(SHA-1, REMOTE_EXTERNAL)");
}
+ @Test
+ void testValidate_ProvidedAllComparableAlgorithmsMustMatch() {
+ // the weaker algorithm matches, the stronger one does not: the artifact must be rejected regardless
+ // of map iteration order (previously the first matching algorithm accepted the artifact and skipped
+ // the remaining, stronger checksums)
+ Map<String, String> provided = new LinkedHashMap<>();
+ provided.put(MD5, "bar");
+ provided.put(SHA1, "foo");
+ ChecksumValidator validator = newValidator(provided, SHA1, MD5);
+ try {
+ validator.validate(checksums(SHA1, "not-foo", MD5, "bar"), null);
+ fail("expected exception");
+ } catch (ChecksumFailureException e) {
+ assertEquals("foo", e.getExpected());
+ assertEquals(ChecksumKind.PROVIDED.name(), e.getExpectedKind());
+ assertEquals("not-foo", e.getActual());
+ assertTrue(e.isRetryWorthy());
+ }
+ fetcher.assertFetchedFiles();
+ policy.assertCallbacks("match(MD5, PROVIDED)", "mismatch(SHA-1, PROVIDED)");
+ }
+
+ @Test
+ void testValidate_ProvidedAllMatchingAccepts() throws Exception {
+ Map<String, String> provided = new LinkedHashMap<>();
+ provided.put(SHA1, "foo");
+ provided.put(MD5, "bar");
+ ChecksumValidator validator = newValidator(provided, SHA1, MD5);
+ validator.validate(checksums(SHA1, "foo", MD5, "bar"), null);
+ fetcher.assertFetchedFiles();
+ policy.assertCallbacks("match(SHA-1, PROVIDED)", "match(MD5, PROVIDED)");
+ }
+
+ @Test
+ void testValidate_IncludedWeakMatchDoesNotMaskStrongMismatch() {
+ ChecksumValidator validator = newValidator(SHA1, MD5);
+ try {
+ validator.validate(checksums(SHA1, "not-foo", MD5, "bar"), checksums(MD5, "bar", SHA1, "foo"));
+ fail("expected exception");
+ } catch (ChecksumFailureException e) {
+ assertEquals("foo", e.getExpected());
+ assertEquals(ChecksumKind.REMOTE_INCLUDED.name(), e.getExpectedKind());
+ assertEquals("not-foo", e.getActual());
+ }
+ fetcher.assertFetchedFiles();
+ policy.assertCallbacks("match(MD5, REMOTE_INCLUDED)", "mismatch(SHA-1, REMOTE_INCLUDED)");
+ }
+
@Test
void testValidate_AcceptOnEnd() throws Exception {
policy.inspectAll = true;
diff --git a/maven-resolver-generator-gnupg/src/main/java/org/eclipse/aether/generator/gnupg/GnupgSignatureArtifactGenerator.java b/maven-resolver-generator-gnupg/src/main/java/org/eclipse/aether/generator/gnupg/GnupgSignatureArtifactGenerator.java
index c9d3ed491..a29eec498 100644
--- a/maven-resolver-generator-gnupg/src/main/java/org/eclipse/aether/generator/gnupg/GnupgSignatureArtifactGenerator.java
+++ b/maven-resolver-generator-gnupg/src/main/java/org/eclipse/aether/generator/gnupg/GnupgSignatureArtifactGenerator.java
@@ -87,28 +87,48 @@ final class GnupgSignatureArtifactGenerator implements ArtifactGenerator {
try {
artifacts.addAll(generatedArtifacts);
- // back out if PGP signatures found among artifacts
- if (artifacts.stream().anyMatch(a -> a.getExtension().endsWith(ARTIFACT_EXTENSION))) {
- logger.debug("GPG signatures are present among artifacts, bailing out");
+ // Determine, per artifact, which signable artifacts still need a signature. A pre-existing signature
+ // (e.g. produced by maven-gpg-plugin) skips only the artifact it covers; it must not disable signing
+ // of the whole artifact set, which would silently publish partially unsigned releases.
+ ArrayList<Artifact> artifactsToSign = new ArrayList<>();
+ for (Artifact artifact : artifacts) {
+ if (isSignatureArtifact(artifact)) {
+ continue; // never sign a signature
+ }
+ if (!signableArtifactPredicate.test(artifact)) {
+ continue;
+ }
+ if (hasSignature(artifact)) {
+ logger.debug("GPG signature already present for {}, not signing it again", artifact);
+ continue;
+ }
+ artifactsToSign.add(artifact);
+ }
+ if (artifactsToSign.isEmpty()) {
+ logger.debug("GPG signatures are present for all signable artifacts, nothing to sign");
return Collections.emptyList();
}
+ if (artifacts.stream().anyMatch(this::isSignatureArtifact)) {
+ logger.info(
+ "GPG signatures are present for some artifacts only; signing the remaining {} artifact(s) with key {}",
+ artifactsToSign.size(),
+ keyInfo);
+ }
// sign relevant artifacts
ArrayList<Artifact> result = new ArrayList<>();
- for (Artifact artifact : artifacts) {
- if (signableArtifactPredicate.test(artifact)) {
- Path signatureTempFile = Files.createTempFile("signer-pgp", "tmp");
- signatureTempFiles.add(signatureTempFile);
- try (InputStream artifactContent = Files.newInputStream(artifact.getPath());
- OutputStream signatureContent = Files.newOutputStream(signatureTempFile)) {
- sign(artifactContent, signatureContent);
- }
- result.add(new SubArtifact(
- artifact,
- artifact.getClassifier(),
- artifact.getExtension() + ARTIFACT_EXTENSION,
- signatureTempFile.toFile()));
+ for (Artifact artifact : artifactsToSign) {
+ Path signatureTempFile = Files.createTempFile("signer-pgp", "tmp");
+ signatureTempFiles.add(signatureTempFile);
+ try (InputStream artifactContent = Files.newInputStream(artifact.getPath());
+ OutputStream signatureContent = Files.newOutputStream(signatureTempFile)) {
+ sign(artifactContent, signatureContent);
}
+ result.add(new SubArtifact(
+ artifact,
+ artifact.getClassifier(),
+ artifact.getExtension() + ARTIFACT_EXTENSION,
+ signatureTempFile.toFile()));
}
logger.debug("Signed {} artifacts with key {}", result.size(), keyInfo);
return result;
@@ -117,6 +137,20 @@ final class GnupgSignatureArtifactGenerator implements ArtifactGenerator {
}
}
+ private boolean isSignatureArtifact(Artifact artifact) {
+ return artifact.getExtension().endsWith(ARTIFACT_EXTENSION);
+ }
+
+ private boolean hasSignature(Artifact artifact) {
+ String signatureExtension = artifact.getExtension() + ARTIFACT_EXTENSION;
+ return artifacts.stream()
+ .anyMatch(a -> a.getExtension().equals(signatureExtension)
+ && a.getClassifier().equals(artifact.getClassifier())
+ && a.getArtifactId().equals(artifact.getArtifactId())
+ && a.getGroupId().equals(artifact.getGroupId())
+ && a.getVersion().equals(artifact.getVersion()));
+ }
+
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
diff --git a/maven-resolver-generator-gnupg/src/test/java/org/eclipse/aether/generator/gnupg/GpgSignerFactoryTest.java b/maven-resolver-generator-gnupg/src/test/java/org/eclipse/aether/generator/gnupg/GpgSignerFactoryTest.java
index 9f746f2df..94c4400a5 100644
--- a/maven-resolver-generator-gnupg/src/test/java/org/eclipse/aether/generator/gnupg/GpgSignerFactoryTest.java
+++ b/maven-resolver-generator-gnupg/src/test/java/org/eclipse/aether/generator/gnupg/GpgSignerFactoryTest.java
@@ -154,6 +154,32 @@ public class GpgSignerFactoryTest {
}
}
+ @Test
+ void signRemainingArtifactsWhenSomeSignaturesArePresent() throws Exception {
+ Path keyFile =
+ Paths.get("src/test/resources/gpg-signing/gpg-secret.key").toAbsolutePath();
+ String keyPass = "TheBigSecret";
+ GnupgSignatureArtifactGeneratorFactory factory = createFactory();
+ try (ArtifactGenerator signer =
+ factory.newInstance(createSession(keyFile, keyPass, false), new DeployRequest())) {
+ assertNotNull(signer);
+ Path artifactPath = Paths.get("src/test/resources/gpg-signing/artifact.txt");
+
+ // the jar already carries its signature; the pom does not: the pre-existing signature must skip
+ // only the jar, not disable signing of the whole artifact set (which would silently publish a
+ // partially unsigned release)
+ Collection<? extends Artifact> signatures = signer.generate(Arrays.asList(
+ new DefaultArtifact("org.apache.maven.resolver:test:jar:1.0").setPath(artifactPath),
+ new DefaultArtifact("org.apache.maven.resolver:test:jar.asc:1.0").setPath(artifactPath),
+ new DefaultArtifact("org.apache.maven.resolver:test:pom:1.0").setPath(artifactPath)));
+
+ assertEquals(1, signatures.size());
+ Artifact signature = signatures.iterator().next();
+ assertEquals("pom.asc", signature.getExtension());
+ assertEquals("", signature.getClassifier());
+ }
+ }
+
@Test
void signNonInteractive() throws Exception {
Path keyFile =
diff --git a/maven-resolver-generator-sigstore/src/main/java/org/eclipse/aether/generator/sigstore/SigstoreSignatureArtifactGenerator.java b/maven-resolver-generator-sigstore/src/main/java/org/eclipse/aether/generator/sigstore/SigstoreSignatureArtifactGenerator.java
index 63906cf07..472bcb6f0 100644
--- a/maven-resolver-generator-sigstore/src/main/java/org/eclipse/aether/generator/sigstore/SigstoreSignatureArtifactGenerator.java
+++ b/maven-resolver-generator-sigstore/src/main/java/org/eclipse/aether/generator/sigstore/SigstoreSignatureArtifactGenerator.java
@@ -79,11 +79,32 @@ final class SigstoreSignatureArtifactGenerator implements ArtifactGenerator {
try {
artifacts.addAll(generatedArtifacts);
- // back out if Sigstore signatures found among artifacts
- if (artifacts.stream().anyMatch(a -> a.getExtension().endsWith(ARTIFACT_EXTENSION))) {
- logger.debug("Sigstore signatures are present among artifacts, bailing out");
+ // Determine, per artifact, which signable artifacts still need a signature. A pre-existing signature
+ // skips only the artifact it covers; it must not disable signing of the whole artifact set, which
+ // would silently publish partially unsigned releases.
+ ArrayList<Artifact> artifactsToSign = new ArrayList<>();
+ for (Artifact artifact : artifacts) {
+ if (isSignatureArtifact(artifact)) {
+ continue; // never sign a signature
+ }
+ if (!signableArtifactPredicate.test(artifact)) {
+ continue;
+ }
+ if (hasSignature(artifact)) {
+ logger.debug("Sigstore signature already present for {}, not signing it again", artifact);
+ continue;
+ }
+ artifactsToSign.add(artifact);
+ }
+ if (artifactsToSign.isEmpty()) {
+ logger.debug("Sigstore signatures are present for all signable artifacts, nothing to sign");
return Collections.emptyList();
}
+ if (artifacts.stream().anyMatch(this::isSignatureArtifact)) {
+ logger.info(
+ "Sigstore signatures are present for some artifacts only; signing the remaining {} artifact(s)",
+ artifactsToSign.size());
+ }
// sign relevant artifacts
ArrayList<Artifact> result = new ArrayList<>();
@@ -92,47 +113,45 @@ final class SigstoreSignatureArtifactGenerator implements ArtifactGenerator {
try (KeylessSigner signer = publicStaging
? KeylessSigner.builder().sigstoreStagingDefaults().build()
: KeylessSigner.builder().sigstorePublicDefaults().build()) {
- for (Artifact artifact : artifacts) {
- if (signableArtifactPredicate.test(artifact)) {
- Path fileToSign = artifact.getPath();
- Path signatureTempFile = Files.createTempFile("signer-sigstore", "tmp");
- signatureTempFiles.add(signatureTempFile);
-
- logger.debug("Signing " + artifact);
- long start = System.currentTimeMillis();
- Bundle bundle = signer.signFile(fileToSign);
-
- X509Certificate cert = (X509Certificate)
- bundle.getCertPath().getCertificates().get(0);
- long durationMinutes = Certificates.validity(cert, ChronoUnit.MINUTES);
-
- logger.debug(" Fulcio certificate (valid for "
- + durationMinutes
- + " m) obtained for "
- + cert.getSubjectAlternativeNames()
- .iterator()
- .next()
- .get(1)
- + " (by "
- + FulcioOidHelper.getIssuerV2(cert)
- + " IdP)");
-
- pathProcessor.write(signatureTempFile, bundle.toJson());
-
- long duration = System.currentTimeMillis() - start;
- logger.debug(" > Rekor entry "
- + bundle.getEntries().get(0).getLogIndex()
- + " obtained in "
- + duration
- + " ms, saved to "
- + signatureTempFile);
-
- result.add(new SubArtifact(
- artifact,
- artifact.getClassifier(),
- artifact.getExtension() + ARTIFACT_EXTENSION,
- signatureTempFile.toFile()));
- }
+ for (Artifact artifact : artifactsToSign) {
+ Path fileToSign = artifact.getPath();
+ Path signatureTempFile = Files.createTempFile("signer-sigstore", "tmp");
+ signatureTempFiles.add(signatureTempFile);
+
+ logger.debug("Signing " + artifact);
+ long start = System.currentTimeMillis();
+ Bundle bundle = signer.signFile(fileToSign);
+
+ X509Certificate cert = (X509Certificate)
+ bundle.getCertPath().getCertificates().get(0);
+ long durationMinutes = Certificates.validity(cert, ChronoUnit.MINUTES);
+
+ logger.debug(" Fulcio certificate (valid for "
+ + durationMinutes
+ + " m) obtained for "
+ + cert.getSubjectAlternativeNames()
+ .iterator()
+ .next()
+ .get(1)
+ + " (by "
+ + FulcioOidHelper.getIssuerV2(cert)
+ + " IdP)");
+
+ pathProcessor.write(signatureTempFile, bundle.toJson());
+
+ long duration = System.currentTimeMillis() - start;
+ logger.debug(" > Rekor entry "
+ + bundle.getEntries().get(0).getLogIndex()
+ + " obtained in "
+ + duration
+ + " ms, saved to "
+ + signatureTempFile);
+
+ result.add(new SubArtifact(
+ artifact,
+ artifact.getClassifier(),
+ artifact.getExtension() + ARTIFACT_EXTENSION,
+ signatureTempFile.toFile()));
}
} finally {
Thread.currentThread().setContextClassLoader(originalClassLoader);
@@ -150,6 +169,20 @@ final class SigstoreSignatureArtifactGenerator implements ArtifactGenerator {
}
}
+ private boolean isSignatureArtifact(Artifact artifact) {
+ return artifact.getExtension().endsWith(ARTIFACT_EXTENSION);
+ }
+
+ private boolean hasSignature(Artifact artifact) {
+ String signatureExtension = artifact.getExtension() + ARTIFACT_EXTENSION;
+ return artifacts.stream()
+ .anyMatch(a -> a.getExtension().equals(signatureExtension)
+ && a.getClassifier().equals(artifact.getClassifier())
+ && a.getArtifactId().equals(artifact.getArtifactId())
+ && a.getGroupId().equals(artifact.getGroupId())
+ && a.getVersion().equals(artifact.getVersion()));
+ }
+
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
index 8f052b92b..b41d845ae 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
@@ -27,6 +27,7 @@ import java.util.Map;
import org.eclipse.aether.ConfigurationProperties;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.ArtifactRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
@@ -101,6 +102,29 @@ public abstract class FileTrustedChecksumsSourceSupport implements TrustedChecks
return null;
}
+ /**
+ * This implementation will call into underlying code only if enabled, and will enforce non-{@code null} return
+ * value. In worst case, empty map should be returned, meaning "no trusted checksums available".
+ *
+ * @since 2.0.23
+ */
+ @Override
+ public Map<String, String> getTrustedMetadataChecksums(
+ RepositorySystemSession session,
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+ requireNonNull(session, "session is null");
+ requireNonNull(metadata, "metadata is null");
+ requireNonNull(artifactRepository, "artifactRepository is null");
+ requireNonNull(checksumAlgorithmFactories, "checksumAlgorithmFactories is null");
+ if (isEnabled(session)) {
+ return requireNonNull(
+ doGetTrustedMetadataChecksums(session, metadata, artifactRepository, checksumAlgorithmFactories));
+ }
+ return null;
+ }
+
/**
* This implementation will call into underlying code only if enabled. Underlying implementation may still choose
* to return {@code null}.
@@ -123,6 +147,19 @@ public abstract class FileTrustedChecksumsSourceSupport implements TrustedChecks
ArtifactRepository artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories);
+ /**
+ * Implementors MUST NOT return {@code null} at this point, as this source is enabled. Metadata checksums are
+ * looked up using the same file conventions as artifact checksums, with the metadata path composed from the
+ * origin repository key (for example {@code g/a/v/maven-metadata-central.xml}).
+ *
+ * @since 2.0.23
+ */
+ protected abstract Map<String, String> doGetTrustedMetadataChecksums(
+ RepositorySystemSession session,
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories);
+
/**
* Implementors may override this method and return {@link Writer} instance.
*/
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
index 8094374ca..c72366e64 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
@@ -34,6 +34,7 @@ import java.util.function.Function;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.internal.impl.LocalPathComposer;
+import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.ArtifactRepository;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
import org.eclipse.aether.spi.io.ChecksumProcessor;
@@ -48,7 +49,9 @@ import static java.util.Objects.requireNonNull;
/**
* Sparse file {@link FileTrustedChecksumsSourceSupport} implementation that use specified directory as base
* directory, where it expects artifacts checksums on standard Maven2 "local" layout. This implementation uses Artifact
- * coordinates solely to form path from basedir, pretty much as Maven local repository does.
+ * coordinates solely to form path from basedir, pretty much as Maven local repository does. Metadata checksums are
+ * covered as well, on the same layout, with the metadata path composed from the origin repository key (for example
+ * {@code g/a/v/maven-metadata-central.xml.sha1}).
* <p>
* The source by default is "origin aware", it will factor in origin repository ID as well into base directory name
* (for example ".checksums/central/...").
@@ -163,6 +166,45 @@ public final class SparseDirectoryTrustedChecksumsSource extends FileTrustedChec
return checksums;
}
+ @Override
+ protected Map<String, String> doGetTrustedMetadataChecksums(
+ RepositorySystemSession session,
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+ final boolean originAware = isOriginAware(session);
+ final HashMap<String, String> checksums = new HashMap<>();
+ Path basedir = getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false);
+ if (Files.isDirectory(basedir)) {
+ for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
+ Path checksumPath = basedir.resolve(calculateMetadataPath(
+ originAware, metadata, repositoryKey(session, artifactRepository), checksumAlgorithmFactory));
+
+ if (!Files.isRegularFile(checksumPath)) {
+ LOGGER.debug(
+ "Metadata '{}' trusted checksum '{}' not found on path '{}'",
+ metadata,
+ checksumAlgorithmFactory.getName(),
+ checksumPath);
+ continue;
+ }
+
+ try {
+ String checksum = checksumProcessor.readChecksum(checksumPath);
+ if (checksum != null) {
+ checksums.put(checksumAlgorithmFactory.getName(), checksum);
+ }
+ } catch (IOException e) {
+ // unexpected, log
+ LOGGER.warn(
+ "Could not read metadata '{}' trusted checksum on path '{}'", metadata, checksumPath, e);
+ throw new UncheckedIOException(e);
+ }
+ }
+ }
+ return checksums;
+ }
+
@Override
protected Writer doGetTrustedArtifactChecksumsWriter(RepositorySystemSession session) {
return new SparseDirectoryWriter(
@@ -187,6 +229,21 @@ public final class SparseDirectoryTrustedChecksumsSource extends FileTrustedChec
return path;
}
+ private String calculateMetadataPath(
+ boolean originAware,
+ Metadata metadata,
+ String safeRepositoryId,
+ ChecksumAlgorithmFactory checksumAlgorithmFactory) {
+ String path = localPathComposer.getPathForMetadata(metadata, safeRepositoryId) + "."
+ + checksumAlgorithmFactory.getFileExtension();
+ if (originAware) {
+ // defense in depth: same guard as for artifact paths, the repository key is spliced into a directory
+ PathUtils.validatePathComponent(safeRepositoryId, "repository key");
+ path = safeRepositoryId + "/" + path;
+ }
+ return path;
+ }
+
private class SparseDirectoryWriter implements Writer {
private final Path basedir;
@@ -218,5 +275,23 @@ public final class SparseDirectoryTrustedChecksumsSource extends FileTrustedChec
checksumProcessor.writeChecksum(checksumPath, checksum);
}
}
+
+ @Override
+ public void addTrustedMetadataChecksums(
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
+ Map<String, String> trustedMetadataChecksums)
+ throws IOException {
+ for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
+ Path checksumPath = basedir.resolve(calculateMetadataPath(
+ originAware,
+ metadata,
+ idToPathSegmentFunction.apply(artifactRepository),
+ checksumAlgorithmFactory));
+ String checksum = requireNonNull(trustedMetadataChecksums.get(checksumAlgorithmFactory.getName()));
+ checksumProcessor.writeChecksum(checksumPath, checksum);
+ }
+ }
}
}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
index f7f451546..bc982be8c 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
@@ -43,6 +43,7 @@ import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.impl.RepositorySystemLifecycle;
import org.eclipse.aether.internal.impl.LocalPathComposer;
+import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.ArtifactRepository;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
import org.eclipse.aether.spi.io.PathProcessor;
@@ -57,7 +58,8 @@ import static java.util.Objects.requireNonNull;
* Compact file {@link FileTrustedChecksumsSourceSupport} implementation that use specified directory as base
* directory, where it expects a "summary" file named as "checksums.${checksumExt}" for each checksum algorithm.
* File format is GNU Coreutils compatible: each line holds checksum followed by two spaces and artifact relative path
- * (from local repository root, without leading "./"). This means that trusted checksums summary file can be used to
+ * (from local repository root, without leading "./"). Metadata checksums are covered as well, keyed by the metadata
+ * path composed from the origin repository key (for example {@code g/a/v/maven-metadata-central.xml}). This means that trusted checksums summary file can be used to
* validate artifacts or generate it using standard GNU tools like GNU {@code sha1sum} is (for BSD derivatives same
* file can be used with {@code -r} switch).
* <p>
@@ -170,10 +172,38 @@ public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksum
Artifact artifact,
ArtifactRepository artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+ return doGetTrustedPathChecksums(
+ session,
+ localPathComposer.getPathForArtifact(artifact, false),
+ artifactRepository,
+ checksumAlgorithmFactories);
+ }
+
+ @Override
+ protected Map<String, String> doGetTrustedMetadataChecksums(
+ RepositorySystemSession session,
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+ return doGetTrustedPathChecksums(
+ session,
+ localPathComposer.getPathForMetadata(metadata, repositoryKey(session, artifactRepository)),
+ artifactRepository,
+ checksumAlgorithmFactories);
+ }
+
+ /**
+ * Looks up the trusted checksums for given local-repository style path (the summary file 2nd column), be it
+ * an artifact or a metadata path.
+ */
+ private Map<String, String> doGetTrustedPathChecksums(
+ RepositorySystemSession session,
+ String path,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
final HashMap<String, String> result = new HashMap<>();
final Path basedir = getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false);
if (Files.isDirectory(basedir)) {
- final String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
final boolean originAware = isOriginAware(session);
for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
Path summaryFile = summaryFile(
@@ -183,7 +213,7 @@ public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksum
checksumAlgorithmFactory.getFileExtension());
ConcurrentHashMap<String, String> algorithmChecksums =
checksums.computeIfAbsent(summaryFile, f -> loadProvidedChecksums(summaryFile));
- String checksum = algorithmChecksums.get(artifactPath);
+ String checksum = algorithmChecksums.get(path);
if (checksum != null) {
result.put(checksumAlgorithmFactory.getName(), checksum);
}
@@ -284,27 +314,50 @@ public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksum
ArtifactRepository artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
Map<String, String> trustedArtifactChecksums) {
- String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
+ addTrustedPathChecksums(
+ artifact,
+ localPathComposer.getPathForArtifact(artifact, false),
+ artifactRepository,
+ checksumAlgorithmFactories,
+ trustedArtifactChecksums);
+ }
+
+ @Override
+ public void addTrustedMetadataChecksums(
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
+ Map<String, String> trustedMetadataChecksums) {
+ addTrustedPathChecksums(
+ metadata,
+ localPathComposer.getPathForMetadata(metadata, repositoryKeyFunction.apply(artifactRepository)),
+ artifactRepository,
+ checksumAlgorithmFactories,
+ trustedMetadataChecksums);
+ }
+
+ private void addTrustedPathChecksums(
+ Object subject,
+ String path,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
+ Map<String, String> trustedChecksums) {
for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
Path summaryFile = summaryFile(
basedir,
originAware,
repositoryKeyFunction.apply(artifactRepository),
checksumAlgorithmFactory.getFileExtension());
- String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName()));
+ String checksum = requireNonNull(trustedChecksums.get(checksumAlgorithmFactory.getName()));
String oldChecksum = cache.computeIfAbsent(summaryFile, k -> loadProvidedChecksums(summaryFile))
- .put(artifactPath, checksum);
+ .put(path, checksum);
if (oldChecksum == null) {
changedChecksums.put(summaryFile, Boolean.TRUE); // new
} else if (!Objects.equals(oldChecksum, checksum)) {
changedChecksums.put(summaryFile, Boolean.TRUE); // replaced
- LOGGER.info(
- "Trusted checksum for artifact {} replaced: old {}, new {}",
- artifact,
- oldChecksum,
- checksum);
+ LOGGER.info("Trusted checksum for {} replaced: old {}, new {}", subject, oldChecksum, checksum);
}
}
}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapter.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapter.java
index 374cb8f54..16d2ca368 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapter.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapter.java
@@ -27,10 +27,12 @@ import java.util.Map;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.spi.checksums.ProvidedChecksumsSource;
import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
import static java.util.Objects.requireNonNull;
@@ -83,4 +85,33 @@ public final class TrustedToProvidedChecksumsSourceAdapter implements ProvidedCh
}
return null;
}
+
+ @Override
+ public Map<String, String> getProvidedMetadataChecksums(
+ RepositorySystemSession session,
+ MetadataDownload transfer,
+ RemoteRepository repository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+ Metadata metadata = transfer.getMetadata();
+ Map<String, String> trustedChecksums;
+ // check for connector repository
+ for (TrustedChecksumsSource trustedChecksumsSource : trustedChecksumsSources.values()) {
+ trustedChecksums = trustedChecksumsSource.getTrustedMetadataChecksums(
+ session, metadata, repository, checksumAlgorithmFactories);
+ if (trustedChecksums != null && !trustedChecksums.isEmpty()) {
+ return trustedChecksums;
+ }
+ }
+ // if repo above is "mirrorOf", this one kicks in
+ for (RemoteRepository remoteRepository : transfer.getRepositories()) {
+ for (TrustedChecksumsSource trustedChecksumsSource : trustedChecksumsSources.values()) {
+ trustedChecksums = trustedChecksumsSource.getTrustedMetadataChecksums(
+ session, metadata, remoteRepository, checksumAlgorithmFactories);
+ if (trustedChecksums != null && !trustedChecksums.isEmpty()) {
+ return trustedChecksums;
+ }
+ }
+ }
+ return null;
+ }
}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java
index eef4f2cc1..14c0349e1 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java
@@ -28,6 +28,8 @@ import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.impl.RepositorySystemLifecycle;
import org.eclipse.aether.internal.impl.DefaultRepositorySystemLifecycle;
import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
import org.junit.jupiter.api.BeforeEach;
@@ -43,6 +45,14 @@ public abstract class FileTrustedChecksumsSourceTestSupport {
private static final String ARTIFACT_TRUSTED_CHECKSUM = "trustedChecksum";
+ private static final Metadata METADATA_WITHOUT_CHECKSUM =
+ new DefaultMetadata("test", "test", "1.0", "maven-metadata.xml", Metadata.Nature.RELEASE);
+
+ private static final Metadata METADATA_WITH_CHECKSUM =
+ new DefaultMetadata("test", "test", "2.0", "maven-metadata.xml", Metadata.Nature.RELEASE);
+
+ private static final String METADATA_TRUSTED_CHECKSUM = "trustedMetadataChecksum";
+
private DefaultRepositorySystemSession session;
private ChecksumAlgorithmFactory checksumAlgorithmFactory;
@@ -73,6 +83,13 @@ public abstract class FileTrustedChecksumsSourceTestSupport {
prepareSession.getLocalRepository(),
Collections.singletonList(checksumAlgorithmFactory),
checksums);
+ HashMap<String, String> metadataChecksums = new HashMap<>();
+ metadataChecksums.put(checksumAlgorithmFactory.getName(), METADATA_TRUSTED_CHECKSUM);
+ writer.addTrustedMetadataChecksums(
+ METADATA_WITH_CHECKSUM,
+ prepareSession.getLocalRepository(),
+ Collections.singletonList(checksumAlgorithmFactory),
+ metadataChecksums);
checksumWritten = true;
}
}
@@ -116,4 +133,40 @@ public abstract class FileTrustedChecksumsSourceTestSupport {
assertEquals(1, providedChecksums.size());
assertEquals(ARTIFACT_TRUSTED_CHECKSUM, providedChecksums.get(checksumAlgorithmFactory.getName()));
}
+
+ @Test
+ void metadataNotEnabled() {
+ assertNull(subject.getTrustedMetadataChecksums(
+ session,
+ METADATA_WITH_CHECKSUM,
+ session.getLocalRepository(),
+ Collections.singletonList(checksumAlgorithmFactory)));
+ }
+
+ @Test
+ void noProvidedMetadataChecksum() {
+ enableSource(session);
+ Map<String, String> providedChecksums = subject.getTrustedMetadataChecksums(
+ session,
+ METADATA_WITHOUT_CHECKSUM,
+ session.getLocalRepository(),
+ Collections.singletonList(checksumAlgorithmFactory));
+ assertNotNull(providedChecksums);
+ assertTrue(providedChecksums.isEmpty());
+ }
+
+ @Test
+ void haveProvidedMetadataChecksum() {
+ assumeTrue(checksumWritten);
+ enableSource(session);
+ Map<String, String> providedChecksums = subject.getTrustedMetadataChecksums(
+ session,
+ METADATA_WITH_CHECKSUM,
+ session.getLocalRepository(),
+ Collections.singletonList(checksumAlgorithmFactory));
+ assertNotNull(providedChecksums);
+ assertFalse(providedChecksums.isEmpty());
+ assertEquals(1, providedChecksums.size());
+ assertEquals(METADATA_TRUSTED_CHECKSUM, providedChecksums.get(checksumAlgorithmFactory.getName()));
+ }
}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapterTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapterTest.java
index 760fd3a3b..00ced5873 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapterTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/TrustedToProvidedChecksumsSourceAdapterTest.java
@@ -26,9 +26,12 @@ import java.util.Map;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -43,6 +46,12 @@ public class TrustedToProvidedChecksumsSourceAdapterTest {
private final Artifact artifactWithoutChecksum = new DefaultArtifact("g:a:v2");
+ private final Metadata metadataWithChecksum =
+ new DefaultMetadata("g", "a", "v1", "maven-metadata.xml", Metadata.Nature.RELEASE);
+
+ private final Metadata metadataWithoutChecksum =
+ new DefaultMetadata("g", "a", "v2", "maven-metadata.xml", Metadata.Nature.RELEASE);
+
private final RemoteRepository repository =
new RemoteRepository.Builder("repo", "default", "https://example.com").build();
@@ -62,6 +71,11 @@ public class TrustedToProvidedChecksumsSourceAdapterTest {
when(trustedChecksumsSource.getTrustedArtifactChecksums(
eq(session), eq(artifactWithChecksum), eq(repository), eq(checksums)))
.thenReturn(result);
+ HashMap<String, String> metadataResult = new HashMap<>();
+ metadataResult.put(Sha1ChecksumAlgorithmFactory.NAME, "bar");
+ when(trustedChecksumsSource.getTrustedMetadataChecksums(
+ eq(session), eq(metadataWithChecksum), eq(repository), eq(checksums)))
+ .thenReturn(metadataResult);
adapter = new TrustedToProvidedChecksumsSourceAdapter(
Collections.singletonMap("trusted", trustedChecksumsSource));
}
@@ -103,4 +117,42 @@ public class TrustedToProvidedChecksumsSourceAdapterTest {
Map<String, String> chk = adapter.getProvidedArtifactChecksums(session, transfer, mrm, checksums);
assertNull(chk);
}
+
+ @Test
+ void testMetadataSimplePositive() {
+ MetadataDownload transfer = new MetadataDownload();
+ transfer.setMetadata(metadataWithChecksum);
+ Map<String, String> chk = adapter.getProvidedMetadataChecksums(session, transfer, repository, checksums);
+ assertNotNull(chk);
+ assertEquals("bar", chk.get(Sha1ChecksumAlgorithmFactory.NAME));
+ }
+
+ @Test
+ void testMetadataSimpleNegative() {
+ MetadataDownload transfer = new MetadataDownload();
+ transfer.setMetadata(metadataWithoutChecksum);
+ Map<String, String> chk = adapter.getProvidedMetadataChecksums(session, transfer, repository, checksums);
+ assertNull(chk);
+ }
+
+ @Test
+ void testMetadataMrmPositive() {
+ RemoteRepository mrm = new RemoteRepository.Builder("mrm", "default", "https://example.com").build();
+ MetadataDownload transfer = new MetadataDownload();
+ transfer.setMetadata(metadataWithChecksum);
+ transfer.setRepositories(Collections.singletonList(repository));
+ Map<String, String> chk = adapter.getProvidedMetadataChecksums(session, transfer, mrm, checksums);
+ assertNotNull(chk);
+ assertEquals("bar", chk.get(Sha1ChecksumAlgorithmFactory.NAME));
+ }
+
+ @Test
+ void testMetadataMrmNegative() {
+ RemoteRepository mrm = new RemoteRepository.Builder("mrm", "default", "https://example.com").build();
+ MetadataDownload transfer = new MetadataDownload();
+ transfer.setMetadata(metadataWithoutChecksum);
+ transfer.setRepositories(Collections.singletonList(repository));
+ Map<String, String> chk = adapter.getProvidedMetadataChecksums(session, transfer, mrm, checksums);
+ assertNull(chk);
+ }
}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/ProvidedChecksumsSource.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/ProvidedChecksumsSource.java
index ddab3cf8f..ff4f83356 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/ProvidedChecksumsSource.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/ProvidedChecksumsSource.java
@@ -24,6 +24,7 @@ import java.util.Map;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
@@ -57,4 +58,30 @@ public interface ProvidedChecksumsSource {
ArtifactDownload transfer,
RemoteRepository remoteRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories);
+
+ /**
+ * May return the provided checksums (for given metadata transfer) from source other than remote repository,
+ * or {@code null} if it has no checksums available for given transfer. Semantics are the same as for
+ * {@link #getProvidedArtifactChecksums(RepositorySystemSession, ArtifactDownload, RemoteRepository, List)},
+ * but covering metadata downloads: metadata like {@code maven-metadata.xml} influences resolution decisions
+ * (for example version range selection), so it should be coverable by the strongest configured integrity
+ * source just like artifacts are.
+ * <p>
+ * The default implementation returns {@code null} ("nothing to add here"), preserving the behavior of
+ * implementations written before this method existed.
+ *
+ * @param session The current session.
+ * @param transfer The metadata transfer that is about to be executed.
+ * @param remoteRepository The remote repository connector is about to contact.
+ * @param checksumAlgorithmFactories The checksum algorithms that are expected.
+ * @return Map of expected checksums, or {@code null}.
+ * @since 2.0.23
+ */
+ default Map<String, String> getProvidedMetadataChecksums(
+ RepositorySystemSession session,
+ MetadataDownload transfer,
+ RemoteRepository remoteRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+ return null;
+ }
}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java
index 2af3590cc..b74d70529 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java
@@ -24,6 +24,7 @@ import java.util.Map;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.ArtifactRepository;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
@@ -55,6 +56,31 @@ public interface TrustedChecksumsSource {
ArtifactRepository artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories);
+ /**
+ * May return the trusted checksums (for given metadata) from trusted source, or {@code null} if not enabled
+ * or the implementation does not cover metadata. Semantics are the same as for
+ * {@link #getTrustedArtifactChecksums(RepositorySystemSession, Artifact, ArtifactRepository, List)}, but
+ * covering metadata: metadata like {@code maven-metadata.xml} influences resolution decisions (for example
+ * version range selection), so trusted sources able to attest metadata should expose that here.
+ * <p>
+ * The default implementation returns {@code null} ("not enabled for metadata"), preserving the behavior of
+ * implementations written before this method existed.
+ *
+ * @param session The repository system session, never {@code null}.
+ * @param metadata The metadata we want checksums for, never {@code null}.
+ * @param artifactRepository The origin repository: local, workspace, remote repository, never {@code null}.
+ * @param checksumAlgorithmFactories The checksum algorithms that are expected, never {@code null}.
+ * @return Map of expected checksums, or {@code null}.
+ * @since 2.0.23
+ */
+ default Map<String, String> getTrustedMetadataChecksums(
+ RepositorySystemSession session,
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+ return null;
+ }
+
/**
* A writer that is able to write/add trusted checksums to this implementation.
*/
@@ -70,6 +96,25 @@ public interface TrustedChecksumsSource {
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
Map<String, String> trustedArtifactChecksums)
throws IOException;
+
+ /**
+ * Performs whatever implementation requires to "set" (write/add/append) given map of trusted checksums,
+ * for given metadata. Semantics are the same as for
+ * {@link #addTrustedArtifactChecksums(Artifact, ArtifactRepository, List, Map)}, but covering metadata.
+ * <p>
+ * The default implementation records nothing, preserving the behavior of implementations written before
+ * this method existed.
+ *
+ * @since 2.0.23
+ */
+ default void addTrustedMetadataChecksums(
+ Metadata metadata,
+ ArtifactRepository artifactRepository,
+ List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
+ Map<String, String> trustedMetadataChecksums)
+ throws IOException {
+ // no-op: implementations written before this method existed record artifact checksums only
+ }
}
/**