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