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 CONF_NAME_CHECKSUM_ALGORITHMS = "checksumAlgorithms"; 081 082 private static final String DEFAULT_CHECKSUM_ALGORITHMS = "SHA-1"; 083 084 private static final String CONF_NAME_FAIL_IF_MISSING = "failIfMissing"; 085 086 private static final String CONF_NAME_SNAPSHOTS = "snapshots"; 087 /** 088 * The scope to apply during post-processing. Accepted values are {@code all} (is default and is what happened 089 * before), and {@code project} when the scope of verification are project dependencies only (i.e. plugins are 090 * not verified). 091 * 092 * @since 1.9.25 093 */ 094 public static final String CONFIG_PROP_SCOPE = "scope"; 095 096 public static final String ALL_SCOPE = "all"; 097 098 public static final String PROJECT_SCOPE = "project"; 099 100 public static final String DEFAULT_SCOPE = ALL_SCOPE; 101 102 private static final String CONF_NAME_RECORD = "record"; 103 104 private static final String CHECKSUM_ALGORITHMS_CACHE_KEY = 105 TrustedChecksumsArtifactResolverPostProcessor.class.getName() + ".checksumAlgorithms"; 106 107 private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector; 108 109 private final Map<String, TrustedChecksumsSource> trustedChecksumsSources; 110 111 @Inject 112 public TrustedChecksumsArtifactResolverPostProcessor( 113 ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector, 114 Map<String, TrustedChecksumsSource> trustedChecksumsSources) { 115 super(NAME); 116 this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector); 117 this.trustedChecksumsSources = requireNonNull(trustedChecksumsSources); 118 } 119 120 private boolean inScope(RepositorySystemSession session, ArtifactResult artifactResult) { 121 String scope = ConfigUtils.getString(session, DEFAULT_SCOPE, configPropKey(CONFIG_PROP_SCOPE)); 122 if (ALL_SCOPE.equals(scope)) { 123 return artifactResult.isResolved(); 124 } else if (PROJECT_SCOPE.equals(scope)) { 125 return artifactResult.isResolved() 126 && artifactResult.getRequest().getRequestContext().startsWith("project"); 127 } else { 128 throw new IllegalArgumentException("Unknown value for configuration " + CONFIG_PROP_SCOPE + ": " + scope); 129 } 130 } 131 132 @SuppressWarnings("unchecked") 133 @Override 134 protected void doPostProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) { 135 final List<ChecksumAlgorithmFactory> checksumAlgorithms = (List<ChecksumAlgorithmFactory>) session.getData() 136 .computeIfAbsent( 137 CHECKSUM_ALGORITHMS_CACHE_KEY, 138 () -> checksumAlgorithmFactorySelector.selectList( 139 ConfigUtils.parseCommaSeparatedUniqueNames(ConfigUtils.getString( 140 session, 141 DEFAULT_CHECKSUM_ALGORITHMS, 142 configPropKey(CONF_NAME_CHECKSUM_ALGORITHMS))))); 143 144 final boolean failIfMissing = ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_FAIL_IF_MISSING)); 145 final boolean record = ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_RECORD)); 146 final boolean snapshots = ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_SNAPSHOTS)); 147 148 for (ArtifactResult artifactResult : artifactResults) { 149 if (artifactResult.getRequest().getArtifact().isSnapshot() && !snapshots) { 150 continue; 151 } 152 if (inScope(session, artifactResult)) { 153 if (record) { 154 recordArtifactChecksums(session, artifactResult, checksumAlgorithms); 155 } else if (!validateArtifactChecksums(session, artifactResult, checksumAlgorithms, failIfMissing)) { 156 artifactResult.setArtifact(artifactResult.getArtifact().setFile(null)); // make it unresolved 157 } 158 } 159 } 160 } 161 162 /** 163 * Calculates and records checksums into trusted sources that support writing. 164 */ 165 private void recordArtifactChecksums( 166 RepositorySystemSession session, 167 ArtifactResult artifactResult, 168 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) { 169 Artifact artifact = artifactResult.getArtifact(); 170 ArtifactRepository artifactRepository = artifactResult.getRepository(); 171 try { 172 final Map<String, String> calculatedChecksums = 173 ChecksumAlgorithmHelper.calculate(artifact.getFile(), checksumAlgorithmFactories); 174 175 for (TrustedChecksumsSource trustedChecksumsSource : trustedChecksumsSources.values()) { 176 TrustedChecksumsSource.Writer writer = 177 trustedChecksumsSource.getTrustedArtifactChecksumsWriter(session); 178 if (writer != null) { 179 try { 180 writer.addTrustedArtifactChecksums( 181 artifact, artifactRepository, checksumAlgorithmFactories, calculatedChecksums); 182 } catch (IOException e) { 183 throw new UncheckedIOException( 184 "Could not write required checksums for " + artifact.getFile(), e); 185 } 186 } 187 } 188 } catch (IOException e) { 189 throw new UncheckedIOException("Could not calculate required checksums for " + artifact.getFile(), e); 190 } 191 } 192 193 /** 194 * Validates trusted checksums against {@link ArtifactResult}, returns {@code true} denoting "valid" checksums or 195 * {@code false} denoting "invalid" checksums. 196 */ 197 private boolean validateArtifactChecksums( 198 RepositorySystemSession session, 199 ArtifactResult artifactResult, 200 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories, 201 boolean failIfMissing) { 202 Artifact artifact = artifactResult.getArtifact(); 203 ArtifactRepository artifactRepository = artifactResult.getRepository(); 204 boolean valid = true; 205 boolean validated = false; 206 try { 207 // full set: calculate all algorithms we were asked for 208 final Map<String, String> calculatedChecksums = 209 ChecksumAlgorithmHelper.calculate(artifact.getFile(), checksumAlgorithmFactories); 210 211 for (Map.Entry<String, TrustedChecksumsSource> entry : trustedChecksumsSources.entrySet()) { 212 final String trustedSourceName = entry.getKey(); 213 final TrustedChecksumsSource trustedChecksumsSource = entry.getValue(); 214 215 // upper bound set: ask source for checksums, ideally same as calculatedChecksums but may be less 216 Map<String, String> trustedChecksums = trustedChecksumsSource.getTrustedArtifactChecksums( 217 session, artifact, artifactRepository, checksumAlgorithmFactories); 218 219 if (trustedChecksums == null) { 220 continue; // not enabled 221 } 222 validated = true; 223 224 if (!calculatedChecksums.equals(trustedChecksums)) { 225 Set<String> missingTrustedAlg = new HashSet<>(calculatedChecksums.keySet()); 226 missingTrustedAlg.removeAll(trustedChecksums.keySet()); 227 228 if (!missingTrustedAlg.isEmpty() && failIfMissing) { 229 artifactResult.addException(new ChecksumFailureException("Missing from " + trustedSourceName 230 + " trusted checksum(s) " + missingTrustedAlg + " for artifact " 231 + ArtifactIdUtils.toId(artifact))); 232 valid = false; 233 } 234 235 // compare values but only present ones, failIfMissing handled above 236 // we still want to report all: algX - missing, algY - mismatch, etc 237 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) { 238 String calculatedChecksum = calculatedChecksums.get(checksumAlgorithmFactory.getName()); 239 String trustedChecksum = trustedChecksums.get(checksumAlgorithmFactory.getName()); 240 if (trustedChecksum != null && !Objects.equals(calculatedChecksum, trustedChecksum)) { 241 artifactResult.addException(new ChecksumFailureException("Artifact " 242 + ArtifactIdUtils.toId(artifact) + " trusted checksum mismatch: " 243 + trustedSourceName + "=" + trustedChecksum + "; calculated=" 244 + calculatedChecksum)); 245 valid = false; 246 } 247 } 248 } 249 } 250 251 if (!validated && failIfMissing) { 252 artifactResult.addException(new ChecksumFailureException( 253 "There are no enabled trusted checksums" + " source(s) to validate against.")); 254 valid = false; 255 } 256 } catch (IOException e) { 257 throw new UncheckedIOException(e); 258 } 259 return valid; 260 } 261}