View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.impl.resolution;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.IOException;
26  import java.io.UncheckedIOException;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.Set;
32  
33  import org.eclipse.aether.Keys;
34  import org.eclipse.aether.RepositorySystemSession;
35  import org.eclipse.aether.artifact.Artifact;
36  import org.eclipse.aether.repository.ArtifactRepository;
37  import org.eclipse.aether.resolution.ArtifactResult;
38  import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
39  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
40  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
41  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper;
42  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
43  import org.eclipse.aether.transfer.ChecksumFailureException;
44  import org.eclipse.aether.util.ConfigUtils;
45  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
46  
47  import static java.util.Objects.requireNonNull;
48  
49  /**
50   * Artifact resolver processor that verifies the checksums of all resolved artifacts against trusted checksums. Is also
51   * able to "record" (calculate and write them) to trusted checksum sources, that do support this operation.
52   * <p>
53   * It uses a list of {@link ChecksumAlgorithmFactory}ies to work with, by default SHA-1.
54   * <p>
55   * Configuration keys:
56   * <ul>
57   *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms} - Comma separated
58   *       list of {@link ChecksumAlgorithmFactory} names to use (default "SHA-1").</li>
59   *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.failIfMissing} - To fail if artifact
60   *       being validated is missing a trusted checksum (default {@code false}).</li>
61   *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.snapshots} - Should snapshot artifacts be
62   *       handled (validated or recorded). Snapshots are by "best practice" in-house produced, hence should be trusted
63   *       (default {@code false}).</li>
64   *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.record} - If this value set to {@code true},
65   *       this component with not validate but "record" encountered artifact checksums instead
66   *       (default {@code false}).</li>
67   * </ul>
68   * <p>
69   * This component uses {@link TrustedChecksumsSource} as source of checksums for validation and also to "record" the
70   * calculated checksums. To have this component usable, there must exist at least one enabled checksum source. In case
71   * of multiple checksum sources enabled, ALL of them are used as source for validation or recording. This
72   * implies that if two enabled checksum sources "disagree" about an artifact checksum, the validation failure is
73   * inevitable.
74   *
75   * @since 1.9.0
76   */
77  @Singleton
78  @Named(TrustedChecksumsArtifactResolverPostProcessor.NAME)
79  public final class TrustedChecksumsArtifactResolverPostProcessor extends ArtifactResolverPostProcessorSupport {
80      public static final String NAME = "trustedChecksums";
81  
82      private static final String CONFIG_PROPS_PREFIX =
83              ArtifactResolverPostProcessorSupport.CONFIG_PROPS_PREFIX + NAME + ".";
84  
85      /**
86       * Is post processor enabled.
87       *
88       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
89       * @configurationType {@link java.lang.Boolean}
90       * @configurationDefaultValue false
91       */
92      public static final String CONFIG_PROP_ENABLED = ArtifactResolverPostProcessorSupport.CONFIG_PROPS_PREFIX + NAME;
93  
94      /**
95       * The checksum algorithms to apply during post-processing as comma separated list.
96       *
97       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
98       * @configurationType {@link java.lang.String}
99       * @configurationDefaultValue {@link #DEFAULT_CHECKSUM_ALGORITHMS}
100      */
101     public static final String CONFIG_PROP_CHECKSUM_ALGORITHMS = CONFIG_PROPS_PREFIX + "checksumAlgorithms";
102 
103     public static final String DEFAULT_CHECKSUM_ALGORITHMS = "SHA-1";
104 
105     /**
106      * The scope to apply during post-processing. Accepted values are {@code all} (is default and is what happened
107      * before), and {@code project} when the scope of verification are project dependencies only (i.e. plugins are
108      * not verified).
109      *
110      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
111      * @configurationType {@link java.lang.String}
112      * @configurationDefaultValue {@link #DEFAULT_SCOPE}
113      * @since 2.0.11
114      */
115     public static final String CONFIG_PROP_SCOPE = CONFIG_PROPS_PREFIX + "scope";
116 
117     public static final String ALL_SCOPE = "all";
118 
119     public static final String PROJECT_SCOPE = "project";
120 
121     public static final String DEFAULT_SCOPE = ALL_SCOPE;
122 
123     /**
124      * Should post processor fail resolution if checksum is missing?
125      *
126      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
127      * @configurationType {@link java.lang.Boolean}
128      * @configurationDefaultValue false
129      */
130     public static final String CONFIG_PROP_FAIL_IF_MISSING = CONFIG_PROPS_PREFIX + "failIfMissing";
131 
132     /**
133      * Should post processor process snapshots as well?
134      *
135      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
136      * @configurationType {@link java.lang.Boolean}
137      * @configurationDefaultValue false
138      */
139     public static final String CONFIG_PROP_SNAPSHOTS = CONFIG_PROPS_PREFIX + "snapshots";
140 
141     /**
142      * Should post processor go into "record" mode (and collect checksums instead of validate them)?
143      *
144      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
145      * @configurationType {@link java.lang.Boolean}
146      * @configurationDefaultValue false
147      */
148     public static final String CONFIG_PROP_RECORD = CONFIG_PROPS_PREFIX + "record";
149 
150     private static final Object CHECKSUM_ALGORITHMS_CACHE_KEY =
151             Keys.of(TrustedChecksumsArtifactResolverPostProcessor.class, "checksumAlgorithms");
152 
153     private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;
154 
155     private final Map<String, TrustedChecksumsSource> trustedChecksumsSources;
156 
157     @Inject
158     public TrustedChecksumsArtifactResolverPostProcessor(
159             ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector,
160             Map<String, TrustedChecksumsSource> trustedChecksumsSources) {
161         this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector);
162         this.trustedChecksumsSources = requireNonNull(trustedChecksumsSources);
163     }
164 
165     @Override
166     protected boolean isEnabled(RepositorySystemSession session) {
167         return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED);
168     }
169 
170     private boolean inScope(RepositorySystemSession session, ArtifactResult artifactResult) {
171         String scope = ConfigUtils.getString(session, DEFAULT_SCOPE, CONFIG_PROP_SCOPE);
172         if (ALL_SCOPE.equals(scope)) {
173             return artifactResult.isResolved();
174         } else if (PROJECT_SCOPE.equals(scope)) {
175             return artifactResult.isResolved()
176                     && artifactResult.getRequest().getRequestContext().startsWith("project");
177         } else {
178             throw new IllegalArgumentException("Unknown value for configuration " + CONFIG_PROP_SCOPE + ": " + scope);
179         }
180     }
181 
182     @SuppressWarnings("unchecked")
183     @Override
184     protected void doPostProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) {
185         final List<ChecksumAlgorithmFactory> checksumAlgorithms = (List<ChecksumAlgorithmFactory>) session.getData()
186                 .computeIfAbsent(
187                         CHECKSUM_ALGORITHMS_CACHE_KEY,
188                         () -> checksumAlgorithmFactorySelector.selectList(
189                                 ConfigUtils.parseCommaSeparatedUniqueNames(ConfigUtils.getString(
190                                         session, DEFAULT_CHECKSUM_ALGORITHMS, CONFIG_PROP_CHECKSUM_ALGORITHMS))));
191 
192         final boolean failIfMissing = ConfigUtils.getBoolean(session, false, CONFIG_PROP_FAIL_IF_MISSING);
193         final boolean record = ConfigUtils.getBoolean(session, false, CONFIG_PROP_RECORD);
194         final boolean snapshots = ConfigUtils.getBoolean(session, false, CONFIG_PROP_SNAPSHOTS);
195 
196         for (ArtifactResult artifactResult : artifactResults) {
197             if (artifactResult.getRequest().getArtifact().isSnapshot() && !snapshots) {
198                 continue;
199             }
200             if (inScope(session, artifactResult)) {
201                 if (record) {
202                     recordArtifactChecksums(session, artifactResult, checksumAlgorithms);
203                 } else if (!validateArtifactChecksums(session, artifactResult, checksumAlgorithms, failIfMissing)) {
204                     artifactResult.setArtifact(artifactResult.getArtifact().setPath(null)); // make it unresolved
205                 }
206             }
207         }
208     }
209 
210     /**
211      * Calculates and records checksums into trusted sources that support writing.
212      */
213     private void recordArtifactChecksums(
214             RepositorySystemSession session,
215             ArtifactResult artifactResult,
216             List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
217         Artifact artifact = artifactResult.getArtifact();
218         ArtifactRepository artifactRepository = artifactResult.getRepository();
219         try {
220             final Map<String, String> calculatedChecksums =
221                     ChecksumAlgorithmHelper.calculate(artifact.getPath(), checksumAlgorithmFactories);
222 
223             for (TrustedChecksumsSource trustedChecksumsSource : trustedChecksumsSources.values()) {
224                 TrustedChecksumsSource.Writer writer =
225                         trustedChecksumsSource.getTrustedArtifactChecksumsWriter(session);
226                 if (writer != null) {
227                     try {
228                         writer.addTrustedArtifactChecksums(
229                                 artifact, artifactRepository, checksumAlgorithmFactories, calculatedChecksums);
230                     } catch (IOException e) {
231                         throw new UncheckedIOException(
232                                 "Could not write required checksums for " + artifact.getPath(), e);
233                     }
234                 }
235             }
236         } catch (IOException e) {
237             throw new UncheckedIOException("Could not calculate required checksums for " + artifact.getPath(), e);
238         }
239     }
240 
241     /**
242      * Validates trusted checksums against {@link ArtifactResult}, returns {@code true} denoting "valid" checksums or
243      * {@code false} denoting "invalid" checksums.
244      */
245     private boolean validateArtifactChecksums(
246             RepositorySystemSession session,
247             ArtifactResult artifactResult,
248             List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
249             boolean failIfMissing) {
250         Artifact artifact = artifactResult.getArtifact();
251         ArtifactRepository artifactRepository = artifactResult.getRepository();
252         boolean valid = true;
253         boolean validated = false;
254         try {
255             // full set: calculate all algorithms we were asked for
256             final Map<String, String> calculatedChecksums =
257                     ChecksumAlgorithmHelper.calculate(artifact.getPath(), checksumAlgorithmFactories);
258 
259             for (Map.Entry<String, TrustedChecksumsSource> entry : trustedChecksumsSources.entrySet()) {
260                 final String trustedSourceName = entry.getKey();
261                 final TrustedChecksumsSource trustedChecksumsSource = entry.getValue();
262 
263                 // upper bound set: ask source for checksums, ideally same as calculatedChecksums but may be less
264                 Map<String, String> trustedChecksums = trustedChecksumsSource.getTrustedArtifactChecksums(
265                         session, artifact, artifactRepository, checksumAlgorithmFactories);
266 
267                 if (trustedChecksums == null) {
268                     continue; // not enabled
269                 }
270                 validated = true;
271 
272                 if (!calculatedChecksums.equals(trustedChecksums)) {
273                     Set<String> missingTrustedAlg = new HashSet<>(calculatedChecksums.keySet());
274                     missingTrustedAlg.removeAll(trustedChecksums.keySet());
275 
276                     if (!missingTrustedAlg.isEmpty() && failIfMissing) {
277                         artifactResult.addException(
278                                 artifactRepository,
279                                 ChecksumFailureException.noneAvailable(
280                                         "Missing from " + trustedSourceName
281                                                 + " trusted checksum(s) " + missingTrustedAlg + " for artifact "
282                                                 + ArtifactIdUtils.toId(artifact),
283                                         ChecksumPolicy.ChecksumKind.PROVIDED.name()));
284                         valid = false;
285                     }
286 
287                     // compare values but only present ones, failIfMissing handled above
288                     // we still want to report all: algX - missing, algY - mismatch, etc
289                     for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
290                         String calculatedChecksum = calculatedChecksums.get(checksumAlgorithmFactory.getName());
291                         String trustedChecksum = trustedChecksums.get(checksumAlgorithmFactory.getName());
292                         if (trustedChecksum != null && !Objects.equals(calculatedChecksum, trustedChecksum)) {
293                             artifactResult.addException(
294                                     artifactRepository,
295                                     ChecksumFailureException.mismatchDetail(
296                                             "Trusted Checksums Source: " + trustedSourceName,
297                                             trustedChecksum,
298                                             ChecksumPolicy.ChecksumKind.PROVIDED.name(),
299                                             calculatedChecksum));
300                             valid = false;
301                         }
302                     }
303                 }
304             }
305 
306             if (!validated && failIfMissing) {
307                 artifactResult.addException(
308                         artifactRepository,
309                         ChecksumFailureException.noneAvailable(
310                                 "There are no enabled trusted checksums source(s) to validate against.",
311                                 ChecksumPolicy.ChecksumKind.PROVIDED.name()));
312                 valid = false;
313             }
314         } catch (IOException e) {
315             throw new UncheckedIOException(e);
316         }
317         return valid;
318     }
319 }