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