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;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
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 CONF_NAME_CHECKSUM_ALGORITHMS = "checksumAlgorithms";
083
084    private static final String DEFAULT_CHECKSUM_ALGORITHMS = "SHA-1";
085
086    private static final String CONF_NAME_FAIL_IF_MISSING = "failIfMissing";
087
088    private static final String CONF_NAME_SNAPSHOTS = "snapshots";
089
090    private static final String CONF_NAME_RECORD = "record";
091
092    private static final String CHECKSUM_ALGORITHMS_CACHE_KEY =
093            TrustedChecksumsArtifactResolverPostProcessor.class.getName() + ".checksumAlgorithms";
094
095    private static final Logger LOGGER = LoggerFactory.getLogger(TrustedChecksumsArtifactResolverPostProcessor.class);
096
097    private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;
098
099    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}