001package org.eclipse.aether.internal.impl.resolution;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import javax.inject.Inject;
023import javax.inject.Named;
024import javax.inject.Singleton;
025
026import java.io.IOException;
027import java.io.UncheckedIOException;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Objects;
032import java.util.Set;
033
034import org.eclipse.aether.RepositorySystemSession;
035import org.eclipse.aether.artifact.Artifact;
036import org.eclipse.aether.repository.ArtifactRepository;
037import org.eclipse.aether.resolution.ArtifactResult;
038import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
039import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
040import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
041import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper;
042import org.eclipse.aether.transfer.ChecksumFailureException;
043import org.eclipse.aether.util.ConfigUtils;
044import org.eclipse.aether.util.artifact.ArtifactIdUtils;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048import static java.util.Objects.requireNonNull;
049
050/**
051 * Artifact resolver processor that verifies the checksums of all resolved artifacts against trusted checksums. Is also
052 * able to "record" (calculate and write them) to trusted checksum sources, that do support this operation.
053 * <p>
054 * It uses a list of {@link ChecksumAlgorithmFactory}ies to work with, by default SHA-1.
055 * <p>
056 * Configuration keys:
057 * <ul>
058 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms} - Comma separated
059 *       list of {@link ChecksumAlgorithmFactory} names to use (default "SHA-1").</li>
060 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.failIfMissing} - To fail if artifact
061 *       being validated is missing a trusted checksum (default {@code false}).</li>
062 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.snapshots} - Should snapshot artifacts be
063 *       handled (validated or recorded). Snapshots are by "best practice" in-house produced, hence should be trusted
064 *       (default {@code false}).</li>
065 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.record} - If this value set to {@code true},
066 *       this component with not validate but "record" encountered artifact checksums instead
067 *       (default {@code false}).</li>
068 * </ul>
069 * <p>
070 * This component uses {@link TrustedChecksumsSource} as source of checksums for validation and also to "record" the
071 * calculated checksums. To have this component usable, there must exist at least one enabled checksum source. In case
072 * of multiple checksum sources enabled, ALL of them are used as source for validation or recording. This
073 * implies that if two enabled checksum sources "disagree" about an artifact checksum, the validation failure is
074 * inevitable.
075 *
076 * @since 1.9.0
077 */
078@Singleton
079@Named( TrustedChecksumsArtifactResolverPostProcessor.NAME )
080public final class TrustedChecksumsArtifactResolverPostProcessor
081        extends ArtifactResolverPostProcessorSupport
082{
083    public static final String NAME = "trustedChecksums";
084
085    private static final String CONF_NAME_CHECKSUM_ALGORITHMS = "checksumAlgorithms";
086
087    private static final String DEFAULT_CHECKSUM_ALGORITHMS = "SHA-1";
088
089    private static final String CONF_NAME_FAIL_IF_MISSING = "failIfMissing";
090
091    private static final String CONF_NAME_SNAPSHOTS = "snapshots";
092
093    private static final String CONF_NAME_RECORD = "record";
094
095    private static final String CHECKSUM_ALGORITHMS_CACHE_KEY =
096            TrustedChecksumsArtifactResolverPostProcessor.class.getName() + ".checksumAlgorithms";
097
098    private static final Logger LOGGER = LoggerFactory.getLogger( TrustedChecksumsArtifactResolverPostProcessor.class );
099
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}