summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF2
-rw-r--r--org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory1
-rw-r--r--org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties7
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java16
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java76
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java388
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java28
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java6
-rw-r--r--org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties7
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java44
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java47
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java85
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java3
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java56
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java12
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java46
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java307
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/errors/WrongObjectTypeException.java65
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java158
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java71
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java20
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java86
24 files changed, 1507 insertions, 44 deletions
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index 655dcca803..e5f432dc57 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -9,11 +9,13 @@ Bundle-Localization: plugin
Bundle-Version: 5.11.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
+ org.bouncycastle.bcpg.sig;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp;version="[1.65.0,2.0.0)",
+ org.bouncycastle.openpgp.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
new file mode 100644
index 0000000000..17ab30fba7
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignatureVerifierFactory \ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
index 1441c63e8e..83ed9059ec 100644
--- a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
+++ b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
@@ -8,4 +8,11 @@ gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key:
gpgNotASigningKey=Secret key ({0}) is not suitable for signing
gpgKeyInfo=GPG Key (fingerprint {0})
gpgSigningCancelled=Signing was cancelled
+nonSignatureError=Signature does not decode into a signature object
+signatureInconsistent=Inconsistent signature; key ID {0} does not match issuer fingerprint {1}
+signatureKeyLookupError=Error occurred while looking for public key
+signatureNoKeyInfo=No way to determine a public key from the signature
+signatureNoPublicKey=No public key found to verify the signature
+signatureParseError=Signature cannot be parsed
+signatureVerificationError=Signature verification failed
unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
index 1a00b0fc79..9403ba2352 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
@@ -1,3 +1,12 @@
+/*
+ * Copyright (C) 2018, 2021 Salesforce 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.gpg.bc.internal;
import org.eclipse.jgit.nls.NLS;
@@ -28,6 +37,13 @@ public final class BCText extends TranslationBundle {
/***/ public String gpgNotASigningKey;
/***/ public String gpgKeyInfo;
/***/ public String gpgSigningCancelled;
+ /***/ public String nonSignatureError;
+ /***/ public String signatureInconsistent;
+ /***/ public String signatureKeyLookupError;
+ /***/ public String signatureNoKeyInfo;
+ /***/ public String signatureNoPublicKey;
+ /***/ public String signatureParseError;
+ /***/ public String signatureVerificationError;
/***/ public String unableToSignCommitNoSecretKey;
}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
index 1389f8bd1b..13655c0d55 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2020 Salesforce and others
+ * Copyright (C) 2018, 2021 Salesforce 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
@@ -14,12 +14,14 @@ import static java.nio.file.Files.newInputStream;
import java.io.BufferedInputStream;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
@@ -247,16 +249,32 @@ public class BouncyCastleGpgKeyLocator {
return false;
}
- private String toFingerprint(String keyId) {
+ private static String toFingerprint(String keyId) {
if (keyId.startsWith("0x")) { //$NON-NLS-1$
return keyId.substring(2);
}
return keyId;
}
- private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
+ static PGPPublicKey findPublicKey(String fingerprint, String keySpec)
+ throws IOException, PGPException {
+ PGPPublicKey result = findPublicKeyInPubring(USER_PGP_PUBRING_FILE,
+ fingerprint, keySpec);
+ if (result == null && exists(USER_KEYBOX_PATH)) {
+ try {
+ result = findPublicKeyInKeyBox(USER_KEYBOX_PATH, fingerprint,
+ keySpec);
+ } catch (NoSuchAlgorithmException | NoSuchProviderException
+ | IOException | NoOpenPgpKeyException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return result;
+ }
+
+ private static PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob,
+ String keyId)
throws IOException {
- String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
if (keyId.isEmpty()) {
return null;
}
@@ -270,10 +288,11 @@ public class BouncyCastleGpgKeyLocator {
return null;
}
- private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
+ private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob,
+ String keySpec)
throws IOException {
for (UserID userID : keyBlob.getUserIds()) {
- if (containsSigningKey(userID.getUserIDAsString(), signingKey)) {
+ if (containsSigningKey(userID.getUserIDAsString(), keySpec)) {
return getSigningPublicKey(keyBlob);
}
}
@@ -285,6 +304,10 @@ public class BouncyCastleGpgKeyLocator {
*
* @param keyboxFile
* the KeyBox file
+ * @param keyId
+ * to look for, may be null
+ * @param keySpec
+ * to look for
* @return publicKey the public key (maybe <code>null</code>)
* @throws IOException
* in case of problems reading the file
@@ -293,19 +316,22 @@ public class BouncyCastleGpgKeyLocator {
* @throws NoOpenPgpKeyException
* if the file does not contain any OpenPGP key
*/
- private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
+ private static PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile,
+ String keyId, String keySpec)
throws IOException, NoSuchAlgorithmException,
NoSuchProviderException, NoOpenPgpKeyException {
KeyBox keyBox = readKeyBoxFile(keyboxFile);
+ String id = keyId != null ? keyId
+ : toFingerprint(keySpec).toLowerCase(Locale.ROOT);
boolean hasOpenPgpKey = false;
for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
hasOpenPgpKey = true;
- PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
+ PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id);
if (key != null) {
return key;
}
- key = findPublicKeyByUserId(keyBlob);
+ key = findPublicKeyByUserId(keyBlob, keySpec);
if (key != null) {
return key;
}
@@ -349,7 +375,8 @@ public class BouncyCastleGpgKeyLocator {
// pubring.gpg also try secring.gpg to find the secret key.
if (exists(USER_KEYBOX_PATH)) {
try {
- publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH);
+ publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH, null,
+ signingKey);
if (publicKey != null) {
key = findSecretKeyForKeyBoxPublicKey(publicKey,
USER_KEYBOX_PATH);
@@ -372,7 +399,8 @@ public class BouncyCastleGpgKeyLocator {
}
}
if (exists(USER_PGP_PUBRING_FILE)) {
- publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE);
+ publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE, null,
+ signingKey);
if (publicKey != null) {
// GPG < 2.1 may have both; the agent using the directory
// and gpg using secring.gpg. GPG >= 2.1 delegates all
@@ -562,6 +590,11 @@ public class BouncyCastleGpgKeyLocator {
* Return the first public key matching the key id ({@link #signingKey}.
*
* @param pubringFile
+ * to search
+ * @param keyId
+ * to look for, may be null
+ * @param keySpec
+ * to look for
*
* @return the PGP public key, or {@code null} if none found
* @throws IOException
@@ -569,14 +602,16 @@ public class BouncyCastleGpgKeyLocator {
* @throws PGPException
* on BouncyCastle errors
*/
- private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
+ private static PGPPublicKey findPublicKeyInPubring(Path pubringFile,
+ String keyId, String keySpec)
throws IOException, PGPException {
try (InputStream in = newInputStream(pubringFile)) {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
new BufferedInputStream(in),
new JcaKeyFingerprintCalculator());
- String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
+ String id = keyId != null ? keyId
+ : toFingerprint(keySpec).toLowerCase(Locale.ROOT);
Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
while (keyrings.hasNext()) {
PGPPublicKeyRing keyRing = keyrings.next();
@@ -586,30 +621,33 @@ public class BouncyCastleGpgKeyLocator {
// try key id
String fingerprint = Hex.toHexString(key.getFingerprint())
.toLowerCase(Locale.ROOT);
- if (fingerprint.endsWith(keyId)) {
+ if (fingerprint.endsWith(id)) {
return key;
}
// try user id
Iterator<String> userIDs = key.getUserIDs();
while (userIDs.hasNext()) {
String userId = userIDs.next();
- if (containsSigningKey(userId, signingKey)) {
+ if (containsSigningKey(userId, keySpec)) {
return key;
}
}
}
}
+ } catch (FileNotFoundException | NoSuchFileException e) {
+ // Ignore and return null
}
return null;
}
- private PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
+ private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
throws IOException {
return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing()
.getPublicKey(fingerprint);
}
- private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException {
+ private static PGPPublicKey getSigningPublicKey(KeyBlob blob)
+ throws IOException {
PGPPublicKey masterKey = null;
Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob)
.getPGPPublicKeyRing().getPublicKeys();
@@ -629,7 +667,7 @@ public class BouncyCastleGpgKeyLocator {
return masterKey;
}
- private boolean isSigningKey(PGPPublicKey key) {
+ private static boolean isSigningKey(PGPPublicKey key) {
Iterator signatures = key.getSignatures();
while (signatures.hasNext()) {
PGPSignature sig = (PGPSignature) signatures.next();
@@ -641,7 +679,7 @@ public class BouncyCastleGpgKeyLocator {
return false;
}
- private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
+ private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
NoSuchAlgorithmException, NoSuchProviderException,
NoOpenPgpKeyException {
if (keyboxFile.toFile().length() == 0) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java
new file mode 100644
index 0000000000..7161895a6b
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java
@@ -0,0 +1,388 @@
+/*
+ * 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.gpg.bc.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Security;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Locale;
+
+import org.bouncycastle.bcpg.sig.IssuerFingerprint;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.util.LRUMap;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
+ */
+public class BouncyCastleGpgSignatureVerifier implements GpgSignatureVerifier {
+
+ private static void registerBouncyCastleProviderIfNecessary() {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ }
+
+ /**
+ * Creates a new instance and registers the BouncyCastle security provider
+ * if needed.
+ */
+ public BouncyCastleGpgSignatureVerifier() {
+ registerBouncyCastleProviderIfNecessary();
+ }
+
+ // To support more efficient signature verification of multiple objects we
+ // cache public keys once found in a LRU cache.
+
+ private static final Object NO_KEY = new Object();
+
+ private LRUMap<String, Object> byFingerprint = new LRUMap<>(16, 200);
+
+ private LRUMap<String, Object> bySigner = new LRUMap<>(16, 200);
+
+ @Override
+ public String getName() {
+ return "bc"; //$NON-NLS-1$
+ }
+
+ @Override
+ @Nullable
+ public SignatureVerification verifySignature(@NonNull RevObject object,
+ @NonNull GpgConfig config) throws IOException {
+ if (object instanceof RevCommit) {
+ RevCommit commit = (RevCommit) object;
+ byte[] signatureData = commit.getRawGpgSignature();
+ if (signatureData == null) {
+ return null;
+ }
+ byte[] raw = commit.getRawBuffer();
+ // Now remove the GPG signature
+ byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
+ int start = RawParseUtils.headerStart(header, raw, 0);
+ if (start < 0) {
+ return null;
+ }
+ int end = RawParseUtils.headerEnd(raw, start);
+ // start is at the beginning of the header's content
+ start -= header.length + 1;
+ // end is on the terminating LF; we need to skip that, too
+ if (end < raw.length) {
+ end++;
+ }
+ byte[] data = new byte[raw.length - (end - start)];
+ System.arraycopy(raw, 0, data, 0, start);
+ System.arraycopy(raw, end, data, start, raw.length - end);
+ return verify(data, signatureData);
+ } else if (object instanceof RevTag) {
+ RevTag tag = (RevTag) object;
+ byte[] signatureData = tag.getRawGpgSignature();
+ if (signatureData == null) {
+ return null;
+ }
+ byte[] raw = tag.getRawBuffer();
+ // The signature is just tacked onto the end of the message, which
+ // is last in the buffer.
+ byte[] data = Arrays.copyOfRange(raw, 0,
+ raw.length - signatureData.length);
+ return verify(data, signatureData);
+ }
+ return null;
+ }
+
+ static PGPSignature parseSignature(InputStream in)
+ throws IOException, PGPException {
+ try (InputStream sigIn = PGPUtil.getDecoderStream(in)) {
+ JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(sigIn);
+ Object obj = pgpFactory.nextObject();
+ if (obj instanceof PGPCompressedData) {
+ obj = new JcaPGPObjectFactory(
+ ((PGPCompressedData) obj).getDataStream()).nextObject();
+ }
+ if (obj instanceof PGPSignatureList) {
+ return ((PGPSignatureList) obj).get(0);
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public SignatureVerification verify(byte[] data, byte[] signatureData)
+ throws IOException {
+ PGPSignature signature = null;
+ String fingerprint = null;
+ String signer = null;
+ String keyId = null;
+ try (InputStream sigIn = new ByteArrayInputStream(signatureData)) {
+ signature = parseSignature(sigIn);
+ if (signature != null) {
+ // Try to figure out something to find the public key with.
+ if (signature.hasSubpackets()) {
+ PGPSignatureSubpacketVector packets = signature
+ .getHashedSubPackets();
+ IssuerFingerprint fingerprintPacket = packets
+ .getIssuerFingerprint();
+ if (fingerprintPacket != null) {
+ fingerprint = Hex
+ .toHexString(fingerprintPacket.getFingerprint())
+ .toLowerCase(Locale.ROOT);
+ }
+ signer = packets.getSignerUserID();
+ if (signer != null) {
+ signer = BouncyCastleGpgSigner.extractSignerId(signer);
+ }
+ }
+ keyId = Long.toUnsignedString(signature.getKeyID(), 16)
+ .toLowerCase(Locale.ROOT);
+ } else {
+ throw new JGitInternalException(BCText.get().nonSignatureError);
+ }
+ } catch (PGPException e) {
+ throw new JGitInternalException(BCText.get().signatureParseError,
+ e);
+ }
+ Date signatureCreatedAt = signature.getCreationTime();
+ if (fingerprint == null && signer == null && keyId == null) {
+ return new VerificationResult(signatureCreatedAt, null, null, null,
+ false, false, TrustLevel.UNKNOWN,
+ BCText.get().signatureNoKeyInfo);
+ }
+ if (fingerprint != null && keyId != null
+ && !fingerprint.endsWith(keyId)) {
+ return new VerificationResult(signatureCreatedAt, signer, fingerprint,
+ null, false, false, TrustLevel.UNKNOWN,
+ MessageFormat.format(BCText.get().signatureInconsistent,
+ keyId, fingerprint));
+ }
+ if (fingerprint == null && keyId != null) {
+ fingerprint = keyId;
+ }
+ // Try to find the public key
+ String keySpec = '<' + signer + '>';
+ Object cached = null;
+ PGPPublicKey publicKey = null;
+ try {
+ cached = byFingerprint.get(fingerprint);
+ if (cached != null) {
+ if (cached instanceof PGPPublicKey) {
+ publicKey = (PGPPublicKey) cached;
+ }
+ } else if (!StringUtils.isEmptyOrNull(signer)) {
+ cached = bySigner.get(signer);
+ if (cached != null) {
+ if (cached instanceof PGPPublicKey) {
+ publicKey = (PGPPublicKey) cached;
+ }
+ }
+ }
+ if (cached == null) {
+ publicKey = BouncyCastleGpgKeyLocator.findPublicKey(fingerprint,
+ keySpec);
+ }
+ } catch (IOException | PGPException e) {
+ throw new JGitInternalException(
+ BCText.get().signatureKeyLookupError, e);
+ }
+ if (publicKey == null) {
+ if (cached == null) {
+ byFingerprint.put(fingerprint, NO_KEY);
+ byFingerprint.put(keyId, NO_KEY);
+ if (signer != null) {
+ bySigner.put(signer, NO_KEY);
+ }
+ }
+ return new VerificationResult(signatureCreatedAt, signer,
+ fingerprint, null, false, false, TrustLevel.UNKNOWN,
+ BCText.get().signatureNoPublicKey);
+ }
+ if (cached == null) {
+ byFingerprint.put(fingerprint, publicKey);
+ byFingerprint.put(keyId, publicKey);
+ if (signer != null) {
+ bySigner.put(signer, publicKey);
+ }
+ }
+ String user = null;
+ Iterator<String> userIds = publicKey.getUserIDs();
+ if (!StringUtils.isEmptyOrNull(signer)) {
+ while (userIds.hasNext()) {
+ String userId = userIds.next();
+ if (BouncyCastleGpgKeyLocator.containsSigningKey(userId,
+ keySpec)) {
+ user = userId;
+ break;
+ }
+ }
+ }
+ if (user == null) {
+ userIds = publicKey.getUserIDs();
+ if (userIds.hasNext()) {
+ user = userIds.next();
+ }
+ }
+ boolean expired = false;
+ long validFor = publicKey.getValidSeconds();
+ if (validFor > 0 && signatureCreatedAt != null) {
+ Instant expiredAt = publicKey.getCreationTime().toInstant()
+ .plusSeconds(validFor);
+ expired = expiredAt.isBefore(signatureCreatedAt.toInstant());
+ }
+ // Trust data is not defined in OpenPGP; the format is implementation
+ // specific. We don't use the GPG trustdb but simply the trust packet
+ // on the public key, if present. Even if present, it may or may not
+ // be set.
+ byte[] trustData = publicKey.getTrustData();
+ TrustLevel trust = parseGpgTrustPacket(trustData);
+ boolean verified = false;
+ try {
+ signature.init(
+ new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(BouncyCastleProvider.PROVIDER_NAME),
+ publicKey);
+ signature.update(data);
+ verified = signature.verify();
+ } catch (PGPException e) {
+ throw new JGitInternalException(
+ BCText.get().signatureVerificationError, e);
+ }
+ return new VerificationResult(signatureCreatedAt, signer, fingerprint, user,
+ verified, expired, trust, null);
+ }
+
+ private TrustLevel parseGpgTrustPacket(byte[] packet) {
+ if (packet == null || packet.length < 6) {
+ // A GPG trust packet has at least 6 bytes.
+ return TrustLevel.UNKNOWN;
+ }
+ if (packet[2] != 'g' || packet[3] != 'p' || packet[4] != 'g') {
+ // Not a GPG trust packet
+ return TrustLevel.UNKNOWN;
+ }
+ int trust = packet[0] & 0x0F;
+ switch (trust) {
+ case 0: // No determined/set
+ case 1: // Trust expired; i.e., calculation outdated or key expired
+ case 2: // Undefined: not enough information to set
+ return TrustLevel.UNKNOWN;
+ case 3:
+ return TrustLevel.NEVER;
+ case 4:
+ return TrustLevel.MARGINAL;
+ case 5:
+ return TrustLevel.FULL;
+ case 6:
+ return TrustLevel.ULTIMATE;
+ default:
+ return TrustLevel.UNKNOWN;
+ }
+ }
+
+ @Override
+ public void clear() {
+ byFingerprint.clear();
+ bySigner.clear();
+ }
+
+ private static class VerificationResult implements SignatureVerification {
+
+ private final Date creationDate;
+
+ private final String signer;
+
+ private final String keyUser;
+
+ private final String fingerprint;
+
+ private final boolean verified;
+
+ private final boolean expired;
+
+ private final @NonNull TrustLevel trustLevel;
+
+ private final String message;
+
+ public VerificationResult(Date creationDate, String signer,
+ String fingerprint, String user, boolean verified,
+ boolean expired, @NonNull TrustLevel trust, String message) {
+ this.creationDate = creationDate;
+ this.signer = signer;
+ this.fingerprint = fingerprint;
+ this.keyUser = user;
+ this.verified = verified;
+ this.expired = expired;
+ this.trustLevel = trust;
+ this.message = message;
+ }
+
+ @Override
+ public Date getCreationDate() {
+ return creationDate;
+ }
+
+ @Override
+ public String getSigner() {
+ return signer;
+ }
+
+ @Override
+ public String getKeyUser() {
+ return keyUser;
+ }
+
+ @Override
+ public String getKeyFingerprint() {
+ return fingerprint;
+ }
+
+ @Override
+ public boolean isExpired() {
+ return expired;
+ }
+
+ @Override
+ public TrustLevel getTrustLevel() {
+ return trustLevel;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public boolean getVerified() {
+ return verified;
+ }
+ }
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java
new file mode 100644
index 0000000000..ae82b758a6
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java
@@ -0,0 +1,28 @@
+/*
+ * 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.gpg.bc.internal;
+
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
+
+/**
+ * A {@link GpgSignatureVerifierFactory} that creates
+ * {@link GpgSignatureVerifier} instances that verify GPG signatures using
+ * BouncyCastle and that do cache public keys.
+ */
+public final class BouncyCastleGpgSignatureVerifierFactory
+ extends GpgSignatureVerifierFactory {
+
+ @Override
+ public GpgSignatureVerifier getVerifier() {
+ return new BouncyCastleGpgSignatureVerifier();
+ }
+
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
index f448d5e9e8..9f48e5431e 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2020, Salesforce and others
+ * Copyright (C) 2018, 2021, Salesforce 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
@@ -39,9 +39,9 @@ import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgObjectSigner;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
-import org.eclipse.jgit.lib.GpgObjectSigner;
import org.eclipse.jgit.lib.ObjectBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
@@ -210,7 +210,7 @@ public class BouncyCastleGpgSigner extends GpgSigner
}
}
- private String extractSignerId(String pgpUserId) {
+ static String extractSignerId(String pgpUserId) {
int from = pgpUserId.indexOf('<');
if (from >= 0) {
int to = pgpUserId.indexOf('>', from + 1);
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index afa253eeb5..df55eb0776 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -77,14 +77,15 @@ invalidHttpProxyOnlyHttpSupported=Invalid http_proxy: {0}: Only http supported.
invalidRecurseSubmodulesMode=Invalid recurse submodules mode: {0}
invalidUntrackedFilesMode=Invalid untracked files mode ''{0}''
jgitVersion=jgit version {0}
-lineFormat={0}
-listeningOn=Listening on {0}
lfsNoAccessKey=No accessKey in {0}
lfsNoSecretKey=No secretKey in {0}
lfsProtocolUrl=LFS protocol URL: {0}
lfsStoreDirectory=LFS objects stored in: {0}
lfsStoreUrl=LFS store URL: {0}
lfsUnknownStoreType="Unknown LFS store type: {0}"
+lineFormat={0}
+listeningOn=Listening on {0}
+logNoSignatureVerifier="No signature verifier available"
mergeConflict=CONFLICT(content): Merge conflict in {0}
mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge:
mergeFailed=Automatic merge failed; fix conflicts and then commit the result
@@ -411,6 +412,7 @@ usage_show=Display one commit
usage_showRefNamesMatchingCommits=Show ref names matching commits
usage_showPatch=display patch
usage_showNotes=Add this ref to the list of note branches from which notes are displayed
+usage_showSignature=Verify signatures of signed commits in the log
usage_showTimeInMilliseconds=Show mtime in milliseconds
usage_squash=Squash commits as if a real merge happened, but do not make a commit or move the HEAD.
usage_srcPrefix=show the source prefix instead of "a/"
@@ -424,6 +426,7 @@ usage_tagLocalUser=create a signed annotated tag using the specified GPG key ID
usage_tagMessage=create an annotated tag with the given message, unsigned unless -s or -u are given, or config tag.gpgSign is true, or tar.forceSignAnnotated is true and -a is not given
usage_tagSign=create a signed annotated tag
usage_tagNoSign=suppress signing the tag
+usage_tagVerify=Verify the GPG signature
usage_untrackedFilesMode=show untracked files
usage_updateRef=reference to update
usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
index 55efd23c6a..353b64b9be 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2010, Google Inc.
- * Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2006, 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2008, 2021, Shawn O. Pearce <spearce@spearce.org> 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
@@ -31,12 +31,17 @@ import org.eclipse.jgit.diff.RenameDetector;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
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.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.pgm.internal.VerificationUtils;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.util.GitDateFormatter;
@@ -68,6 +73,9 @@ class Log extends RevWalkTextBuiltin {
additionalNoteRefs.add(notesRef);
}
+ @Option(name = "--show-signature", usage = "usage_showSignature")
+ private boolean showSignature;
+
@Option(name = "--date", usage = "usage_date")
void dateFormat(String date) {
if (date.toLowerCase(Locale.ROOT).equals(date))
@@ -147,6 +155,10 @@ class Log extends RevWalkTextBuiltin {
// END -- Options shared with Diff
+ private GpgSignatureVerifier verifier;
+
+ private GpgConfig config;
+
Log() {
dateFormatter = new GitDateFormatter(Format.DEFAULT);
}
@@ -161,6 +173,7 @@ class Log extends RevWalkTextBuiltin {
/** {@inheritDoc} */
@Override
protected void run() {
+ config = new GpgConfig(db.getConfig());
diffFmt.setRepository(db);
try {
diffFmt.setPathFilter(pathFilter);
@@ -197,6 +210,9 @@ class Log extends RevWalkTextBuiltin {
throw die(e.getMessage(), e);
} finally {
diffFmt.close();
+ if (verifier != null) {
+ verifier.clear();
+ }
}
}
@@ -229,6 +245,9 @@ class Log extends RevWalkTextBuiltin {
}
outw.println();
+ if (showSignature) {
+ showSignature(c);
+ }
final PersonIdent author = c.getAuthorIdent();
outw.println(MessageFormat.format(CLIText.get().authorInfo, author.getName(), author.getEmailAddress()));
outw.println(MessageFormat.format(CLIText.get().dateInfo,
@@ -252,6 +271,27 @@ class Log extends RevWalkTextBuiltin {
outw.flush();
}
+ private void showSignature(RevCommit c) throws IOException {
+ if (c.getRawGpgSignature() == null) {
+ return;
+ }
+ if (verifier == null) {
+ GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory
+ .getDefault();
+ if (factory == null) {
+ throw die(CLIText.get().logNoSignatureVerifier, null);
+ }
+ verifier = factory.getVerifier();
+ }
+ SignatureVerification verification = verifier.verifySignature(c,
+ config);
+ if (verification == null) {
+ return;
+ }
+ VerificationUtils.writeVerification(outw, verification,
+ verifier.getName(), c.getCommitterIdent());
+ }
+
/**
* @param c
* @return <code>true</code> if at least one note was printed,
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
index 1d43220ca8..3beab60a8b 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
@@ -29,10 +29,15 @@ import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.pgm.internal.VerificationUtils;
import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
@@ -59,6 +64,9 @@ class Show extends TextBuiltin {
@Option(name = "--", metaVar = "metaVar_path", handler = PathTreeFilterHandler.class)
protected TreeFilter pathFilter = TreeFilter.ALL;
+ @Option(name = "--show-signature", usage = "usage_showSignature")
+ private boolean showSignature;
+
// BEGIN -- Options shared with Diff
@Option(name = "-p", usage = "usage_showPatch")
boolean showPatch;
@@ -220,13 +228,16 @@ class Show extends TextBuiltin {
}
outw.println();
- String[] lines = tag.getFullMessage().split("\n"); //$NON-NLS-1$
- for (String s : lines) {
- outw.println(s);
+ String fullMessage = tag.getFullMessage();
+ if (!fullMessage.isEmpty()) {
+ String[] lines = tag.getFullMessage().split("\n"); //$NON-NLS-1$
+ for (String s : lines) {
+ outw.println(s);
+ }
}
byte[] rawSignature = tag.getRawGpgSignature();
if (rawSignature != null) {
- lines = RawParseUtils.decode(rawSignature).split("\n"); //$NON-NLS-1$
+ String[] lines = RawParseUtils.decode(rawSignature).split("\n"); //$NON-NLS-1$
for (String s : lines) {
outw.println(s);
}
@@ -258,6 +269,10 @@ class Show extends TextBuiltin {
c.getId().copyTo(outbuffer, outw);
outw.println();
+ if (showSignature) {
+ showSignature(c);
+ }
+
final PersonIdent author = c.getAuthorIdent();
outw.println(MessageFormat.format(CLIText.get().authorInfo,
author.getName(), author.getEmailAddress()));
@@ -296,4 +311,28 @@ class Show extends TextBuiltin {
}
outw.println();
}
+
+ private void showSignature(RevCommit c) throws IOException {
+ if (c.getRawGpgSignature() == null) {
+ return;
+ }
+ GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory
+ .getDefault();
+ if (factory == null) {
+ throw die(CLIText.get().logNoSignatureVerifier, null);
+ }
+ GpgSignatureVerifier verifier = factory.getVerifier();
+ GpgConfig config = new GpgConfig(db.getConfig());
+ try {
+ SignatureVerification verification = verifier.verifySignature(c,
+ config);
+ if (verification == null) {
+ return;
+ }
+ VerificationUtils.writeVerification(outw, verification,
+ verifier.getName(), c.getCommitterIdent());
+ } finally {
+ verifier.clear();
+ }
+ }
}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
index 4cc62b339c..e2cd31d198 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
@@ -4,7 +4,7 @@
* Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org>
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg.lists@dewire.com>
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2021 Shawn O. Pearce <spearce@spearce.org> 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
@@ -22,43 +22,60 @@ import java.util.List;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListTagCommand;
import org.eclipse.jgit.api.TagCommand;
+import org.eclipse.jgit.api.VerificationResult;
+import org.eclipse.jgit.api.VerifySignatureCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.pgm.internal.VerificationUtils;
+import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@Command(common = true, usage = "usage_CreateATag")
class Tag extends TextBuiltin {
- @Option(name = "-f", usage = "usage_forceReplacingAnExistingTag")
+
+ @Option(name = "--force", aliases = { "-f" }, forbids = { "--delete",
+ "--verify" }, usage = "usage_forceReplacingAnExistingTag")
private boolean force;
- @Option(name = "-d", usage = "usage_tagDelete")
+ @Option(name = "--delete", aliases = { "-d" }, forbids = {
+ "--verify" }, usage = "usage_tagDelete")
private boolean delete;
@Option(name = "--annotate", aliases = {
- "-a" }, usage = "usage_tagAnnotated")
+ "-a" }, forbids = { "--delete",
+ "--verify" }, usage = "usage_tagAnnotated")
private boolean annotated;
- @Option(name = "-m", metaVar = "metaVar_message", usage = "usage_tagMessage")
+ @Option(name = "-m", forbids = { "--delete",
+ "--verify" }, metaVar = "metaVar_message", usage = "usage_tagMessage")
private String message;
@Option(name = "--sign", aliases = { "-s" }, forbids = {
- "--no-sign" }, usage = "usage_tagSign")
+ "--no-sign", "--delete", "--verify" }, usage = "usage_tagSign")
private boolean sign;
@Option(name = "--no-sign", usage = "usage_tagNoSign", forbids = {
- "--sign" })
+ "--sign", "--delete", "--verify" })
private boolean noSign;
@Option(name = "--local-user", aliases = {
- "-u" }, metaVar = "metaVar_tagLocalUser", usage = "usage_tagLocalUser")
+ "-u" }, forbids = { "--delete",
+ "--verify" }, metaVar = "metaVar_tagLocalUser", usage = "usage_tagLocalUser")
private String gpgKeyId;
+ @Option(name = "--verify", aliases = { "-v" }, forbids = { "--delete",
+ "--force", "--annotate", "-m", "--sign", "--no-sign",
+ "--local-user" }, usage = "usage_tagVerify")
+ private boolean verify;
+
@Argument(index = 0, metaVar = "metaVar_name")
private String tagName;
@@ -70,7 +87,25 @@ class Tag extends TextBuiltin {
protected void run() {
try (Git git = new Git(db)) {
if (tagName != null) {
- if (delete) {
+ if (verify) {
+ VerifySignatureCommand verifySig = git.verifySignature()
+ .setMode(VerifySignatureCommand.VerifyMode.TAGS)
+ .addName(tagName);
+
+ VerificationResult verification = verifySig.call()
+ .get(tagName);
+ if (verification == null) {
+ showUnsigned(git, tagName);
+ } else {
+ Throwable error = verification.getException();
+ if (error != null) {
+ throw die(error.getMessage(), error);
+ }
+ writeVerification(verifySig.getVerifier().getName(),
+ (RevTag) verification.getObject(),
+ verification.getVerification());
+ }
+ } else if (delete) {
List<String> deletedTags = git.tagDelete().setTags(tagName)
.call();
if (deletedTags.isEmpty()) {
@@ -116,4 +151,36 @@ class Tag extends TextBuiltin {
throw die(e.getMessage(), e);
}
}
+
+ private void showUnsigned(Git git, String wantedTag) throws IOException {
+ ObjectId id = git.getRepository().resolve(wantedTag);
+ if (id != null && !ObjectId.zeroId().equals(id)) {
+ try (RevWalk walk = new RevWalk(git.getRepository())) {
+ showTag(walk.parseTag(id));
+ }
+ } else {
+ throw die(
+ MessageFormat.format(CLIText.get().tagNotFound, wantedTag));
+ }
+ }
+
+ private void showTag(RevTag tag) throws IOException {
+ outw.println("object " + tag.getObject().name()); //$NON-NLS-1$
+ outw.println("type " + Constants.typeString(tag.getObject().getType())); //$NON-NLS-1$
+ outw.println("tag " + tag.getTagName()); //$NON-NLS-1$
+ outw.println("tagger " + tag.getTaggerIdent().toExternalString()); //$NON-NLS-1$
+ outw.println();
+ outw.print(tag.getFullMessage());
+ }
+
+ private void writeVerification(String name, RevTag tag,
+ SignatureVerification verification) throws IOException {
+ showTag(tag);
+ if (verification == null) {
+ outw.println();
+ return;
+ }
+ VerificationUtils.writeVerification(outw, verification, name,
+ tag.getTaggerIdent());
+ }
}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index c68019e5d0..991b3ba58a 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com>
- * Copyright (C) 2013, Obeo and others
+ * Copyright (C) 2013, 2021 Obeo 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
@@ -163,6 +163,7 @@ public class CLIText extends TranslationBundle {
/***/ public String lfsUnknownStoreType;
/***/ public String lineFormat;
/***/ public String listeningOn;
+ /***/ public String logNoSignatureVerifier;
/***/ public String mergeCheckoutConflict;
/***/ public String mergeConflict;
/***/ public String mergeFailed;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java
new file mode 100644
index 0000000000..c1f8a86a8c
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.pgm.internal;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.util.GitDateFormatter;
+import org.eclipse.jgit.util.SignatureUtils;
+import org.eclipse.jgit.util.io.ThrowingPrintWriter;
+
+/**
+ * Utilities for signature verification.
+ */
+public final class VerificationUtils {
+
+ private VerificationUtils() {
+ // No instantiation
+ }
+
+ /**
+ * Writes information about a signature verification to the given writer.
+ *
+ * @param out
+ * to write to
+ * @param verification
+ * to show
+ * @param name
+ * of the verifier used
+ * @param creator
+ * of the object verified; used for time zone information
+ * @throws IOException
+ * if writing fails
+ */
+ public static void writeVerification(ThrowingPrintWriter out,
+ SignatureVerification verification, String name,
+ PersonIdent creator) throws IOException {
+ String[] text = SignatureUtils
+ .toString(verification, creator,
+ new GitDateFormatter(GitDateFormatter.Format.LOCALE))
+ .split("\n"); //$NON-NLS-1$
+ for (String line : text) {
+ out.print(name);
+ out.print(": "); //$NON-NLS-1$
+ out.println(line);
+ }
+ }
+}
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index a8b2e563f2..1924621046 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -622,6 +622,8 @@ shortCompressedStreamAt=Short compressed stream at {0}
shortReadOfBlock=Short read of block.
shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section.
shortSkipOfBlock=Short skip of block.
+signatureVerificationError=Signature verification failed
+signatureVerificationUnavailable=No signature verifier registered
signedTagMessageNoLf=A non-empty message of a signed tag must end in LF.
signingServiceUnavailable=Signing service is not available
similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100.
@@ -763,6 +765,13 @@ uriNotFoundWithMessage={0} not found: {1}
URINotSupported=URI not supported: {0}
userConfigInvalid=Git config in the user's home directory {0} is invalid {1}
validatingGitModules=Validating .gitmodules files
+verifySignatureBad=BAD signature from "{0}"
+verifySignatureExpired=Expired signature from "{0}"
+verifySignatureGood=Good signature from "{0}"
+verifySignatureIssuer=issuer "{0}"
+verifySignatureKey=using key {0}
+verifySignatureMade=Signature made {0}
+verifySignatureTrust=[{0}]
walkFailure=Walk failure.
wantNoSpaceWithCapabilities=No space between oid and first capability in first want line
wantNotValid=want {0} not valid
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;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 07fb59ddf3..aaba8d6243 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com>
- * Copyright (C) 2012, Research In Motion Limited and others
+ * Copyright (C) 2012, 2021 Research In Motion Limited 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
@@ -650,6 +650,8 @@ public class JGitText extends TranslationBundle {
/***/ public String shortReadOfBlock;
/***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes;
/***/ public String shortSkipOfBlock;
+ /***/ public String signatureVerificationError;
+ /***/ public String signatureVerificationUnavailable;
/***/ public String signedTagMessageNoLf;
/***/ public String signingServiceUnavailable;
/***/ public String similarityScoreMustBeWithinBounds;
@@ -791,6 +793,13 @@ public class JGitText extends TranslationBundle {
/***/ public String URINotSupported;
/***/ public String userConfigInvalid;
/***/ public String validatingGitModules;
+ /***/ public String verifySignatureBad;
+ /***/ public String verifySignatureExpired;
+ /***/ public String verifySignatureGood;
+ /***/ public String verifySignatureIssuer;
+ /***/ public String verifySignatureKey;
+ /***/ public String verifySignatureMade;
+ /***/ public String verifySignatureTrust;
/***/ public String walkFailure;
/***/ public String wantNoSpaceWithCapabilities;
/***/ public String wantNotValid;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java
new file mode 100644
index 0000000000..a7a39c998f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java
@@ -0,0 +1,158 @@
+/*
+ * 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.lib;
+
+import java.io.IOException;
+import java.util.Date;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.revwalk.RevObject;
+
+/**
+ * A {@code GpgVerifier} can verify GPG signatures on git commits and tags.
+ *
+ * @since 5.11
+ */
+public interface GpgSignatureVerifier {
+
+ /**
+ * Verifies the signature on a signed commit or tag.
+ *
+ * @param object
+ * to verify
+ * @param config
+ * the {@link GpgConfig} to use
+ * @return a {@link SignatureVerification} describing the outcome of the
+ * verification, or {@code null} if the object was not signed
+ * @throws IOException
+ * if an error occurs getting a public key
+ * @throws org.eclipse.jgit.api.errors.JGitInternalException
+ * if signature verification fails
+ */
+ @Nullable
+ SignatureVerification verifySignature(@NonNull RevObject object,
+ @NonNull GpgConfig config) throws IOException;
+
+
+ /**
+ * Verifies a given signature for given data.
+ *
+ * @param data
+ * the signature is for
+ * @param signatureData
+ * the ASCII-armored signature
+ * @return a {@link SignatureVerification} describing the outcome
+ * @throws IOException
+ * if the signature cannot be parsed
+ * @throws JGitInternalException
+ * if signature verification fails
+ */
+ public SignatureVerification verify(byte[] data, byte[] signatureData)
+ throws IOException;
+
+ /**
+ * Retrieves the name of this verifier. This should be a short string
+ * identifying the engine that verified the signature, like "gpg" if GPG is
+ * used, or "bc" for a BouncyCastle implementation.
+ *
+ * @return the name
+ */
+ @NonNull
+ String getName();
+
+ /**
+ * A {@link GpgSignatureVerifier} may cache public keys to speed up
+ * verifying signatures on multiple objects. This clears this cache, if any.
+ */
+ void clear();
+
+ /**
+ * A {@code SignatureVerification} returns data about a (positively or
+ * negatively) verified signature.
+ */
+ interface SignatureVerification {
+
+ // Data about the signature.
+
+ @NonNull
+ Date getCreationDate();
+
+ // Data from the signature used to find a public key.
+
+ /**
+ * Obtains the signer as stored in the signature, if known.
+ *
+ * @return the signer, or {@code null} if unknown
+ */
+ String getSigner();
+
+ /**
+ * Obtains the short or long fingerprint of the public key as stored in
+ * the signature, if known.
+ *
+ * @return the fingerprint, or {@code null} if unknown
+ */
+ String getKeyFingerprint();
+
+ // Some information about the found public key.
+
+ /**
+ * Obtains the OpenPGP user ID associated with the key.
+ *
+ * @return the user id, or {@code null} if unknown
+ */
+ String getKeyUser();
+
+ /**
+ * Tells whether the public key used for this signature verification was
+ * expired when the signature was created.
+ *
+ * @return {@code true} if the key was expired already, {@code false}
+ * otherwise
+ */
+ boolean isExpired();
+
+ /**
+ * Obtains the trust level of the public key used to verify the
+ * signature.
+ *
+ * @return the trust level
+ */
+ @NonNull
+ TrustLevel getTrustLevel();
+
+ // The verification result.
+
+ /**
+ * Tells whether the signature verification was successful.
+ *
+ * @return {@code true} if the signature was verified successfully;
+ * {@code false} if not.
+ */
+ boolean getVerified();
+
+ /**
+ * Obtains a human-readable message giving additional information about
+ * the outcome of the verification.
+ *
+ * @return the message, or {@code null} if none set.
+ */
+ String getMessage();
+ }
+
+ /**
+ * The owner's trust in a public key.
+ */
+ enum TrustLevel {
+ UNKNOWN, NEVER, MARGINAL, FULL, ULTIMATE
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
new file mode 100644
index 0000000000..4b1dbedeb1
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
@@ -0,0 +1,71 @@
+/*
+ * 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.lib;
+
+import java.util.Iterator;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@code GpgSignatureVerifierFactory} creates {@link GpgSignatureVerifier} instances.
+ *
+ * @since 5.11
+ */
+public abstract class GpgSignatureVerifierFactory {
+
+ private static final Logger LOG = LoggerFactory
+ .getLogger(GpgSignatureVerifierFactory.class);
+
+ private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault();
+
+ private static GpgSignatureVerifierFactory loadDefault() {
+ try {
+ ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader
+ .load(GpgSignatureVerifierFactory.class);
+ Iterator<GpgSignatureVerifierFactory> iter = loader.iterator();
+ if (iter.hasNext()) {
+ return iter.next();
+ }
+ } catch (ServiceConfigurationError e) {
+ LOG.error(e.getMessage(), e);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the default factory.
+ *
+ * @return the default factory or {@code null} if none set
+ */
+ public static GpgSignatureVerifierFactory getDefault() {
+ return defaultFactory;
+ }
+
+ /**
+ * Sets the default factory.
+ *
+ * @param factory
+ * the new default factory
+ */
+ public static void setDefault(GpgSignatureVerifierFactory factory) {
+ defaultFactory = factory;
+ }
+
+ /**
+ * Creates a new {@link GpgSignatureVerifier}.
+ *
+ * @return the new {@link GpgSignatureVerifier}
+ */
+ public abstract GpgSignatureVerifier getVerifier();
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
index 3499136f92..b9d145008b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
@@ -1,7 +1,7 @@
/*
- * Copyright (C) 2008-2009, Google Inc.
+ * Copyright (C) 2008, 2009, Google Inc.
* Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2021, Shawn O. Pearce <spearce@spearce.org> 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
@@ -344,6 +344,22 @@ public class RevTag extends RevObject {
}
/**
+ * Obtain the raw unparsed tag body (<b>NOTE - THIS IS NOT A COPY</b>).
+ * <p>
+ * This method is exposed only to provide very fast, efficient access to
+ * this tag's message buffer. Applications relying on this buffer should be
+ * very careful to ensure they do not modify its contents during their use
+ * of it.
+ *
+ * @return the raw unparsed tag body. This is <b>NOT A COPY</b>. Do not
+ * alter the returned array.
+ * @since 5.11
+ */
+ public final byte[] getRawBuffer() {
+ return buffer;
+ }
+
+ /**
* Discard the message buffer to reduce memory usage.
* <p>
* After discarding the memory usage of the {@code RevTag} is reduced to
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java
new file mode 100644
index 0000000000..cf06172c17
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java
@@ -0,0 +1,86 @@
+/*
+ * 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.util;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.TrustLevel;
+import org.eclipse.jgit.lib.PersonIdent;
+
+/**
+ * Utilities for signature verification.
+ *
+ * @since 5.11
+ */
+public final class SignatureUtils {
+
+ private SignatureUtils() {
+ // No instantiation
+ }
+
+ /**
+ * Writes information about a signature verification to a string.
+ *
+ * @param verification
+ * to show
+ * @param creator
+ * of the object verified; used for time zone information
+ * @param formatter
+ * to use for dates
+ * @return a textual representation of the {@link SignatureVerification},
+ * using LF as line separator
+ */
+ public static String toString(SignatureVerification verification,
+ PersonIdent creator, GitDateFormatter formatter) {
+ StringBuilder result = new StringBuilder();
+ // Use the creator's timezone for the signature date
+ PersonIdent dateId = new PersonIdent(creator,
+ verification.getCreationDate());
+ result.append(MessageFormat.format(JGitText.get().verifySignatureMade,
+ formatter.formatDate(dateId)));
+ result.append('\n');
+ result.append(MessageFormat.format(
+ JGitText.get().verifySignatureKey,
+ verification.getKeyFingerprint().toUpperCase(Locale.ROOT)));
+ result.append('\n');
+ if (!StringUtils.isEmptyOrNull(verification.getSigner())) {
+ result.append(
+ MessageFormat.format(JGitText.get().verifySignatureIssuer,
+ verification.getSigner()));
+ result.append('\n');
+ }
+ String msg;
+ if (verification.getVerified()) {
+ if (verification.isExpired()) {
+ msg = JGitText.get().verifySignatureExpired;
+ } else {
+ msg = JGitText.get().verifySignatureGood;
+ }
+ } else {
+ msg = JGitText.get().verifySignatureBad;
+ }
+ result.append(MessageFormat.format(msg, verification.getKeyUser()));
+ if (!TrustLevel.UNKNOWN.equals(verification.getTrustLevel())) {
+ result.append(' ' + MessageFormat
+ .format(JGitText.get().verifySignatureTrust, verification
+ .getTrustLevel().name().toLowerCase(Locale.ROOT)));
+ }
+ result.append('\n');
+ msg = verification.getMessage();
+ if (!StringUtils.isEmptyOrNull(msg)) {
+ result.append(msg);
+ result.append('\n');
+ }
+ return result.toString();
+ }
+}