/* * Copyright (C) 2021, Thomas Wolf and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.api; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.ServiceUnavailableException; import org.eclipse.jgit.api.errors.WrongObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.GpgSignatureVerifier; import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; import org.eclipse.jgit.lib.GpgSignatureVerifierFactory; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; /** * A command to verify GPG signatures on tags or commits. * * @since 5.11 */ public class VerifySignatureCommand extends GitCommand> { /** * Describes what kind of objects shall be handled by a * {@link VerifySignatureCommand}. */ public enum VerifyMode { /** * Handle any object type, ignore anything that is not a commit or tag. */ ANY, /** * Handle only commits; throw a {@link WrongObjectTypeException} for * anything else. */ COMMITS, /** * Handle only tags; throw a {@link WrongObjectTypeException} for * anything else. */ TAGS } private final Set namesToCheck = new HashSet<>(); private VerifyMode mode = VerifyMode.ANY; private GpgSignatureVerifier verifier; private GpgConfig config; private boolean ownVerifier; /** * Creates a new {@link VerifySignatureCommand} for the given {@link Repository}. * * @param repo * to operate on */ public VerifySignatureCommand(Repository repo) { super(repo); } /** * Add a name of an object (SHA-1, ref name; anything that can be * {@link Repository#resolve(String) resolved}) to the command to have its * signature verified. * * @param name * to add * @return {@code this} */ public VerifySignatureCommand addName(String name) { checkCallable(); namesToCheck.add(name); return this; } /** * Add names of objects (SHA-1, ref name; anything that can be * {@link Repository#resolve(String) resolved}) to the command to have their * signatures verified. * * @param names * to add; duplicates will be ignored * @return {@code this} */ public VerifySignatureCommand addNames(String... names) { checkCallable(); namesToCheck.addAll(Arrays.asList(names)); return this; } /** * Add names of objects (SHA-1, ref name; anything that can be * {@link Repository#resolve(String) resolved}) to the command to have their * signatures verified. * * @param names * to add; duplicates will be ignored * @return {@code this} */ public VerifySignatureCommand addNames(Collection names) { checkCallable(); namesToCheck.addAll(names); return this; } /** * Sets the mode of operation for this command. * * @param mode * the {@link VerifyMode} to set * @return {@code this} */ public VerifySignatureCommand setMode(@NonNull VerifyMode mode) { checkCallable(); this.mode = mode; return this; } /** * Sets the {@link GpgSignatureVerifier} to use. * * @param verifier * the {@link GpgSignatureVerifier} to use, or {@code null} to * use the default verifier * @return {@code this} */ public VerifySignatureCommand setVerifier(GpgSignatureVerifier verifier) { checkCallable(); this.verifier = verifier; return this; } /** * Sets an external {@link GpgConfig} to use. Whether it will be used it at * the discretion of the {@link #setVerifier(GpgSignatureVerifier)}. * * @param config * to set; if {@code null}, the config will be loaded from the * git config of the repository * @return {@code this} * @since 5.11 */ public VerifySignatureCommand setGpgConfig(GpgConfig config) { checkCallable(); this.config = config; return this; } /** * Retrieves the currently set {@link GpgSignatureVerifier}. Can be used * after a successful {@link #call()} to get the verifier that was used. * * @return the {@link GpgSignatureVerifier} */ public GpgSignatureVerifier getVerifier() { return verifier; } /** * {@link Repository#resolve(String) Resolves} all names added to the * command to git objects and verifies their signature. Non-existing objects * are ignored. *

* Depending on the {@link #setMode(VerifyMode)}, only tags or commits or * any kind of objects are allowed. *

*

* Unsigned objects are silently skipped. *

* * @return a map of the given names to the corresponding * {@link VerificationResult}, excluding ignored or skipped objects. * @throws ServiceUnavailableException * if no {@link GpgSignatureVerifier} was set and no * {@link GpgSignatureVerifierFactory} is available * @throws WrongObjectTypeException * if a name resolves to an object of a type not allowed by the * {@link #setMode(VerifyMode)} mode */ @Override @NonNull public Map call() throws ServiceUnavailableException, WrongObjectTypeException { checkCallable(); setCallable(false); Map result = new HashMap<>(); if (verifier == null) { GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory .getDefault(); if (factory == null) { throw new ServiceUnavailableException( JGitText.get().signatureVerificationUnavailable); } verifier = factory.getVerifier(); ownVerifier = true; } if (config == null) { config = new GpgConfig(repo.getConfig()); } try (RevWalk walk = new RevWalk(repo)) { for (String toCheck : namesToCheck) { ObjectId id = repo.resolve(toCheck); if (id != null && !ObjectId.zeroId().equals(id)) { RevObject object; try { object = walk.parseAny(id); } catch (MissingObjectException e) { continue; } VerificationResult verification = verifyOne(object); if (verification != null) { result.put(toCheck, verification); } } } } catch (IOException e) { throw new JGitInternalException( JGitText.get().signatureVerificationError, e); } finally { if (ownVerifier) { verifier.clear(); } } return result; } private VerificationResult verifyOne(RevObject object) throws WrongObjectTypeException, IOException { int type = object.getType(); if (VerifyMode.TAGS.equals(mode) && type != Constants.OBJ_TAG) { throw new WrongObjectTypeException(object, Constants.OBJ_TAG); } else if (VerifyMode.COMMITS.equals(mode) && type != Constants.OBJ_COMMIT) { throw new WrongObjectTypeException(object, Constants.OBJ_COMMIT); } if (type == Constants.OBJ_COMMIT || type == Constants.OBJ_TAG) { try { GpgSignatureVerifier.SignatureVerification verification = verifier .verifySignature(object, config); if (verification == null) { // Not signed return null; } // Create new result return new Result(object, verification, null); } catch (JGitInternalException e) { return new Result(object, null, e); } } return null; } private static class Result implements VerificationResult { private final Throwable throwable; private final SignatureVerification verification; private final RevObject object; public Result(RevObject object, SignatureVerification verification, Throwable throwable) { this.object = object; this.verification = verification; this.throwable = throwable; } @Override public Throwable getException() { return throwable; } @Override public SignatureVerification getVerification() { return verification; } @Override public RevObject getObject() { return object; } } }