Allow CommitCommand to sign commits

This change introduces the concept of a GpgSigner which will sign
commits. The GpgSigner will be of a specific implementation (eg.,
Bouncycastle or OpenPgP executable). The actual implementation is not
part of this change.

Bug: 382212
Change-Id: Iea5da1e885c039e06bc8d679d46b124cbe504c8e
Also-by: Medha Bhargav Prabhala <mprabhala@salesforce.com>
Signed-off-by: Medha Bhargav Prabhala <mprabhala@salesforce.com>
Signed-off-by: Gunnar Wagenknecht <gunnar@wagenknecht.org>
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 3a13aa5..9128bb6 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
@@ -53,6 +53,7 @@
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.eclipse.jgit.api.errors.EmptyCommitException;
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
@@ -61,9 +62,11 @@
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -628,4 +631,100 @@ private static void addUnmergedEntry(String file, DirCacheBuilder builder) {
 		builder.add(stage2);
 		builder.add(stage3);
 	}
+
+	@Test
+	public void callSignerWithProperSigningKey() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("file1", "file1");
+			git.add().addFilepattern("file1").call();
+
+			String[] signingKey = new String[1];
+			AtomicInteger callCount = new AtomicInteger();
+			GpgSigner.setDefault(new GpgSigner() {
+				@Override
+				public void sign(CommitBuilder commit, String gpgSigningKey) {
+					signingKey[0] = gpgSigningKey;
+					callCount.incrementAndGet();
+				}
+			});
+
+			// first call should use config, which is expected to be null at
+			// this time
+			git.commit().setSign(Boolean.TRUE).setMessage("initial commit")
+					.call();
+			assertNull(signingKey[0]);
+			assertEquals(1, callCount.get());
+
+			writeTrashFile("file2", "file2");
+			git.add().addFilepattern("file2").call();
+
+			// second commit applies config value
+			String expectedConfigSigningKey = "config-" + System.nanoTime();
+			StoredConfig config = git.getRepository().getConfig();
+			config.setString("user", null, "signingKey",
+					expectedConfigSigningKey);
+			config.save();
+
+			git.commit().setSign(Boolean.TRUE).setMessage("initial commit")
+					.call();
+			assertEquals(expectedConfigSigningKey, signingKey[0]);
+			assertEquals(2, callCount.get());
+
+			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)
+					.setMessage("initial commit").call();
+			assertEquals(expectedSigningKey, signingKey[0]);
+			assertEquals(3, callCount.get());
+		}
+	}
+
+	@Test
+	public void callSignerOnlyWhenSigning() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("file1", "file1");
+			git.add().addFilepattern("file1").call();
+
+			AtomicInteger callCount = new AtomicInteger();
+			GpgSigner.setDefault(new GpgSigner() {
+				@Override
+				public void sign(CommitBuilder commit, String gpgSigningKey) {
+					callCount.incrementAndGet();
+				}
+			});
+
+			// first call should use config, which is expected to be null at
+			// this time
+			git.commit().setMessage("initial commit").call();
+			assertEquals(0, callCount.get());
+
+			writeTrashFile("file2", "file2");
+			git.add().addFilepattern("file2").call();
+
+			// now force signing
+			git.commit().setSign(Boolean.TRUE).setMessage("commit").call();
+			assertEquals(1, callCount.get());
+
+			writeTrashFile("file3", "file3");
+			git.add().addFilepattern("file3").call();
+
+			// now rely on config
+			StoredConfig config = git.getRepository().getConfig();
+			config.setBoolean("commit", null, "gpgSign", true);
+			config.save();
+
+			git.commit().setMessage("commit").call();
+			assertEquals(2, callCount.get());
+
+			writeTrashFile("file4", "file4");
+			git.add().addFilepattern("file4").call();
+
+			// now force "no-sign" (even though config is true)
+			git.commit().setSign(Boolean.FALSE).setMessage("commit").call();
+			assertEquals(2, callCount.get());
+		}
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java
new file mode 100644
index 0000000..27ea505
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.junit.Test;
+
+public class CommitBuilderTest {
+
+	private void assertGpgSignatureStringOutcome(String signature,
+			String expectedOutcome) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		CommitBuilder.writeGpgSignatureString(signature, out);
+		String formatted_signature = new String(out.toByteArray(), US_ASCII);
+		assertEquals(expectedOutcome, formatted_signature);
+	}
+
+	@Test
+	public void writeGpgSignatureString_1() throws Exception {
+		// @formatter:off
+		String signature = "-----BEGIN PGP SIGNATURE-----\n" +
+				"Version: BCPG v1.60\n" +
+				"\n" +
+				"iQEcBAABCAAGBQJb9cVhAAoJEKX+6Axg/6TZeFsH/0CY0WX/z7U8+7S5giFX4wH4\n" +
+				"opvBwqyt6OX8lgNwTwBGHFNt8LdmDCCmKoq/XwkNi3ARVjLhe3gBcKXNoavvPk2Z\n" +
+				"gIg5ChevGkU4afWCOMLVEYnkCBGw2+86XhrK1P7gTHEk1Rd+Yv1ZRDJBY+fFO7yz\n" +
+				"uSBuF5RpEY2sJiIvp27Gub/rY3B5NTR/feO/z+b9oiP/fMUhpRwG5KuWUsn9NPjw\n" +
+				"3tvbgawYpU/2UnS+xnavMY4t2fjRYjsoxndPLb2MUX8X7vC7FgWLBlmI/rquLZVM\n" +
+				"IQEKkjnA+lhejjK1rv+ulq4kGZJFKGYWYYhRDwFg5PTkzhudhN2SGUq5Wxq1Eg4=\n" +
+				"=b9OI\n" +
+				"-----END PGP SIGNATURE-----";
+		String expectedOutcome = "-----BEGIN PGP SIGNATURE-----\n" +
+				" Version: BCPG v1.60\n" +
+				" \n" +
+				" iQEcBAABCAAGBQJb9cVhAAoJEKX+6Axg/6TZeFsH/0CY0WX/z7U8+7S5giFX4wH4\n" +
+				" opvBwqyt6OX8lgNwTwBGHFNt8LdmDCCmKoq/XwkNi3ARVjLhe3gBcKXNoavvPk2Z\n" +
+				" gIg5ChevGkU4afWCOMLVEYnkCBGw2+86XhrK1P7gTHEk1Rd+Yv1ZRDJBY+fFO7yz\n" +
+				" uSBuF5RpEY2sJiIvp27Gub/rY3B5NTR/feO/z+b9oiP/fMUhpRwG5KuWUsn9NPjw\n" +
+				" 3tvbgawYpU/2UnS+xnavMY4t2fjRYjsoxndPLb2MUX8X7vC7FgWLBlmI/rquLZVM\n" +
+				" IQEKkjnA+lhejjK1rv+ulq4kGZJFKGYWYYhRDwFg5PTkzhudhN2SGUq5Wxq1Eg4=\n" +
+				" =b9OI\n" +
+				" -----END PGP SIGNATURE-----";
+		// @formatter:on
+		assertGpgSignatureStringOutcome(signature, expectedOutcome);
+	}
+
+	@Test
+	public void writeGpgSignatureString_failsForNonAscii() throws Exception {
+		String signature = "Ü Ä";
+		try {
+			CommitBuilder.writeGpgSignatureString(signature,
+					new ByteArrayOutputStream());
+			fail("Exception expected");
+		} catch (IllegalArgumentException e) {
+			// good
+			String message = MessageFormat.format(JGitText.get().notASCIIString,
+					signature);
+			assertEquals(message, e.getMessage());
+		}
+	}
+
+	@Test
+	public void writeGpgSignatureString_oneLineNotModified() throws Exception {
+		String signature = "    A string   ";
+		String expectedOutcome = signature;
+		assertGpgSignatureStringOutcome(signature, expectedOutcome);
+	}
+
+	@Test
+	public void writeGpgSignatureString_preservesRandomWhitespace()
+			throws Exception {
+		// @formatter:off
+		String signature = "    String with    \n"
+				+ "Line 2\n"
+				+ " Line 3\n"
+				+ "Line 4   \n"
+				+ "  Line 5  ";
+		String expectedOutcome = "    String with    \n"
+				+ " Line 2\n"
+				+ "  Line 3\n"
+				+ " Line 4   \n"
+				+ "   Line 5  ";
+		// @formatter:on
+		assertGpgSignatureStringOutcome(signature, expectedOutcome);
+	}
+
+	@Test
+	public void writeGpgSignatureString_replaceCR() throws Exception {
+		// @formatter:off
+		String signature = "String with \r"
+				+ "Line 2\r"
+				+ "Line 3\r"
+				+ "Line 4\r"
+				+ "Line 5";
+		String expectedOutcome = "String with \n"
+				+ " Line 2\n"
+				+ " Line 3\n"
+				+ " Line 4\n"
+				+ " Line 5";
+		// @formatter:on
+		assertGpgSignatureStringOutcome(signature, expectedOutcome);
+	}
+
+	@Test
+	public void writeGpgSignatureString_replaceCRLF() throws Exception {
+		// @formatter:off
+		String signature = "String with \r\n"
+				+ "Line 2\r\n"
+				+ "Line 3\r\n"
+				+ "Line 4\r\n"
+				+ "Line 5";
+		String expectedOutcome = "String with \n"
+				+ " Line 2\n"
+				+ " Line 3\n"
+				+ " Line 4\n"
+				+ " Line 5";
+		// @formatter:on
+		assertGpgSignatureStringOutcome(signature, expectedOutcome);
+	}
+
+	@Test
+	public void writeGpgSignatureString_replaceCRLFMixed() throws Exception {
+		// @formatter:off
+		String signature = "String with \r"
+				+ "Line 2\r\n"
+				+ "Line 3\r"
+				+ "Line 4\r\n"
+				+ "Line 5";
+		String expectedOutcome = "String with \n"
+				+ " Line 2\n"
+				+ " Line 3\n"
+				+ " Line 4\n"
+				+ " Line 5";
+		// @formatter:on
+		assertGpgSignatureStringOutcome(signature, expectedOutcome);
+	}
+
+	@Test
+	public void setGpgSignature() throws Exception {
+		GpgSignature dummy = new GpgSignature(new byte[0]);
+
+		CommitBuilder builder = new CommitBuilder();
+		assertNull(builder.getGpgSignature());
+
+		builder.setGpgSignature(dummy);
+		assertSame(dummy, builder.getGpgSignature());
+
+		builder.setGpgSignature(null);
+		assertNull(builder.getGpgSignature());
+	}
+}
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 fca9018..dc26e58 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -505,6 +505,7 @@
 onlyAlreadyUpToDateAndFastForwardMergesAreAvailable=only already-up-to-date and fast forward merges are available
 onlyOneFetchSupported=Only one fetch supported
 onlyOneOperationCallPerConnectionIsSupported=Only one operation call per connection is supported.
