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}