summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.gpg.bc
diff options
context:
space:
mode:
authorThomas Wolf <thomas.wolf@paranor.ch>2021-01-07 17:11:57 +0100
committerMatthias Sohn <matthias.sohn@sap.com>2021-02-16 00:37:00 +0100
commit3774fcc848da7526ffa74211cbb2781df5731125 (patch)
tree71aee433ac3a5b1c8efa2de628de7dd4560c4a5d /org.eclipse.jgit.gpg.bc
parent15a38e5b4f79792c8ce85c8eddd567c32350de74 (diff)
downloadjgit-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.gpg.bc')
-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
8 files changed, 502 insertions, 22 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);