blob: 6a2a44ea2db87ecf154a9b4145cd376cda378a51 [file] [log] [blame]
/*
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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<Map<String, VerificationResult>> {
/**
* 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<String> 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<String> 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.
* <p>
* Depending on the {@link #setMode(VerifyMode)}, only tags or commits or
* any kind of objects are allowed.
* </p>
* <p>
* Unsigned objects are silently skipped.
* </p>
*
* @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<String, VerificationResult> call()
throws ServiceUnavailableException, WrongObjectTypeException {
checkCallable();
setCallable(false);
Map<String, VerificationResult> 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;
}
}
}