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.Keys;
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.spi.connector.checksum.ChecksumPolicy;
043import org.eclipse.aether.transfer.ChecksumFailureException;
044import org.eclipse.aether.util.ConfigUtils;
045import org.eclipse.aether.util.artifact.ArtifactIdUtils;
046
047import static java.util.Objects.requireNonNull;
048
049/**
050 * Artifact resolver processor that verifies the checksums of all resolved artifacts against trusted checksums. Is also
051 * able to "record" (calculate and write them) to trusted checksum sources, that do support this operation.
052 * <p>
053 * It uses a list of {@link ChecksumAlgorithmFactory}ies to work with, by default SHA-1.
054 * <p>
055 * Configuration keys:
056 * <ul>
057 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms} - Comma separated
058 *       list of {@link ChecksumAlgorithmFactory} names to use (default "SHA-1").</li>
059 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.failIfMissing} - To fail if artifact
060 *       being validated is missing a trusted checksum (default {@code false}).</li>
061 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.snapshots} - Should snapshot artifacts be
062 *       handled (validated or recorded). Snapshots are by "best practice" in-house produced, hence should be trusted
063 *       (default {@code false}).</li>
064 *     <li>{@code aether.artifactResolver.postProcessor.trustedChecksums.record} - If this value set to {@code true},
065 *       this component with not validate but "record" encountered artifact checksums instead
066 *       (default {@code false}).</li>
067 * </ul>
068 * <p>
069 * This component uses {@link TrustedChecksumsSource} as source of checksums for validation and also to "record" the
070 * calculated checksums. To have this component usable, there must exist at least one enabled checksum source. In case
071 * of multiple checksum sources enabled, ALL of them are used as source for validation or recording. This
072 * implies that if two enabled checksum sources "disagree" about an artifact checksum, the validation failure is
073 * inevitable.
074 *
075 * @since 1.9.0
076 */
077@Singleton
078@Named(TrustedChecksumsArtifactResolverPostProcessor.NAME)
079public final class TrustedChecksumsArtifactResolverPostProcessor extends ArtifactResolverPostProcessorSupport {
080    public static final String NAME = "trustedChecksums";
081
082    private static final String CONFIG_PROPS_PREFIX =
083            ArtifactResolverPostProcessorSupport.CONFIG_PROPS_PREFIX + NAME + ".";
084
085    /**
086     * Is post processor enabled.
087     *
088     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
089     * @configurationType {@link java.lang.Boolean}
090     * @configurationDefaultValue false
091     */
092    public static final String CONFIG_PROP_ENABLED = ArtifactResolverPostProcessorSupport.CONFIG_PROPS_PREFIX + NAME;
093
094    /**
095     * The checksum algorithms to apply during post-processing as comma separated list.
096     *
097     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
098     * @configurationType {@link java.lang.String}
099     * @configurationDefaultValue {@link #DEFAULT_CHECKSUM_ALGORITHMS}
100     */
101    public static final String CONFIG_PROP_CHECKSUM_ALGORITHMS = CONFIG_PROPS_PREFIX + "checksumAlgorithms";
102
103    public static final String DEFAULT_CHECKSUM_ALGORITHMS = "SHA-1";
104
105    /**
106     * The scope to apply during post-processing. Accepted values are {@code all} (is default and is what happened
107     * before), and {@code project} when the scope of verification are project dependencies only (i.e. plugins are
108     * not verified).
109     *
110     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
111     * @configurationType {@link java.lang.String}
112     * @configurationDefaultValue {@link #DEFAULT_SCOPE}
113     * @since 2.0.11
114     */
115    public static final String CONFIG_PROP_SCOPE = CONFIG_PROPS_PREFIX + "scope";
116
117    public static final String ALL_SCOPE = "all";
118
119    public static final String PROJECT_SCOPE = "project";
120
121    public static final String DEFAULT_SCOPE = ALL_SCOPE;
122
123    /**
124     * Should post processor fail resolution if checksum is missing?
125     *
126     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
127     * @configurationType {@link java.lang.Boolean}
128     * @configurationDefaultValue false
129     */
130    public static final String CONFIG_PROP_FAIL_IF_MISSING = CONFIG_PROPS_PREFIX + "failIfMissing";
131
132    /**
133     * Should post processor process snapshots as well?
134     *
135     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
136     * @configurationType {@link java.lang.Boolean}
137     * @configurationDefaultValue false
138     */
139    public static final String CONFIG_PROP_SNAPSHOTS = CONFIG_PROPS_PREFIX + "snapshots";
140
141    /**
142     * Should post processor go into "record" mode (and collect checksums instead of validate them)?
143     *
144     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
145     * @configurationType {@link java.lang.Boolean}
146     * @configurationDefaultValue false
147     */
148    public static final String CONFIG_PROP_RECORD = CONFIG_PROPS_PREFIX + "record";
149
150    private static final Object CHECKSUM_ALGORITHMS_CACHE_KEY =
151            Keys.of(TrustedChecksumsArtifactResolverPostProcessor.class, "checksumAlgorithms");
152
153    private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;
154
155    private final Map<String, TrustedChecksumsSource> trustedChecksumsSources;
156
157    @Inject
158    public TrustedChecksumsArtifactResolverPostProcessor(
159            ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector,
160            Map<String, TrustedChecksumsSource> trustedChecksumsSources) {
161        this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector);
162        this.trustedChecksumsSources = requireNonNull(trustedChecksumsSources);
163    }
164
165    @Override
166    protected boolean isEnabled(RepositorySystemSession session) {
167        return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED);
168    }
169
170    private boolean inScope(RepositorySystemSession session, ArtifactResult artifactResult) {
171        String scope = ConfigUtils.getString(session, DEFAULT_SCOPE, CONFIG_PROP_SCOPE);
172        if (ALL_SCOPE.equals(scope)) {
173            return artifactResult.isResolved();
174        } else if (PROJECT_SCOPE.equals(scope)) {
175            return artifactResult.isResolved()
176                    && artifactResult.getRequest().getRequestContext().startsWith("project");
177        } else {
178            throw new IllegalArgumentException("Unknown value for configuration " + CONFIG_PROP_SCOPE + ": " + scope);
179        }
180    }
181
182    @SuppressWarnings("unchecked")
183    @Override
184    protected void doPostProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) {
185        final List<ChecksumAlgorithmFactory> checksumAlgorithms = (List<ChecksumAlgorithmFactory>) session.getData()
186                .computeIfAbsent(
187                        CHECKSUM_ALGORITHMS_CACHE_KEY,
188                        () -> checksumAlgorithmFactorySelector.selectList(
189                                ConfigUtils.parseCommaSeparatedUniqueNames(ConfigUtils.getString(
190                                        session, DEFAULT_CHECKSUM_ALGORITHMS, CONFIG_PROP_CHECKSUM_ALGORITHMS))));
191
192        final boolean failIfMissing = ConfigUtils.getBoolean(session, false, CONFIG_PROP_FAIL_IF_MISSING);
193        final boolean record = ConfigUtils.getBoolean(session, false, CONFIG_PROP_RECORD);
194        final boolean snapshots = ConfigUtils.getBoolean(session, false, CONFIG_PROP_SNAPSHOTS);
195
196        for (ArtifactResult artifactResult : artifactResults) {
197            if (artifactResult.getRequest().getArtifact().isSnapshot() && !snapshots) {
198                continue;
199            }
200            if (inScope(session, artifactResult)) {
201                if (record) {
202                    recordArtifactChecksums(session, artifactResult, checksumAlgorithms);
203                } else if (!validateArtifactChecksums(session, artifactResult, checksumAlgorithms, failIfMissing)) {
204                    artifactResult.setArtifact(artifactResult.getArtifact().setPath(null)); // make it unresolved
205                }
206            }
207        }
208    }
209
210    /**
211     * Calculates and records checksums into trusted sources that support writing.
212     */
213    private void recordArtifactChecksums(
214            RepositorySystemSession session,
215            ArtifactResult artifactResult,
216            List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
217        Artifact artifact = artifactResult.getArtifact();
218        ArtifactRepository artifactRepository = artifactResult.getRepository();
219        try {
220            final Map<String, String> calculatedChecksums =
221                    ChecksumAlgorithmHelper.calculate(artifact.getPath(), checksumAlgorithmFactories);
222
223            for (TrustedChecksumsSource trustedChecksumsSource : trustedChecksumsSources.values()) {
224                TrustedChecksumsSource.Writer writer =
225                        trustedChecksumsSource.getTrustedArtifactChecksumsWriter(session);
226                if (writer != null) {
227                    try {
228                        writer.addTrustedArtifactChecksums(
229                                artifact, artifactRepository, checksumAlgorithmFactories, calculatedChecksums);
230                    } catch (IOException e) {
231                        throw new UncheckedIOException(
232                                "Could not write required checksums for " + artifact.getPath(), e);
233                    }
234                }
235            }
236        } catch (IOException e) {
237            throw new UncheckedIOException("Could not calculate required checksums for " + artifact.getPath(), e);
238        }
239    }
240
241    /**
242     * Validates trusted checksums against {@link ArtifactResult}, returns {@code true} denoting "valid" checksums or
243     * {@code false} denoting "invalid" checksums.
244     */
245    private boolean validateArtifactChecksums(
246            RepositorySystemSession session,
247            ArtifactResult artifactResult,
248            List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
249            boolean failIfMissing) {
250        Artifact artifact = artifactResult.getArtifact();
251        ArtifactRepository artifactRepository = artifactResult.getRepository();
252        boolean valid = true;
253        boolean validated = false;
254        try {
255            // full set: calculate all algorithms we were asked for
256            final Map<String, String> calculatedChecksums =
257                    ChecksumAlgorithmHelper.calculate(artifact.getPath(), checksumAlgorithmFactories);
258
259            for (Map.Entry<String, TrustedChecksumsSource> entry : trustedChecksumsSources.entrySet()) {
260                final String trustedSourceName = entry.getKey();
261                final TrustedChecksumsSource trustedChecksumsSource = entry.getValue();
262
263                // upper bound set: ask source for checksums, ideally same as calculatedChecksums but may be less
264                Map<String, String> trustedChecksums = trustedChecksumsSource.getTrustedArtifactChecksums(
265                        session, artifact, artifactRepository, checksumAlgorithmFactories);
266
267                if (trustedChecksums == null) {
268                    continue; // not enabled
269                }
270                validated = true;
271
272                if (!calculatedChecksums.equals(trustedChecksums)) {
273                    Set<String> missingTrustedAlg = new HashSet<>(calculatedChecksums.keySet());
274                    missingTrustedAlg.removeAll(trustedChecksums.keySet());
275
276                    if (!missingTrustedAlg.isEmpty() && failIfMissing) {
277                        artifactResult.addException(
278                                artifactRepository,
279                                ChecksumFailureException.noneAvailable(
280                                        "Missing from " + trustedSourceName
281                                                + " trusted checksum(s) " + missingTrustedAlg + " for artifact "
282                                                + ArtifactIdUtils.toId(artifact),
283                                        ChecksumPolicy.ChecksumKind.PROVIDED.name()));
284                        valid = false;
285                    }
286
287                    // compare values but only present ones, failIfMissing handled above
288                    // we still want to report all: algX - missing, algY - mismatch, etc
289                    for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
290                        String calculatedChecksum = calculatedChecksums.get(checksumAlgorithmFactory.getName());
291                        String trustedChecksum = trustedChecksums.get(checksumAlgorithmFactory.getName());
292                        if (trustedChecksum != null && !Objects.equals(calculatedChecksum, trustedChecksum)) {
293                            artifactResult.addException(
294                                    artifactRepository,
295                                    ChecksumFailureException.mismatchDetail(
296                                            "Trusted Checksums Source: " + trustedSourceName,
297                                            trustedChecksum,
298                                            ChecksumPolicy.ChecksumKind.PROVIDED.name(),
299                                            calculatedChecksum));
300                            valid = false;
301                        }
302                    }
303                }
304            }
305
306            if (!validated && failIfMissing) {
307                artifactResult.addException(
308                        artifactRepository,
309                        ChecksumFailureException.noneAvailable(
310                                "There are no enabled trusted checksums source(s) to validate against.",
311                                ChecksumPolicy.ChecksumKind.PROVIDED.name()));
312                valid = false;
313            }
314        } catch (IOException e) {
315            throw new UncheckedIOException(e);
316        }
317        return valid;
318    }
319}