+onlyOpenPgpSupportedForSigning=OpenPGP is the only supported signing option with JGit at this time (gpg.format must be set to openpgp).
 openFilesMustBeAtLeast1=Open files must be >= 1
 openingConnection=Opening connection
 operationCanceled=Operation {0} was canceled
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 d07532c..00d3842 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -61,6 +61,7 @@
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.api.errors.NoMessageException;
 import org.eclipse.jgit.api.errors.UnmergedPathsException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuildIterator;
@@ -76,6 +77,9 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -139,6 +143,12 @@ public class CommitCommand extends GitCommand<RevCommit> {
 
 	private Boolean allowEmpty;
 
+	private Boolean signCommit;
+
+	private String signingKey;
+
+	private GpgSigner gpgSigner;
+
 	/**
 	 * Constructor for CommitCommand
 	 *
@@ -251,6 +261,11 @@ public RevCommit call() throws GitAPIException, NoHeadException,
 
 				commit.setParentIds(parents);
 				commit.setTreeId(indexTreeId);
+
+				if (signCommit.booleanValue()) {
+					gpgSigner.sign(commit, signingKey);
+				}
+
 				ObjectId commitId = odi.insert(commit);
 				odi.flush();
 
@@ -517,9 +532,10 @@ private int lookupOnly(String pathString) {
 	 *
 	 * @throws NoMessageException
 	 *             if the commit message has not been specified
+	 * @throws UnsupportedSigningFormatException if the configured gpg.format is not supported
 	 */
 	private void processOptions(RepositoryState state, RevWalk rw)
-			throws NoMessageException {
+			throws NoMessageException, UnsupportedSigningFormatException {
 		if (committer == null)
 			committer = new PersonIdent(repo);
 		if (author == null && !amend)
@@ -572,6 +588,22 @@ private void processOptions(RepositoryState state, RevWalk rw)
 			// as long as we don't support -C option we have to have
 			// an explicit message
 			throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
+
+		GpgConfig gpgConfig = new GpgConfig(repo.getConfig());
+		if (signCommit == null) {
+			signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE
+					: Boolean.FALSE;
+		}
+		if (signingKey == null) {
+			signingKey = gpgConfig.getSigningKey();
+		}
+		if (gpgSigner == null) {
+			if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
+				throw new UnsupportedSigningFormatException(
+						JGitText.get().onlyOpenPgpSupportedForSigning);
+			}
+			gpgSigner = GpgSigner.getDefault();
+		}
 	}
 
 	private boolean isMergeDuringRebase(RepositoryState state) {
@@ -873,4 +905,42 @@ public CommitCommand setHookOutputStream(String hookName,
 		hookOutRedirect.put(hookName, hookStdOut);
 		return this;
 	}
+
+	/**
+	 * Sets the signing key
+	 * <p>
+	 * Per spec of user.signingKey: this will be sent to the GPG program as is,
+	 * i.e. can be anything supported by the GPG program.
+	 * </p>
+	 * <p>
+	 * Note, if none was set or <code>null</code> is specified a default will be
+	 * obtained from the configuration.
+	 * </p>
+	 *
+	 * @param signingKey
+	 *            signing key (maybe <code>null</code>)
+	 * @return {@code this}
+	 * @since 5.3
+	 */
+	public CommitCommand setSigningKey(String signingKey) {
+		checkCallable();
+		this.signingKey = signingKey;
+		return this;
+	}
+
+	/**
+	 * Sets whether the commit should be signed.
+	 *
+	 * @param sign
+	 *            <code>true</code> to sign, <code>false</code> to not sign and
+	 *            <code>null</code> for default behavior (read from
+	 *            configuration)
+	 * @return {@code this}
+	 * @since 5.3
+	 */
+	public CommitCommand setSign(Boolean sign) {
+		checkCallable();
+		this.signCommit = sign;
+		return this;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnsupportedSigningFormatException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnsupportedSigningFormatException.java
new file mode 100644
index 0000000..eb5db6a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnsupportedSigningFormatException.java
@@ -0,0 +1,69 @@
+/*
+ * 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.api.errors;
+
+/**
+ * Exception thrown when the configured gpg.format is not supported.
+ *
+ * @since 5.3
+ */
+public class UnsupportedSigningFormatException extends GitAPIException {
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * Constructor for UnsupportedGpgFormatException
+	 *
+	 * @param message
+	 *            error message
+	 * @param cause
+	 *            a {@link java.lang.Throwable}
+	 */
+	public UnsupportedSigningFormatException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * Constructor for UnsupportedGpgFormatException
+	 *
+	 * @param message
+	 *            error message
+	 */
+	public UnsupportedSigningFormatException(String message) {
+		super(message);
+	}
+}
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 a24cff1..9da7f15 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -566,6 +566,7 @@ public static JGitText get() {
 	/***/ public String onlyAlreadyUpToDateAndFastForwardMergesAreAvailable;
 	/***/ public String onlyOneFetchSupported;
 	/***/ public String onlyOneOperationCallPerConnectionIsSupported;
+	/***/ public String onlyOpenPgpSupportedForSigning;
 	/***/ public String openFilesMustBeAtLeast1;
 	/***/ public String openingConnection;
 	/***/ public String operationCanceled;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
index c30833d..a30f042 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
@@ -49,11 +49,15 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
+import java.text.MessageFormat;
 import java.util.List;
 
+import org.eclipse.jgit.internal.JGitText;
+
 /**
  * Mutable builder to construct a commit recording the state of a project.
  *
@@ -76,6 +80,8 @@ public class CommitBuilder {
 
 	private static final byte[] hcommitter = Constants.encodeASCII("committer"); //$NON-NLS-1$
 
+	private static final byte[] hgpgsig = Constants.encodeASCII("gpgsig"); //$NON-NLS-1$
+
 	private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$
 
 	private ObjectId treeId;
@@ -86,6 +92,8 @@ public class CommitBuilder {
 
 	private PersonIdent committer;
 
+	private GpgSignature gpgSignature;
+
 	private String message;
 
 	private Charset encoding;
@@ -156,6 +164,38 @@ public void setCommitter(PersonIdent newCommitter) {
 	}
 
 	/**
+	 * Set the GPG signature of this commit
+	 * <p>
+	 * Note, the signature set here will change the payload of the commit, i.e.
+	 * the output of {@link #build()} will include the signature. Thus, the
+	 * typical flow will be:
+	 * <ol>
+	 * <li>call {@link #build()} without a signature set to obtain payload</li>
+	 * <li>create {@link GpgSignature} from payload</li>
+	 * <li>set {@link GpgSignature}</li>
+	 * </ol>
+	 * </p>
+	 *
+	 * @param newSignature
+	 *            the signature to set or <code>null</code> to unset
+	 * @since 5.3
+	 */
+	public void setGpgSignature(GpgSignature newSignature) {
+		gpgSignature = newSignature;
+	}
+
+	/**
+	 * Get the GPG signature of this commit.
+	 *
+	 * @return the GPG signature of this commit, maybe <code>null</code> if the
+	 *         commit is not to be signed
+	 * @since 5.3
+	 */
+	public GpgSignature getGpgSignature() {
+		return gpgSignature;
+	}
+
+	/**
 	 * Get the ancestors of this commit.
 	 *
 	 * @return the ancestors of this commit. Never null.
@@ -316,6 +356,13 @@ public Charset getEncoding() {
 			w.flush();
 			os.write('\n');
 
+			if (getGpgSignature() != null) {
+				os.write(hgpgsig);
+				os.write(' ');
+				writeGpgSignatureString(getGpgSignature().toExternalString(), os);
+				os.write('\n');
+			}
+
 			if (getEncoding() != UTF_8) {
 				os.write(hencoding);
 				os.write(' ');
@@ -339,6 +386,50 @@ public Charset getEncoding() {
 	}
 
 	/**
+	 * Writes signature to output as per <a href=
+	 * "https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L66,L89">gpgsig
+	 * header</a>.
+	 * <p>
+	 * CRLF and CR will be sanitized to LF and signature will have a hanging
+	 * indent of one space starting with line two.
+	 * </p>
+	 *
+	 * @param in
+	 *            signature string with line breaks
+	 * @param out
+	 *            output stream
+	 * @throws IOException
+	 *             thrown by the output stream
+	 * @throws IllegalArgumentException
+	 *             if the signature string contains non 7-bit ASCII chars
+	 */
+	static void writeGpgSignatureString(String in, OutputStream out)
+			throws IOException, IllegalArgumentException {
+		for (int i = 0; i < in.length(); ++i) {
+			char ch = in.charAt(i);
+			if (ch == '\r') {
+				if (i + 1 < in.length() && in.charAt(i + 1) == '\n') {
+					out.write('\n');
+					out.write(' ');
+					++i;
+				} else {
+					out.write('\n');
+					out.write(' ');
+				}
+			} else if (ch == '\n') {
+				out.write('\n');
+				out.write(' ');
+			} else {
+				// sanity check
+				if (ch > 127)
+					throw new IllegalArgumentException(MessageFormat
+							.format(JGitText.get().notASCIIString, in));
+				out.write(ch);
+			}
+		}
+	}
+
+	/**
 	 * Format this builder's state as a commit object.
 	 *
 	 * @return this object in the canonical commit format, suitable for storage
@@ -377,6 +468,10 @@ public String toString() {
 		r.append(committer != null ? committer.toString() : "NOT_SET");
 		r.append("\n");
 
+		r.append("gpgSignature ");
+		r.append(gpgSignature != null ? gpgSignature.toString() : "NOT_SET");
+		r.append("\n");
+
 		if (encoding != null && encoding != UTF_8) {
 			r.append("encoding ");
 			r.append(encoding.name());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java
new file mode 100644
index 0000000..663f850
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import java.io.Serializable;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A structure for holding GPG signature together with additional related data.
+ *
+ * @since 5.3
+ */
+public class GpgSignature implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private byte[] signature;
+
+	/**
+	 * Creates a new instance with the specified signature
+	 *
+	 * @param signature
+	 *            the signature
+	 */
+	public GpgSignature(@NonNull byte[] signature) {
+		this.signature = signature;
+	}
+
+	/**
+	 * Format for Git storage.
+	 * <p>
+	 * This returns the ASCII Armor as per
+	 * https://tools.ietf.org/html/rfc4880#section-6.2.
+	 * </p>
+	 *
+	 * @return a string of the signature ready to be embedded in a Git object
+	 */
+	public String toExternalString() {
+		return new String(signature, US_ASCII);
+	}
+
+	/** {@inheritDoc} */
+	@Override
+	@SuppressWarnings("nls")
+	public String toString() {
+		final StringBuilder r = new StringBuilder();
+
+		r.append("GpgSignature[");
+		r.append(
+				this.signature != null ? "length " + signature.length : "null");
+		r.append("]");
+
+		return r.toString();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
new file mode 100644
index 0000000..e509c97
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * Creates GPG signatures for Git objects.
+ *
+ * @since 5.3
+ */
+public abstract class GpgSigner {
+
+	private static GpgSigner defaultSigner;
+
+	/**
+	 * Get the default signer, or <code>null</code>.
+	 *
+	 * @return the default signer, or <code>null</code>.
+	 */
+	public static GpgSigner getDefault() {
+		return defaultSigner;
+	}
+
+	/**
+	 * Set the default signer.
+	 *
+	 * @param signer
+	 *            the new default signer, may be <code>null</code> to select no
+	 *            default.
+	 */
+	public static void setDefault(GpgSigner signer) {
+		GpgSigner.defaultSigner = signer;
+	}
+
+	/**
+	 * Signs the specified commit.
+	 *
+	 * <p>
+	 * Implementors should obtain the payload for signing from the specified
+	 * commit via {@link CommitBuilder#build()} and create a proper
+	 * {@link GpgSignature}. The generated signature must be set on the
+	 * specified {@code commit} (see
+	 * {@link CommitBuilder#setGpgSignature(GpgSignature)}).
+	 * </p>
+	 * <p>
+	 * Any existing signature on the commit must be discarded prior obtaining
+	 * the payload via {@link CommitBuilder#build()}.
+	 * </p>
+	 *
+	 * @param commit
+	 *            the commit to sign (must not be <code>null</code> and must be
+	 *            complete to allow proper calculation of payload)
+	 * @param gpgSigningKey
+	 *            the signing key (passed as is to the GPG signing tool)
+	 */
+	public abstract void sign(@NonNull CommitBuilder commit,
+			String gpgSigningKey);
+
+}