diff options
author | Thomas Wolf <thomas.wolf@paranor.ch> | 2021-01-07 17:11:57 +0100 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2021-02-16 00:37:00 +0100 |
commit | 3774fcc848da7526ffa74211cbb2781df5731125 (patch) | |
tree | 71aee433ac3a5b1c8efa2de628de7dd4560c4a5d /org.eclipse.jgit/src/org/eclipse/jgit/api | |
parent | 15a38e5b4f79792c8ce85c8eddd567c32350de74 (diff) | |
download | jgit-3774fcc848da7526ffa74211cbb2781df5731125.tar.gz jgit-3774fcc848da7526ffa74211cbb2781df5731125.zip |
GPG signature verification via BouncyCastle
Add a GpgSignatureVerifier interface, plus a factory to create
instances thereof that is provided via the ServiceLoader mechanism.
Implement the new interface for BouncyCastle. A verifier maintains
an internal LRU cache of previously found public keys to speed up
verifying multiple objects (tag or commits). Mergetags are not handled.
Provide a new VerifySignatureCommand in org.eclipse.jgit.api together
with a factory method Git.verifySignature(). The command can verify
signatures on tags or commits, and can be limited to accept only tags
or commits. Provide a new public WrongObjectTypeException thrown when
the command is limited to either tags or commits and a name resolves
to some other object kind.
In jgit.pgm, implement "git tag -v", "git log --show-signature", and
"git show --show-signature". The output is similar to command-line
gpg invoked via git, but not identical. In particular, lines are not
prefixed by "gpg:" but by "bc:".
Trust levels for public keys are read from the keys' trust packets,
not from GPG's internal trust database. A trust packet may or may
not be set. Command-line GPG produces more warning lines depending
on the trust level, warning about keys with a trust level below
"full".
There are no unit tests because JGit still doesn't have any setup to
do signing unit tests; this would require at least a faked .gpg
directory with pre-created key rings and keys, and a way to make the
BouncyCastle classes use that directory instead of the default. See
bug 547538 and also bug 544847.
Tested manually with a small test repository containing signed and
unsigned commits and tags, with signatures made with different keys
and made by command-line git using GPG 2.2.25 and by JGit using
BouncyCastle 1.65.
Bug: 547751
Change-Id: If7e34aeed6ca6636a92bf774d893d98f6d459181
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/api')
4 files changed, 429 insertions, 1 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index 64314772b7..3b3e10e7b2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> - * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others + * Copyright (C) 2010, 2021 Chris Aniszczyk <caniszczyk@gmail.com> 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 @@ -773,6 +773,16 @@ public class Git implements AutoCloseable { } /** + * Return a command to verify signatures of tags or commits. + * + * @return a {@link VerifySignatureCommand} + * @since 5.11 + */ + public VerifySignatureCommand verifySignature() { + return new VerifySignatureCommand(repo); + } + + /** * Get repository * * @return the git repository this class is interacting with; see diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java new file mode 100644 index 0000000000..21cddf75b7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java @@ -0,0 +1,46 @@ +/* + * 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 org.eclipse.jgit.lib.GpgSignatureVerifier; +import org.eclipse.jgit.revwalk.RevObject; + +/** + * A {@code VerificationResult} describes the outcome of a signature + * verification. + * + * @see VerifySignatureCommand + * + * @since 5.11 + */ +public interface VerificationResult { + + /** + * If an error occurred during signature verification, this retrieves the + * exception. + * + * @return the exception, or {@code null} if none occurred + */ + Throwable getException(); + + /** + * Retrieves the signature verification result. + * + * @return the result, or {@code null} if none was computed + */ + GpgSignatureVerifier.SignatureVerification getVerification(); + + /** + * Retrieves the git object of which the signature was verified. + * + * @return the git object + */ + RevObject getObject(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java new file mode 100644 index 0000000000..6a2a44ea2d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java @@ -0,0 +1,307 @@ +/* + * 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; + } + + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/WrongObjectTypeException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/WrongObjectTypeException.java new file mode 100644 index 0000000000..f639c2f838 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/WrongObjectTypeException.java @@ -0,0 +1,65 @@ +/* + * 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.errors; + +import java.text.MessageFormat; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; + +/** + * A given object is not of an expected object type. + * + * @since 5.11 + */ +public class WrongObjectTypeException extends GitAPIException { + + private static final long serialVersionUID = 1L; + + private String name; + + private int type; + + /** + * Construct a {@link WrongObjectTypeException} for the specified object id, + * giving the expected type. + * + * @param id + * {@link ObjectId} of the object with the unexpected type + * @param type + * expected object type code; see + * {@link Constants}{@code .OBJ_*}. + */ + public WrongObjectTypeException(ObjectId id, int type) { + super(MessageFormat.format(JGitText.get().objectIsNotA, id.name(), + Constants.typeString(type))); + this.name = id.name(); + this.type = type; + } + + /** + * Retrieves the name (SHA-1) of the object. + * + * @return the name + */ + public String getObjectId() { + return name; + } + + /** + * Retrieves the expected type code. See {@link Constants}{@code .OBJ_*}. + * + * @return the type code + */ + public int getExpectedType() { + return type; + } +} |