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}