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