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