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  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
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 CONF_NAME_CHECKSUM_ALGORITHMS = "checksumAlgorithms";
83  
84      private static final String DEFAULT_CHECKSUM_ALGORITHMS = "SHA-1";
85  
86      private static final String CONF_NAME_FAIL_IF_MISSING = "failIfMissing";
87  
88      private static final String CONF_NAME_SNAPSHOTS = "snapshots";
89  
90      private static final String CONF_NAME_RECORD = "record";
91  
92      private static final String CHECKSUM_ALGORITHMS_CACHE_KEY =
93              TrustedChecksumsArtifactResolverPostProcessor.class.getName() + ".checksumAlgorithms";
94  
95      private static final Logger LOGGER = LoggerFactory.getLogger(TrustedChecksumsArtifactResolverPostProcessor.class);
96  
97      private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;
98  
99      private final Map<String, TrustedChecksumsSource> trustedChecksumsSources;
100 
101     @Inject
102     public TrustedChecksumsArtifactResolverPostProcessor(
103             ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector,
104             Map<String, TrustedChecksumsSource> trustedChecksumsSources) {
105         super(NAME);
106         this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector);
107         this.trustedChecksumsSources = requireNonNull(trustedChecksumsSources);
108     }
109 
110     @SuppressWarnings("unchecked")
111     @Override
112     protected void doPostProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) {
113         final List<ChecksumAlgorithmFactory> checksumAlgorithms = (List<ChecksumAlgorithmFactory>) session.getData()
114                 .computeIfAbsent(
115                         CHECKSUM_ALGORITHMS_CACHE_KEY,
116                         () -> checksumAlgorithmFactorySelector.selectList(
117                                 ConfigUtils.parseCommaSeparatedUniqueNames(ConfigUtils.getString(
118                                         session, DEFAULT_CHECKSUM_ALGORITHMS, CONF_NAME_CHECKSUM_ALGORITHMS))));
119 
120         final boolean failIfMissing = ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_FAIL_IF_MISSING));
121         final boolean record = ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_RECORD));
122         final boolean snapshots = ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_SNAPSHOTS));
123 
124         for (ArtifactResult artifactResult : artifactResults) {
125             if (artifactResult.getArtifact().isSnapshot() && !snapshots) {
126                 continue;
127             }
128             if (artifactResult.isResolved()) {
129                 if (record) {
130                     recordArtifactChecksums(session, artifactResult, checksumAlgorithms);
131                 } else if (!validateArtifactChecksums(session, artifactResult, checksumAlgorithms, failIfMissing)) {
132                     artifactResult.setArtifact(artifactResult.getArtifact().setFile(null)); // make it unresolved
133                 }
134             }
135         }
136     }
137 
138     /**
139      * Calculates and records checksums into trusted sources that support writing.
140      */
141     private void recordArtifactChecksums(
142             RepositorySystemSession session,
143             ArtifactResult artifactResult,
144             List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
145         Artifact artifact = artifactResult.getArtifact();
146         ArtifactRepository artifactRepository = artifactResult.getRepository();
147         try {
148             final Map<String, String> calculatedChecksums =
149                     ChecksumAlgorithmHelper.calculate(artifact.getFile(), checksumAlgorithmFactories);
150 
151             for (TrustedChecksumsSource trustedChecksumsSource : trustedChecksumsSources.values()) {
152                 TrustedChecksumsSource.Writer writer =
153                         trustedChecksumsSource.getTrustedArtifactChecksumsWriter(session);
154                 if (writer != null) {
155                     try {
156                         writer.addTrustedArtifactChecksums(
157                                 artifact, artifactRepository, checksumAlgorithmFactories, calculatedChecksums);
158                     } catch (IOException e) {
159                         throw new UncheckedIOException(
160                                 "Could not write required checksums for " + artifact.getFile(), e);
161                     }
162                 }
163             }
164         } catch (IOException e) {
165             throw new UncheckedIOException("Could not calculate required checksums for " + artifact.getFile(), e);
166         }
167     }
168 
169     /**
170      * Validates trusted checksums against {@link ArtifactResult}, returns {@code true} denoting "valid" checksums or
171      * {@code false} denoting "invalid" checksums.
172      */
173     private boolean validateArtifactChecksums(
174             RepositorySystemSession session,
175             ArtifactResult artifactResult,
176             List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
177             boolean failIfMissing) {
178         Artifact artifact = artifactResult.getArtifact();
179         ArtifactRepository artifactRepository = artifactResult.getRepository();
180         boolean valid = true;
181         boolean validated = false;
182         try {
183             // full set: calculate all algorithms we were asked for
184             final Map<String, String> calculatedChecksums =
185                     ChecksumAlgorithmHelper.calculate(artifact.getFile(), checksumAlgorithmFactories);
186 
187             for (Map.Entry<String, TrustedChecksumsSource> entry : trustedChecksumsSources.entrySet()) {
188                 final String trustedSourceName = entry.getKey();
189                 final TrustedChecksumsSource trustedChecksumsSource = entry.getValue();
190 
191                 // upper bound set: ask source for checksums, ideally same as calculatedChecksums but may be less
192                 Map<String, String> trustedChecksums = trustedChecksumsSource.getTrustedArtifactChecksums(
193                         session, artifact, artifactRepository, checksumAlgorithmFactories);
194 
195                 if (trustedChecksums == null) {
196                     continue; // not enabled
197                 }
198                 validated = true;
199 
200                 if (!calculatedChecksums.equals(trustedChecksums)) {
201                     Set<String> missingTrustedAlg = new HashSet<>(calculatedChecksums.keySet());
202                     missingTrustedAlg.removeAll(trustedChecksums.keySet());
203 
204                     if (!missingTrustedAlg.isEmpty() && failIfMissing) {
205                         artifactResult.addException(new ChecksumFailureException("Missing from " + trustedSourceName
206                                 + " trusted checksum(s) " + missingTrustedAlg + " for artifact "
207                                 + ArtifactIdUtils.toId(artifact)));
208                         valid = false;
209                     }
210 
211                     // compare values but only present ones, failIfMissing handled above
212                     // we still want to report all: algX - missing, algY - mismatch, etc
213                     for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
214                         String calculatedChecksum = calculatedChecksums.get(checksumAlgorithmFactory.getName());
215                         String trustedChecksum = trustedChecksums.get(checksumAlgorithmFactory.getName());
216                         if (trustedChecksum != null && !Objects.equals(calculatedChecksum, trustedChecksum)) {
217                             artifactResult.addException(new ChecksumFailureException("Artifact "
218                                     + ArtifactIdUtils.toId(artifact) + " trusted checksum mismatch: "
219                                     + trustedSourceName + "=" + trustedChecksum + "; calculated="
220                                     + calculatedChecksum));
221                             valid = false;
222                         }
223                     }
224                 }
225             }
226 
227             if (!validated && failIfMissing) {
228                 artifactResult.addException(new ChecksumFailureException(
229                         "There are no enabled trusted checksums" + " source(s) to validate against."));
230                 valid = false;
231             }
232         } catch (IOException e) {
233             throw new UncheckedIOException(e);
234         }
235         return valid;
236     }
237 }