View Javadoc
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  	 */
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  	 */
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 	 */
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 	 */
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 	 */
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 	 */
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 	 */
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 	 */
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
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 
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 
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
292 		public Throwable getException() {
293 			return throwable;
294 		}
295 
296 		@Override
297 		public SignatureVerification getVerification() {
298 			return verification;
299 		}
300 
301 		@Override
302 		public RevObject getObject() {
303 			return object;
304 		}
305 
306 	}
307 }