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 5f1992cb5..b76d8f987 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 @@ -56,6 +56,7 @@ import java.util.List; import java.util.TimeZone; import java.util.concurrent.atomic.AtomicInteger; +import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.EmptyCommitException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.diff.DiffEntry; @@ -651,6 +652,14 @@ public class CommitCommandTest extends RepositoryTestCase { signingCommitters[0] = signingCommitter; callCount.incrementAndGet(); } + + @Override + public boolean canLocateSigningKey(String gpgSigningKey, + PersonIdent signingCommitter, + CredentialsProvider credentialsProvider) + throws CanceledException { + return false; + } }); // first call should use config, which is expected to be null at @@ -706,6 +715,14 @@ public class CommitCommandTest extends RepositoryTestCase { PersonIdent signingCommitter, CredentialsProvider credentialsProvider) { callCount.incrementAndGet(); } + + @Override + public boolean canLocateSigningKey(String gpgSigningKey, + PersonIdent signingCommitter, + CredentialsProvider credentialsProvider) + throws CanceledException { + return false; + } }); // first call should use config, which is expected to be null at 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 7796c2058..99a23c6e4 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,7 @@ package org.eclipse.jgit.lib; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.lib.internal.BouncyCastleGpgSigner; import org.eclipse.jgit.transport.CredentialsProvider; @@ -95,9 +96,11 @@ public abstract class GpgSigner { * the commit to sign (must not be null and must be * complete to allow proper calculation of payload) * @param gpgSigningKey - * the signing key (passed as is to the GPG signing tool) + * the signing key to locate (passed as is to the GPG signing + * tool as is; eg., value of user.signingkey) * @param committer - * the signing identity (to help with key lookup) + * the signing identity (to help with key lookup in case signing + * key is not specified) * @param credentialsProvider * provider to use when querying for signing key credentials (eg. * passphrase) @@ -106,7 +109,30 @@ public abstract class GpgSigner { * passphrase) */ public abstract void sign(@NonNull CommitBuilder commit, - String gpgSigningKey, @NonNull PersonIdent committer, + @Nullable String gpgSigningKey, @NonNull PersonIdent committer, + CredentialsProvider credentialsProvider) throws CanceledException; + + /** + * Indicates if a signing key is available for the specified committer + * and/or signing key. + * + * @param gpgSigningKey + * the signing key to locate (passed as is to the GPG signing + * tool as is; eg., value of user.signingkey) + * @param committer + * the signing identity (to help with key lookup in case signing + * key is not specified) + * @param credentialsProvider + * provider to use when querying for signing key credentials (eg. + * passphrase) + * @return true if a signing key is available, + * false otherwise + * @throws CanceledException + * when signing was canceled (eg., user aborted when entering + * passphrase) + */ + public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey, + @NonNull PersonIdent committer, CredentialsProvider credentialsProvider) throws CanceledException; } 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 index f447912f0..4d696dd9e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java @@ -59,8 +59,10 @@ 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.annotations.Nullable; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.GpgSignature; @@ -90,27 +92,50 @@ public class BouncyCastleGpgSigner extends GpgSigner { } @Override - public void sign(@NonNull CommitBuilder commit, String gpgSigningKey, - @NonNull PersonIdent committer, - CredentialsProvider credentialsProvider) throws CanceledException { + public boolean canLocateSigningKey(@Nullable String gpgSigningKey, + PersonIdent committer, CredentialsProvider credentialsProvider) + throws CanceledException { + try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt( + credentialsProvider)) { + BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey, + committer, passphrasePrompt); + return gpgKey != null; + } catch (PGPException | IOException | URISyntaxException e) { + return false; + } + } + + private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey, + PersonIdent committer, + BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) + throws CanceledException, UnsupportedCredentialItem, IOException, + PGPException, URISyntaxException { if (gpgSigningKey == null || gpgSigningKey.isEmpty()) { gpgSigningKey = committer.getEmailAddress(); } + BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator( + gpgSigningKey, passphrasePrompt); + + return keyHelper.findSecretKey(); + } + + @Override + public void sign(@NonNull CommitBuilder commit, + @Nullable String gpgSigningKey, @NonNull PersonIdent committer, + CredentialsProvider credentialsProvider) throws CanceledException { try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt( credentialsProvider)) { - BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator( - gpgSigningKey, passphrasePrompt); - - BouncyCastleGpgKey gpgKey = keyHelper.findSecretKey(); + BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey, + committer, passphrasePrompt); PGPSecretKey secretKey = gpgKey.getSecretKey(); if (secretKey == null) { throw new JGitInternalException( JGitText.get().unableToSignCommitNoSecretKey); } - char[] passphrase = passphrasePrompt - .getPassphrase(secretKey.getPublicKey().getFingerprint(), - gpgKey.getOrigin()); + char[] passphrase = passphrasePrompt.getPassphrase( + secretKey.getPublicKey().getFingerprint(), + gpgKey.getOrigin()); PGPPrivateKey privateKey = secretKey .extractPrivateKey(new JcePBESecretKeyDecryptorBuilder() .setProvider(BouncyCastleProvider.PROVIDER_NAME)