From 137e91a4654b4f516d1f82885c669e14473dbfd6 Mon Sep 17 00:00:00 2001
From: Medha Bhargav Prabhala
Date: Mon, 3 Dec 2018 20:13:27 +0100
Subject: [PATCH] Implement signing commits using BouncyCastle
This also includes a change to generating the jgit CLI jar. Shading is
no longer possible because it breaks the signature of BouncyCastle.
Instead, the Spring Boot Loader Maven plug-in is now used to generate an
executable jar.
Bug: 382212
Change-Id: I35ee3d4b06d9d479475ab2e51b29bed49661bbdc
Also-by: Gunnar Wagenknecht
Signed-off-by: Gunnar Wagenknecht
Signed-off-by: Medha Bhargav Prabhala
Signed-off-by: Matthias Sohn
---
WORKSPACE | 23 ++
lib/BUILD | 27 ++
.../org.eclipse.jgit.feature/feature.xml | 21 ++
.../jgit-4.10-staging.target | 6 +
.../org.eclipse.jgit.target/jgit-4.5.target | 6 +
.../org.eclipse.jgit.target/jgit-4.6.target | 6 +
.../org.eclipse.jgit.target/jgit-4.7.target | 6 +
.../org.eclipse.jgit.target/jgit-4.8.target | 6 +
.../org.eclipse.jgit.target/jgit-4.9.target | 6 +
.../orbit/R20181128170323-2018-12.tpd | 6 +
org.eclipse.jgit.pgm/jgit.sh | 4 +-
org.eclipse.jgit.pgm/pom.xml | 47 +--
.../src/org/eclipse/jgit/pgm/Commit.java | 2 +-
org.eclipse.jgit.test/META-INF/MANIFEST.MF | 1 +
org.eclipse.jgit.test/pom.xml | 13 +-
.../eclipse/jgit/api/CommitCommandTest.java | 22 +-
org.eclipse.jgit/BUILD | 3 +
org.eclipse.jgit/META-INF/MANIFEST.MF | 10 +
org.eclipse.jgit/pom.xml | 16 +-
.../eclipse/jgit/internal/JGitText.properties | 10 +
.../org/eclipse/jgit/api/CommitCommand.java | 24 +-
.../org/eclipse/jgit/internal/JGitText.java | 10 +
.../src/org/eclipse/jgit/lib/GpgSigner.java | 16 +-
.../jgit/lib/internal/BouncyCastleGpgKey.java | 71 ++++
.../internal/BouncyCastleGpgKeyLocator.java | 340 ++++++++++++++++++
.../BouncyCastleGpgKeyPassphrasePrompt.java | 134 +++++++
.../lib/internal/BouncyCastleGpgSigner.java | 135 +++++++
pom.xml | 26 ++
28 files changed, 951 insertions(+), 46 deletions(-)
create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKey.java
create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java
create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyPassphrasePrompt.java
create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java
diff --git a/WORKSPACE b/WORKSPACE
index ea78ab2b1..31820fc79 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -211,3 +211,26 @@ maven_jar(
sha1 = "5bb3d7a38f7ea54138336591d89dd5867b806c02",
src_sha1 = "94e89a8c9f82e38555e95b9f7f58344a247e862c",
)
+
+BOUNCYCASTLE_VER = "1.60"
+
+maven_jar(
+ name = "bcpg-jdk15on",
+ artifact = "org.bouncycastle:bcpg-jdk15on:" + BOUNCYCASTLE_VER,
+ sha1 = "13c7a199c484127daad298996e95818478431a2c",
+ src_sha1 = "edcd9e86d95e39b4da39bb295efd93bc4f56266e",
+)
+
+maven_jar(
+ name = "bcprov-jdk15on",
+ artifact = "org.bouncycastle:bcprov-jdk15on:" + BOUNCYCASTLE_VER,
+ sha1 = "bd47ad3bd14b8e82595c7adaa143501e60842a84",
+ src_sha1 = "7c57a4d13fe53d9abb967bba600dd0b293dafd6a",
+)
+
+maven_jar(
+ name = "bcpkix-jdk15on",
+ artifact = "org.bouncycastle:bcpkix-jdk15on:" + BOUNCYCASTLE_VER,
+ sha1 = "d0c46320fbc07be3a24eb13a56cee4e3d38e0c75",
+ src_sha1 = "a25f041293f401af08efba63ff4bbdce98134a03",
+)
diff --git a/lib/BUILD b/lib/BUILD
index 0c5c22426..d89ebab26 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -147,6 +147,33 @@ java_library(
exports = ["@jsch//jar"],
)
+java_library(
+ name = "bcpg",
+ visibility = [
+ "//org.eclipse.jgit:__pkg__",
+ "//org.eclipse.jgit.test:__pkg__",
+ ],
+ exports = ["@bcpg-jdk15on//jar"],
+)
+
+java_library(
+ name = "bcprov",
+ visibility = [
+ "//org.eclipse.jgit:__pkg__",
+ "//org.eclipse.jgit.test:__pkg__",
+ ],
+ exports = ["@bcprov-jdk15on//jar"],
+)
+
+java_library(
+ name = "bcpkix",
+ visibility = [
+ "//org.eclipse.jgit:__pkg__",
+ "//org.eclipse.jgit.test:__pkg__",
+ ],
+ exports = ["@bcpkix-jdk15on//jar"],
+)
+
java_library(
name = "jzlib",
visibility = [
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
index 8a6e2a705..2874d7037 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
@@ -86,4 +86,25 @@
version="0.0.0"
unpack="false"/>
+
+
+
+
+
+
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10-staging.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10-staging.target
index dff7b79b6..83ca7c491 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10-staging.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10-staging.target
@@ -37,6 +37,12 @@
+
+
+
+
+
+
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
index 3d6b5f308..02955266f 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
@@ -37,6 +37,12 @@
+
+
+
+
+
+
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
index bcde775ab..dfbb45233 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
@@ -37,6 +37,12 @@
+
+
+
+
+
+
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
index c8b601c2b..89a2014dc 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
@@ -37,6 +37,12 @@
+
+
+
+
+
+
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target
index 27e86e87c..45321eea2 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target
@@ -37,6 +37,12 @@
+
+
+
+
+
+
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target
index 4969136cd..baf845a6d 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target
@@ -37,6 +37,12 @@
+
+
+
+
+
+
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20181128170323-2018-12.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20181128170323-2018-12.tpd
index d447d6ca4..fd69da262 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20181128170323-2018-12.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20181128170323-2018-12.tpd
@@ -16,6 +16,12 @@ location "http://download.eclipse.org/tools/orbit/downloads/drops/R2018112817032
org.apache.httpcomponents.httpcore.source [4.4.9.v20180409-1525,4.4.9.v20180409-1525]
org.apache.log4j [1.2.15.v201012070815,1.2.15.v201012070815]
org.apache.log4j.source [1.2.15.v201012070815,1.2.15.v201012070815]
+ org.bouncycastle.bcpg [1.60.0.v20181107-1520,1.60.0.v20181107-1520]
+ org.bouncycastle.bcpg.source [1.60.0.v20181107-1520,1.60.0.v20181107-1520]
+ org.bouncycastle.bcpkix [1.60.0.v20181107-1520,1.60.0.v20181107-1520]
+ org.bouncycastle.bcpkix.source [1.60.0.v20181107-1520,1.60.0.v20181107-1520]
+ org.bouncycastle.bcprov [1.60.0.v20181107-1520,1.60.0.v20181107-1520]
+ org.bouncycastle.bcprov.source [1.60.0.v20181107-1520,1.60.0.v20181107-1520]
org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
org.hamcrest [1.1.0.v20090501071000,1.1.0.v20090501071000]
diff --git a/org.eclipse.jgit.pgm/jgit.sh b/org.eclipse.jgit.pgm/jgit.sh
index 826714751..e26378273 100644
--- a/org.eclipse.jgit.pgm/jgit.sh
+++ b/org.eclipse.jgit.pgm/jgit.sh
@@ -110,9 +110,9 @@ then
LESS=${LESS:-FSRX}
export LESS
- "$java" $java_args org.eclipse.jgit.pgm.Main "$@" | $use_pager
+ "$java" $java_args org.springframework.boot.loader.JarLauncher "$@" | $use_pager
exit
else
- exec "$java" $java_args org.eclipse.jgit.pgm.Main "$@"
+ exec "$java" $java_args org.springframework.boot.loader.JarLauncher "$@"
exit 1
fi
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index 049d4de4a..6faaa00eb 100644
--- a/org.eclipse.jgit.pgm/pom.xml
+++ b/org.eclipse.jgit.pgm/pom.xml
@@ -172,40 +172,19 @@
- org.apache.maven.plugins
- maven-shade-plugin
+ org.springframework.boot
+ spring-boot-maven-plugin
- package
- shade
+ repackage
jgit-cli
- false
-
-
-
-
- org.eclipse.jgit.pgm.Main
- JGit Command Line Interface
-
-
-
-
-
-
- *:*
-
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
- OSGI-OPT/**
-
-
-
- true
- shaded
+ false
+ org.eclipse.jgit.pgm.Main
+ true
+ jgit.sh
@@ -220,11 +199,13 @@
package
-
-
-
-
-
+
+
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
index 81c473191..00d2d100d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
@@ -120,7 +120,7 @@ class Commit extends TextBuiltin {
try {
commit = commitCmd.call();
} catch (JGitInternalException e) {
- throw die(e.getMessage());
+ throw die(e.getMessage(), e);
}
String branchName;
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 7bdb72234..45592e44c 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -11,6 +11,7 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
com.jcraft.jsch;version="[0.1.54,0.2.0)",
net.bytebuddy.dynamic.loading;version="[1.7.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.60.0,2.0.0)",
org.eclipse.jgit.annotations;version="[5.3.0,5.4.0)",
org.eclipse.jgit.api;version="[5.3.0,5.4.0)",
org.eclipse.jgit.api.errors;version="[5.3.0,5.4.0)",
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index 10117d89d..718fa399f 100644
--- a/org.eclipse.jgit.test/pom.xml
+++ b/org.eclipse.jgit.test/pom.xml
@@ -76,10 +76,21 @@
org.bouncycastle
bcprov-jdk15on
- 1.59
test
+
+ org.bouncycastle
+ bcpg-jdk15on
+ test
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ test
+
+
org.hamcrest
hamcrest-library
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
index 9128bb66c..5f1992cb5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
@@ -46,6 +46,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -76,6 +77,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.submodule.SubmoduleWalk;
+import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FS;
@@ -639,21 +641,26 @@ public class CommitCommandTest extends RepositoryTestCase {
git.add().addFilepattern("file1").call();
String[] signingKey = new String[1];
+ PersonIdent[] signingCommitters = new PersonIdent[1];
AtomicInteger callCount = new AtomicInteger();
GpgSigner.setDefault(new GpgSigner() {
@Override
- public void sign(CommitBuilder commit, String gpgSigningKey) {
+ public void sign(CommitBuilder commit, String gpgSigningKey,
+ PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
signingKey[0] = gpgSigningKey;
+ signingCommitters[0] = signingCommitter;
callCount.incrementAndGet();
}
});
// first call should use config, which is expected to be null at
// this time
- git.commit().setSign(Boolean.TRUE).setMessage("initial commit")
+ git.commit().setCommitter(committer).setSign(Boolean.TRUE)
+ .setMessage("initial commit")
.call();
assertNull(signingKey[0]);
assertEquals(1, callCount.get());
+ assertSame(committer, signingCommitters[0]);
writeTrashFile("file2", "file2");
git.add().addFilepattern("file2").call();
@@ -665,20 +672,24 @@ public class CommitCommandTest extends RepositoryTestCase {
expectedConfigSigningKey);
config.save();
- git.commit().setSign(Boolean.TRUE).setMessage("initial commit")
+ git.commit().setCommitter(committer).setSign(Boolean.TRUE)
+ .setMessage("initial commit")
.call();
assertEquals(expectedConfigSigningKey, signingKey[0]);
assertEquals(2, callCount.get());
+ assertSame(committer, signingCommitters[0]);
writeTrashFile("file3", "file3");
git.add().addFilepattern("file3").call();
// now use specific on api
String expectedSigningKey = "my-" + System.nanoTime();
- git.commit().setSign(Boolean.TRUE).setSigningKey(expectedSigningKey)
+ git.commit().setCommitter(committer).setSign(Boolean.TRUE)
+ .setSigningKey(expectedSigningKey)
.setMessage("initial commit").call();
assertEquals(expectedSigningKey, signingKey[0]);
assertEquals(3, callCount.get());
+ assertSame(committer, signingCommitters[0]);
}
}
@@ -691,7 +702,8 @@ public class CommitCommandTest extends RepositoryTestCase {
AtomicInteger callCount = new AtomicInteger();
GpgSigner.setDefault(new GpgSigner() {
@Override
- public void sign(CommitBuilder commit, String gpgSigningKey) {
+ public void sign(CommitBuilder commit, String gpgSigningKey,
+ PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
callCount.incrementAndGet();
}
});
diff --git a/org.eclipse.jgit/BUILD b/org.eclipse.jgit/BUILD
index 6ba7796b7..9ddcb9d9d 100644
--- a/org.eclipse.jgit/BUILD
+++ b/org.eclipse.jgit/BUILD
@@ -26,6 +26,9 @@ java_library(
"//lib:jsch",
"//lib:jzlib",
"//lib:slf4j-api",
+ "//lib:bcpg",
+ "//lib:bcprov",
+ "//lib:bcpkix"
],
)
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 38b734b54..d71910f8a 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -160,6 +160,16 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
com.jcraft.jsch;version="[0.1.37,0.2.0)",
javax.crypto,
javax.net.ssl,
+ org.bouncycastle;version="[1.60.0,2.0.0)",
+ org.bouncycastle.bcpg;version="[1.60.0,2.0.0)",
+ org.bouncycastle.gpg;version="[1.60.0,2.0.0)",
+ org.bouncycastle.gpg.keybox;version="[1.60.0,2.0.0)",
+ org.bouncycastle.jce.provider;version="[1.60.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.60.0,2.0.0)",
+ org.bouncycastle.openpgp.jcajce;version="[1.60.0,2.0.0)",
+ org.bouncycastle.openpgp.operator;version="[1.60.0,2.0.0)",
+ org.bouncycastle.openpgp.operator.jcajce;version="[1.60.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.60.0,2.0.0)",
org.slf4j;version="[1.7.0,2.0.0)",
org.xml.sax,
org.xml.sax.helpers
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index 0659df252..ff819237b 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -84,12 +84,26 @@
JavaEWAH
-
org.slf4j
slf4j-api
+
+ org.bouncycastle
+ bcpg-jdk15on
+
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+
+
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 dc26e5868..6ee144ff6 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -223,6 +223,7 @@ createBranchUnexpectedResult=Create branch returned unexpected result {0}
createNewFileFailed=Could not create new file {0}
createRequiresZeroOldId=Create requires old ID to be zero
credentialPassword=Password
+credentialPassphrase=Passphrase
credentialUsername=Username
daemonAlreadyRunning=Daemon already running
daysAgo={0} days ago
@@ -323,6 +324,14 @@ gcFailed=Garbage collection failed.
gcLogExists=A previous GC run reported an error: ''{0}''. Automatic gc will fail until ''{1}'' is removed.
gcTooManyUnpruned=Too many loose, unpruneable objects after garbage collection. Consider adjusting gc.auto or gc.pruneExpire.
gitmodulesNotFound=.gitmodules not found in tree.
+gpgFailedToParseSecretKey=Failed to parse secret key file in directory: {0}. Is the entered passphrase correct?
+gpgNoCredentialsProvider=missing credentials provider
+gpgNoKeyring=neither pubring.kbx nor secring.gpg files found
+gpgNoKeyInLegacySecring=no matching secret key found in legacy secring.gpg for key or user id: {0}
+gpgNoPublicKeyFound=Unable to find a public-key with key or user id: {0}
+gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key: {0}
+gpgKeyInfo=GPG Key (fingerprint {0})
+gpgSigningCancelled=Signing was cancelled
headRequiredToStash=HEAD required to stash local changes
hoursAgo={0} hours ago
httpConfigCannotNormalizeURL=Cannot normalize URL path {0}: too many .. segments
@@ -716,6 +725,7 @@ unableToCreateNewObject=Unable to create new object: {0}
unableToRemovePath=Unable to remove path ''{0}''
unableToStore=Unable to store {0}.
unableToWrite=Unable to write {0}
+unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
unauthorized=Unauthorized
underflowedReftableBlock=Underflowed reftable block
unencodeableFile=Unencodable file: {0}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index 00d3842da..9f071c4c4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -88,10 +88,12 @@ import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.lib.internal.BouncyCastleGpgSigner;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -149,6 +151,8 @@ public class CommitCommand extends GitCommand {
private GpgSigner gpgSigner;
+ private CredentialsProvider credentialsProvider;
+
/**
* Constructor for CommitCommand
*
@@ -157,6 +161,7 @@ public class CommitCommand extends GitCommand {
*/
protected CommitCommand(Repository repo) {
super(repo);
+ this.credentialsProvider = CredentialsProvider.getDefault();
}
/**
@@ -263,7 +268,8 @@ public class CommitCommand extends GitCommand {
commit.setTreeId(indexTreeId);
if (signCommit.booleanValue()) {
- gpgSigner.sign(commit, signingKey);
+ gpgSigner.sign(commit, signingKey, committer,
+ credentialsProvider);
}
ObjectId commitId = odi.insert(commit);
@@ -603,6 +609,9 @@ public class CommitCommand extends GitCommand {
JGitText.get().onlyOpenPgpSupportedForSigning);
}
gpgSigner = GpgSigner.getDefault();
+ if (gpgSigner == null) {
+ gpgSigner = new BouncyCastleGpgSigner();
+ }
}
}
@@ -943,4 +952,17 @@ public class CommitCommand extends GitCommand {
this.signCommit = sign;
return this;
}
+
+ /**
+ * Sets a {@link CredentialsProvider}
+ *
+ * @param credentialsProvider
+ * the provider to use when querying for credentials (eg., during
+ * signing)
+ * @since 5.3
+ */
+ public void setCredentialsProvider(
+ CredentialsProvider credentialsProvider) {
+ this.credentialsProvider = credentialsProvider;
+ }
}
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 9da7f1574..bd91fe0f8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -284,6 +284,7 @@ public class JGitText extends TranslationBundle {
/***/ public String createNewFileFailed;
/***/ public String createRequiresZeroOldId;
/***/ public String credentialPassword;
+ /***/ public String credentialPassphrase;
/***/ public String credentialUsername;
/***/ public String daemonAlreadyRunning;
/***/ public String daysAgo;
@@ -384,6 +385,14 @@ public class JGitText extends TranslationBundle {
/***/ public String gcLogExists;
/***/ public String gcTooManyUnpruned;
/***/ public String gitmodulesNotFound;
+ /***/ public String gpgFailedToParseSecretKey;
+ /***/ public String gpgNoCredentialsProvider;
+ /***/ public String gpgNoKeyring;
+ /***/ public String gpgNoKeyInLegacySecring;
+ /***/ public String gpgNoPublicKeyFound;
+ /***/ public String gpgNoSecretKeyForPublicKey;
+ /***/ public String gpgKeyInfo;
+ /***/ public String gpgSigningCancelled;
/***/ public String headRequiredToStash;
/***/ public String hoursAgo;
/***/ public String httpConfigCannotNormalizeURL;
@@ -776,6 +785,7 @@ public class JGitText extends TranslationBundle {
/***/ public String unableToRemovePath;
/***/ public String unableToStore;
/***/ public String unableToWrite;
+ /***/ public String unableToSignCommitNoSecretKey;
/***/ public String unauthorized;
/***/ public String underflowedReftableBlock;
/***/ public String unencodeableFile;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
index e509c9783..7796c2058 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
@@ -43,6 +43,9 @@
package org.eclipse.jgit.lib;
import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.lib.internal.BouncyCastleGpgSigner;
+import org.eclipse.jgit.transport.CredentialsProvider;
/**
* Creates GPG signatures for Git objects.
@@ -51,7 +54,7 @@ import org.eclipse.jgit.annotations.NonNull;
*/
public abstract class GpgSigner {
- private static GpgSigner defaultSigner;
+ private static GpgSigner defaultSigner = new BouncyCastleGpgSigner();
/**
* Get the default signer, or null
.
@@ -93,8 +96,17 @@ public abstract class GpgSigner {
* complete to allow proper calculation of payload)
* @param gpgSigningKey
* the signing key (passed as is to the GPG signing tool)
+ * @param committer
+ * the signing identity (to help with key lookup)
+ * @param credentialsProvider
+ * provider to use when querying for signing key credentials (eg.
+ * passphrase)
+ * @throws CanceledException
+ * when signing was canceled (eg., user aborted when entering
+ * passphrase)
*/
public abstract void sign(@NonNull CommitBuilder commit,
- String gpgSigningKey);
+ String gpgSigningKey, @NonNull PersonIdent committer,
+ CredentialsProvider credentialsProvider) throws CanceledException;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKey.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKey.java
new file mode 100644
index 000000000..ef9d7ee39
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKey.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018, Salesforce.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lib.internal;
+
+import java.nio.file.Path;
+
+import org.bouncycastle.openpgp.PGPSecretKey;
+
+/**
+ * Container which holds a {@link #getSecretKey()} together with the
+ * {@link #getOrigin() path it was loaded from}.
+ */
+class BouncyCastleGpgKey {
+
+ private PGPSecretKey secretKey;
+
+ private Path origin;
+
+ public BouncyCastleGpgKey(PGPSecretKey secretKey, Path origin) {
+ this.secretKey = secretKey;
+ this.origin = origin;
+ }
+
+ public PGPSecretKey getSecretKey() {
+ return secretKey;
+ }
+
+ public Path getOrigin() {
+ return origin;
+ }
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java
new file mode 100644
index 000000000..c7cbe360c
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2018, Salesforce.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lib.internal;
+
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.newInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.bouncycastle.gpg.SExprParser;
+import org.bouncycastle.gpg.keybox.BlobType;
+import org.bouncycastle.gpg.keybox.KeyBlob;
+import org.bouncycastle.gpg.keybox.KeyBox;
+import org.bouncycastle.gpg.keybox.KeyInformation;
+import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
+import org.bouncycastle.gpg.keybox.UserID;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Locates GPG keys from either ~/.gnupg/private-keys-v1.d
or
+ * ~/.gnupg/secring.gpg
+ */
+class BouncyCastleGpgKeyLocator {
+
+ private static final Path USER_KEYBOX_PATH = Paths
+ .get(System.getProperty("user.home"), ".gnupg", "pubring.kbx"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+ private static final Path USER_SECRET_KEY_DIR = Paths.get(
+ System.getProperty("user.home"), ".gnupg", "private-keys-v1.d"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+ private static final Path USER_PGP_LEGACY_SECRING_FILE = Paths
+ .get(System.getProperty("user.home"), ".gnupg", "secring.gpg"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+ private final String signingKey;
+
+ private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt;
+
+ /**
+ * Create a new key locator for the specified signing key.
+ *
+ * The signing key must either be a hex representation of a specific key or
+ * a user identity substring (eg., email address). All keys in the KeyBox
+ * will be looked up in the order as returned by the KeyBox. A key id will
+ * be searched before attempting to find a key by user id.
+ *
+ *
+ * @param signingKey
+ * the signing key to search for
+ * @param passphrasePrompt
+ * the provider to use when asking for key passphrase
+ */
+ public BouncyCastleGpgKeyLocator(String signingKey,
+ @NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
+ this.signingKey = signingKey;
+ this.passphrasePrompt = passphrasePrompt;
+ }
+
+ private PGPSecretKey attemptParseSecretKey(Path keyFile,
+ PGPDigestCalculatorProvider calculatorProvider,
+ PBEProtectionRemoverFactory passphraseProvider,
+ PGPPublicKey publicKey) throws IOException {
+ try (InputStream in = newInputStream(keyFile)) {
+ return new SExprParser(calculatorProvider).parseSecretKey(
+ new BufferedInputStream(in), passphraseProvider, publicKey);
+ } catch (PGPException | ClassCastException e) {
+ return null;
+ }
+ }
+
+ private boolean containsSigningKey(String userId) {
+ return userId.toLowerCase(Locale.ROOT)
+ .contains(signingKey.toLowerCase(Locale.ROOT));
+ }
+
+ private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
+ throws IOException {
+ for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
+ if (signingKey.toLowerCase(Locale.ROOT)
+ .equals(Hex.toHexString(keyInfo.getKeyID())
+ .toLowerCase(Locale.ROOT))) {
+ return getFirstPublicKey(keyBlob);
+ }
+ }
+ return null;
+ }
+
+ private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
+ throws IOException {
+ for (UserID userID : keyBlob.getUserIds()) {
+ if (containsSigningKey(userID.getUserIDAsString())) {
+ return getFirstPublicKey(keyBlob);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds a public key associated with the signing key.
+ *
+ * @param keyboxFile
+ * the KeyBox file
+ * @return publicKey the public key (maybe null
)
+ * @throws IOException
+ * in case of problems reading the file
+ */
+ private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
+ throws IOException {
+ KeyBox keyBox = readKeyBoxFile(keyboxFile);
+ for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
+ if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
+ PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
+ if (key != null) {
+ return key;
+ }
+ key = findPublicKeyByUserId(keyBlob);
+ if (key != null) {
+ return key;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Use pubring.kbx when available, if not fallback to secring.gpg or secret
+ * key path provided to parse and return secret key
+ *
+ * @return the secret key
+ * @throws IOException
+ * in case of issues reading key files
+ * @throws PGPException
+ * in case of issues finding a key
+ * @throws CanceledException
+ * @throws URISyntaxException
+ * @throws UnsupportedCredentialItem
+ */
+ public BouncyCastleGpgKey findSecretKey()
+ throws IOException, PGPException, CanceledException,
+ UnsupportedCredentialItem, URISyntaxException {
+ if (exists(USER_KEYBOX_PATH)) {
+ PGPPublicKey publicKey = //
+ findPublicKeyInKeyBox(USER_KEYBOX_PATH);
+
+ if (publicKey != null) {
+ return findSecretKeyForKeyBoxPublicKey(publicKey,
+ USER_KEYBOX_PATH);
+ }
+
+ throw new PGPException(MessageFormat
+ .format(JGitText.get().gpgNoPublicKeyFound, signingKey));
+ } else if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
+ PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
+ USER_PGP_LEGACY_SECRING_FILE);
+
+ if (secretKey != null) {
+ return new BouncyCastleGpgKey(secretKey, USER_PGP_LEGACY_SECRING_FILE);
+ }
+
+ throw new PGPException(MessageFormat.format(
+ JGitText.get().gpgNoKeyInLegacySecring, signingKey));
+ }
+
+ throw new PGPException(JGitText.get().gpgNoKeyring);
+ }
+
+ private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
+ PGPPublicKey publicKey, Path userKeyboxPath)
+ throws PGPException, CanceledException, UnsupportedCredentialItem,
+ URISyntaxException {
+ /*
+ * this is somewhat brute-force but there doesn't seem to be another
+ * way; we have to walk all private key files we find and try to open
+ * them
+ */
+
+ PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
+ .build();
+
+ PBEProtectionRemoverFactory passphraseProvider = new JcePBEProtectionRemoverFactory(
+ passphrasePrompt.getPassphrase(publicKey.getFingerprint(),
+ userKeyboxPath));
+
+ try (Stream keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
+ for (Path keyFile : keyFiles.filter(Files::isRegularFile)
+ .collect(Collectors.toList())) {
+ PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
+ calculatorProvider, passphraseProvider, publicKey);
+ if (secretKey != null) {
+ return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
+ }
+ }
+
+ passphrasePrompt.clear();
+ throw new PGPException(MessageFormat.format(
+ JGitText.get().gpgNoSecretKeyForPublicKey,
+ Long.toHexString(publicKey.getKeyID())));
+ } catch (RuntimeException e) {
+ passphrasePrompt.clear();
+ throw e;
+ } catch (IOException e) {
+ passphrasePrompt.clear();
+ throw new PGPException(MessageFormat.format(
+ JGitText.get().gpgFailedToParseSecretKey,
+ USER_SECRET_KEY_DIR.toAbsolutePath()), e);
+ }
+ }
+
+ /**
+ * Return the first suitable key for signing in the key ring collection. For
+ * this case we only expect there to be one key available for signing.
+ *
+ *
+ * @param signingkey
+ * @param secringFile
+ *
+ * @return the first suitable PGP secret key found for signing
+ * @throws IOException
+ * on I/O related errors
+ * @throws PGPException
+ * on BouncyCastle errors
+ */
+ private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
+ Path secringFile) throws IOException, PGPException {
+
+ try (InputStream in = newInputStream(secringFile)) {
+ PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
+ PGPUtil.getDecoderStream(new BufferedInputStream(in)),
+ new JcaKeyFingerprintCalculator());
+
+ Iterator keyrings = pgpSec.getKeyRings();
+ while (keyrings.hasNext()) {
+ PGPSecretKeyRing keyRing = keyrings.next();
+ Iterator keys = keyRing.getSecretKeys();
+ while (keys.hasNext()) {
+ PGPSecretKey key = keys.next();
+ // try key id
+ String fingerprint = Hex
+ .toHexString(key.getPublicKey().getFingerprint())
+ .toLowerCase(Locale.ROOT);
+ if (fingerprint
+ .endsWith(signingkey.toLowerCase(Locale.ROOT))) {
+ return key;
+ }
+ // try user id
+ Iterator userIDs = key.getUserIDs();
+ while (userIDs.hasNext()) {
+ String userId = userIDs.next();
+ if (containsSigningKey(userId)) {
+ return key;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private PGPPublicKey getFirstPublicKey(KeyBlob keyBlob) throws IOException {
+ return ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing()
+ .getPublicKey();
+ }
+
+ private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException {
+ KeyBox keyBox;
+ try (InputStream in = new BufferedInputStream(
+ newInputStream(keyboxFile))) {
+ // note: KeyBox constructor reads in the whole InputStream at once
+ // this code will change in 1.61 to
+ // either 'new BcKeyBox(in)' or 'new JcaKeyBoxBuilder().build(in)'
+ keyBox = new KeyBox(in, new JcaKeyFingerprintCalculator());
+ }
+ return keyBox;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyPassphrasePrompt.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyPassphrasePrompt.java
new file mode 100644
index 000000000..2efe96291
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyPassphrasePrompt.java
@@ -0,0 +1,134 @@
+/*-
+ * Copyright (C) 2019, Salesforce.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lib.internal;
+
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.text.MessageFormat;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.transport.CredentialItem.CharArrayType;
+import org.eclipse.jgit.transport.CredentialItem.InformationalMessage;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.URIish;
+
+/**
+ * Prompts for a passphrase and caches it until {@link #clear() cleared}.
+ *
+ * Implements {@link AutoCloseable} so it can be used within a
+ * try-with-resources block.
+ *
+ */
+class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
+
+ private CharArrayType passphrase;
+
+ private CredentialsProvider credentialsProvider;
+
+ public BouncyCastleGpgKeyPassphrasePrompt(
+ CredentialsProvider credentialsProvider) {
+ this.credentialsProvider = credentialsProvider;
+ }
+
+ /**
+ * Clears any cached passphrase
+ */
+ public void clear() {
+ if (passphrase != null) {
+ passphrase.clear();
+ passphrase = null;
+ }
+ }
+
+ @Override
+ public void close() {
+ clear();
+ }
+
+ private URIish createURI(Path keyLocation) throws URISyntaxException {
+ return new URIish(keyLocation.toUri().toString());
+ }
+
+ /**
+ * Prompts use for a passphrase unless one was cached from a previous
+ * prompt.
+ *
+ * @param keyFingerprint
+ * the fingerprint to show to the user during prompting
+ * @param keyLocation
+ * the location the key was loaded from
+ * @return the passphrase (maybe null
)
+ * @throws PGPException
+ * @throws CanceledException
+ * in case passphrase was not entered by user
+ * @throws URISyntaxException
+ * @throws UnsupportedCredentialItem
+ */
+ public char[] getPassphrase(byte[] keyFingerprint, Path keyLocation)
+ throws PGPException, CanceledException, UnsupportedCredentialItem,
+ URISyntaxException {
+ if (passphrase == null) {
+ passphrase = new CharArrayType(JGitText.get().credentialPassphrase,
+ true);
+ }
+
+ if (credentialsProvider == null) {
+ throw new PGPException(JGitText.get().gpgNoCredentialsProvider);
+ }
+
+ if (passphrase.getValue() == null
+ && !credentialsProvider.get(createURI(keyLocation),
+ new InformationalMessage(
+ MessageFormat.format(JGitText.get().gpgKeyInfo,
+ Hex.toHexString(keyFingerprint))),
+ passphrase)) {
+ throw new CanceledException(JGitText.get().gpgSigningCancelled);
+ }
+ return passphrase.getValue();
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java
new file mode 100644
index 000000000..f447912f0
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018, Salesforce.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lib.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.Security;
+
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.bcpg.BCPGOutputStream;
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureGenerator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.GpgSignature;
+import org.eclipse.jgit.lib.GpgSigner;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.transport.CredentialsProvider;
+
+/**
+ * GPG Signer using BouncyCastle library
+ */
+public class BouncyCastleGpgSigner extends GpgSigner {
+
+ private static void registerBouncyCastleProviderIfNecessary() {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ }
+
+ /**
+ * Create a new instance.
+ *
+ * The BounceCastleProvider will be registered if necessary.
+ *
+ */
+ public BouncyCastleGpgSigner() {
+ registerBouncyCastleProviderIfNecessary();
+ }
+
+ @Override
+ public void sign(@NonNull CommitBuilder commit, String gpgSigningKey,
+ @NonNull PersonIdent committer,
+ CredentialsProvider credentialsProvider) throws CanceledException {
+ if (gpgSigningKey == null || gpgSigningKey.isEmpty()) {
+ gpgSigningKey = committer.getEmailAddress();
+ }
+
+ try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
+ credentialsProvider)) {
+ BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator(
+ gpgSigningKey, passphrasePrompt);
+
+ BouncyCastleGpgKey gpgKey = keyHelper.findSecretKey();
+ PGPSecretKey secretKey = gpgKey.getSecretKey();
+ if (secretKey == null) {
+ throw new JGitInternalException(
+ JGitText.get().unableToSignCommitNoSecretKey);
+ }
+ char[] passphrase = passphrasePrompt
+ .getPassphrase(secretKey.getPublicKey().getFingerprint(),
+ gpgKey.getOrigin());
+ PGPPrivateKey privateKey = secretKey
+ .extractPrivateKey(new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(BouncyCastleProvider.PROVIDER_NAME)
+ .build(passphrase));
+ PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
+ new JcaPGPContentSignerBuilder(
+ secretKey.getPublicKey().getAlgorithm(),
+ HashAlgorithmTags.SHA256).setProvider(
+ BouncyCastleProvider.PROVIDER_NAME));
+ signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ try (BCPGOutputStream out = new BCPGOutputStream(
+ new ArmoredOutputStream(buffer))) {
+ signatureGenerator.update(commit.build());
+ signatureGenerator.generate().encode(out);
+ }
+ commit.setGpgSignature(new GpgSignature(buffer.toByteArray()));
+ } catch (PGPException | IOException | URISyntaxException e) {
+ throw new JGitInternalException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 09879ea1c..779e85ab6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -202,6 +202,7 @@
3.0.1
1.3.0
2.8.2
+ 1.60
3.1.10
2.22.1
3.8.0
@@ -394,6 +395,12 @@
maven-project-info-reports-plugin
${maven-project-info-reports-plugin-version}
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ 2.1.2.RELEASE
+
@@ -760,6 +767,25 @@
gson
${gson-version}
+
+
+ org.bouncycastle
+ bcpg-jdk15on
+ ${bouncycastle-version}
+
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ ${bouncycastle-version}
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ ${bouncycastle-version}
+
+