1 /* 2 * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others 3 * 4 * This program and the accompanying materials are made available under the 5 * terms of the Eclipse Distribution License v. 1.0 which is available at 6 * https://www.eclipse.org/org/documents/edl-v10.php. 7 * 8 * SPDX-License-Identifier: BSD-3-Clause 9 */ 10 package org.eclipse.jgit.api; 11 12 import java.io.IOException; 13 import java.util.Arrays; 14 import java.util.Collection; 15 import java.util.HashMap; 16 import java.util.HashSet; 17 import java.util.Map; 18 import java.util.Set; 19 20 import org.eclipse.jgit.annotations.NonNull; 21 import org.eclipse.jgit.api.errors.JGitInternalException; 22 import org.eclipse.jgit.api.errors.ServiceUnavailableException; 23 import org.eclipse.jgit.api.errors.WrongObjectTypeException; 24 import org.eclipse.jgit.errors.MissingObjectException; 25 import org.eclipse.jgit.internal.JGitText; 26 import org.eclipse.jgit.lib.Constants; 27 import org.eclipse.jgit.lib.GpgConfig; 28 import org.eclipse.jgit.lib.GpgSignatureVerifier; 29 import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; 30 import org.eclipse.jgit.lib.GpgSignatureVerifierFactory; 31 import org.eclipse.jgit.lib.ObjectId; 32 import org.eclipse.jgit.lib.Repository; 33 import org.eclipse.jgit.revwalk.RevObject; 34 import org.eclipse.jgit.revwalk.RevWalk; 35 36 /** 37 * A command to verify GPG signatures on tags or commits. 38 * 39 * @since 5.11 40 */ 41 public class VerifySignatureCommand extends GitCommand<Map<String, VerificationResult>> { 42 43 /** 44 * Describes what kind of objects shall be handled by a 45 * {@link VerifySignatureCommand}. 46 */ 47 public enum VerifyMode { 48 /** 49 * Handle any object type, ignore anything that is not a commit or tag. 50 */ 51 ANY, 52 /** 53 * Handle only commits; throw a {@link WrongObjectTypeException} for 54 * anything else. 55 */ 56 COMMITS, 57 /** 58 * Handle only tags; throw a {@link WrongObjectTypeException} for 59 * anything else. 60 */ 61 TAGS 62 } 63 64 private final Set<String> namesToCheck = new HashSet<>(); 65 66 private VerifyMode mode = VerifyMode.ANY; 67 68 private GpgSignatureVerifier verifier; 69 70 private GpgConfig config; 71 72 private boolean ownVerifier; 73 74 /** 75 * Creates a new {@link VerifySignatureCommand} for the given {@link Repository}. 76 * 77 * @param repo 78 * to operate on 79 */ VerifySignatureCommand(Repository repo)80 public VerifySignatureCommand(Repository repo) { 81 super(repo); 82 } 83 84 /** 85 * Add a name of an object (SHA-1, ref name; anything that can be 86 * {@link Repository#resolve(String) resolved}) to the command to have its 87 * signature verified. 88 * 89 * @param name 90 * to add 91 * @return {@code this} 92 */ addName(String name)93 public VerifySignatureCommand addName(String name) { 94 checkCallable(); 95 namesToCheck.add(name); 96 return this; 97 } 98 99 /** 100 * Add names of objects (SHA-1, ref name; anything that can be 101 * {@link Repository#resolve(String) resolved}) to the command to have their 102 * signatures verified. 103 * 104 * @param names 105 * to add; duplicates will be ignored 106 * @return {@code this} 107 */ addNames(String... names)108 public VerifySignatureCommand addNames(String... names) { 109 checkCallable(); 110 namesToCheck.addAll(Arrays.asList(names)); 111 return this; 112 } 113 114 /** 115 * Add names of objects (SHA-1, ref name; anything that can be 116 * {@link Repository#resolve(String) resolved}) to the command to have their 117 * signatures verified. 118 * 119 * @param names 120 * to add; duplicates will be ignored 121 * @return {@code this} 122 */ addNames(Collection<String> names)123 public VerifySignatureCommand addNames(Collection<String> names) { 124 checkCallable(); 125 namesToCheck.addAll(names); 126 return this; 127 } 128 129 /** 130 * Sets the mode of operation for this command. 131 * 132 * @param mode 133 * the {@link VerifyMode} to set 134 * @return {@code this} 135 */ setMode(@onNull VerifyMode mode)136 public VerifySignatureCommand setMode(@NonNull VerifyMode mode) { 137 checkCallable(); 138 this.mode = mode; 139 return this; 140 } 141 142 /** 143 * Sets the {@link GpgSignatureVerifier} to use. 144 * 145 * @param verifier 146 * the {@link GpgSignatureVerifier} to use, or {@code null} to 147 * use the default verifier 148 * @return {@code this} 149 */ setVerifier(GpgSignatureVerifier verifier)150 public VerifySignatureCommand setVerifier(GpgSignatureVerifier verifier) { 151 checkCallable(); 152 this.verifier = verifier; 153 return this; 154 } 155 156 /** 157 * Sets an external {@link GpgConfig} to use. Whether it will be used it at 158 * the discretion of the {@link #setVerifier(GpgSignatureVerifier)}. 159 * 160 * @param config 161 * to set; if {@code null}, the config will be loaded from the 162 * git config of the repository 163 * @return {@code this} 164 * @since 5.11 165 */ setGpgConfig(GpgConfig config)166 public VerifySignatureCommand setGpgConfig(GpgConfig config) { 167 checkCallable(); 168 this.config = config; 169 return this; 170 } 171 172 /** 173 * Retrieves the currently set {@link GpgSignatureVerifier}. Can be used 174 * after a successful {@link #call()} to get the verifier that was used. 175 * 176 * @return the {@link GpgSignatureVerifier} 177 */ getVerifier()178 public GpgSignatureVerifier getVerifier() { 179 return verifier; 180 } 181 182 /** 183 * {@link Repository#resolve(String) Resolves} all names added to the 184 * command to git objects and verifies their signature. Non-existing objects 185 * are ignored. 186 * <p> 187 * Depending on the {@link #setMode(VerifyMode)}, only tags or commits or 188 * any kind of objects are allowed. 189 * </p> 190 * <p> 191 * Unsigned objects are silently skipped. 192 * </p> 193 * 194 * @return a map of the given names to the corresponding 195 * {@link VerificationResult}, excluding ignored or skipped objects. 196 * @throws ServiceUnavailableException 197 * if no {@link GpgSignatureVerifier} was set and no 198 * {@link GpgSignatureVerifierFactory} is available 199 * @throws WrongObjectTypeException 200 * if a name resolves to an object of a type not allowed by the 201 * {@link #setMode(VerifyMode)} mode 202 */ 203 @Override 204 @NonNull call()205 public Map<String, VerificationResult> call() 206 throws ServiceUnavailableException, WrongObjectTypeException { 207 checkCallable(); 208 setCallable(false); 209 Map<String, VerificationResult> result = new HashMap<>(); 210 if (verifier == null) { 211 GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory 212 .getDefault(); 213 if (factory == null) { 214 throw new ServiceUnavailableException( 215 JGitText.get().signatureVerificationUnavailable); 216 } 217 verifier = factory.getVerifier(); 218 ownVerifier = true; 219 } 220 if (config == null) { 221 config = new GpgConfig(repo.getConfig()); 222 } 223 try (RevWalk walk = new RevWalk(repo)) { 224 for (String toCheck : namesToCheck) { 225 ObjectId id = repo.resolve(toCheck); 226 if (id != null && !ObjectId.zeroId().equals(id)) { 227 RevObject object; 228 try { 229 object = walk.parseAny(id); 230 } catch (MissingObjectException e) { 231 continue; 232 } 233 VerificationResult verification = verifyOne(object); 234 if (verification != null) { 235 result.put(toCheck, verification); 236 } 237 } 238 } 239 } catch (IOException e) { 240 throw new JGitInternalException( 241 JGitText.get().signatureVerificationError, e); 242 } finally { 243 if (ownVerifier) { 244 verifier.clear(); 245 } 246 } 247 return result; 248 } 249 verifyOne(RevObject object)250 private VerificationResult verifyOne(RevObject object) 251 throws WrongObjectTypeException, IOException { 252 int type = object.getType(); 253 if (VerifyMode.TAGS.equals(mode) && type != Constants.OBJ_TAG) { 254 throw new WrongObjectTypeException(object, Constants.OBJ_TAG); 255 } else if (VerifyMode.COMMITS.equals(mode) 256 && type != Constants.OBJ_COMMIT) { 257 throw new WrongObjectTypeException(object, Constants.OBJ_COMMIT); 258 } 259 if (type == Constants.OBJ_COMMIT || type == Constants.OBJ_TAG) { 260 try { 261 GpgSignatureVerifier.SignatureVerification verification = verifier 262 .verifySignature(object, config); 263 if (verification == null) { 264 // Not signed 265 return null; 266 } 267 // Create new result 268 return new Result(object, verification, null); 269 } catch (JGitInternalException e) { 270 return new Result(object, null, e); 271 } 272 } 273 return null; 274 } 275 276 private static class Result implements VerificationResult { 277 278 private final Throwable throwable; 279 280 private final SignatureVerification verification; 281 282 private final RevObject object; 283 Result(RevObject object, SignatureVerification verification, Throwable throwable)284 public Result(RevObject object, SignatureVerification verification, 285 Throwable throwable) { 286 this.object = object; 287 this.verification = verification; 288 this.throwable = throwable; 289 } 290 291 @Override getException()292 public Throwable getException() { 293 return throwable; 294 } 295 296 @Override getVerification()297 public SignatureVerification getVerification() { 298 return verification; 299 } 300 301 @Override getObject()302 public RevObject getObject() { 303 return object; 304 } 305 306 } 307 } 308