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}