From 2343c688b19d9bce4e75ada280be44043ad2dca2 Mon Sep 17 00:00:00 2001 From: Gunnar Wagenknecht Date: Wed, 5 Dec 2018 20:39:07 +0100 Subject: [PATCH] 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 Signed-off-by: Medha Bhargav Prabhala Signed-off-by: Gunnar Wagenknecht --- .../eclipse/jgit/api/CommitCommandTest.java | 99 +++++++++ .../eclipse/jgit/lib/CommitBuilderTest.java | 201 ++++++++++++++++++ .../eclipse/jgit/internal/JGitText.properties | 1 + .../org/eclipse/jgit/api/CommitCommand.java | 72 ++++++- .../UnsupportedSigningFormatException.java | 69 ++++++ .../org/eclipse/jgit/internal/JGitText.java | 1 + .../org/eclipse/jgit/lib/CommitBuilder.java | 95 +++++++++ .../org/eclipse/jgit/lib/GpgSignature.java | 98 +++++++++ .../src/org/eclipse/jgit/lib/GpgSigner.java | 100 +++++++++ 9 files changed, 735 insertions(+), 1 deletion(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnsupportedSigningFormatException.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java 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 3a13aa5a4..9128bb66c 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.io.File; 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.DirCache; 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 @@ public class CommitCommandTest extends RepositoryTestCase { 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 000000000..27ea50561 --- /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 fca9018db..dc26e5868 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 @@ oldIdMustNotBeNull=Expected old ID must not be null 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 d07532c09..00d3842da 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.NoFilepatternException; 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.internal.JGitText; 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 { private Boolean allowEmpty; + private Boolean signCommit; + + private String signingKey; + + private GpgSigner gpgSigner; + /** * Constructor for CommitCommand * @@ -251,6 +261,11 @@ public class CommitCommand extends GitCommand { commit.setParentIds(parents); commit.setTreeId(indexTreeId); + + if (signCommit.booleanValue()) { + gpgSigner.sign(commit, signingKey); + } + ObjectId commitId = odi.insert(commit); odi.flush(); @@ -517,9 +532,10 @@ public class CommitCommand extends GitCommand { * * @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 @@ public class CommitCommand extends GitCommand { // 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 class CommitCommand extends GitCommand { hookOutRedirect.put(hookName, hookStdOut); return this; } + + /** + * Sets the signing key + *

+ * 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. + *

+ *

+ * Note, if none was set or null is specified a default will be + * obtained from the configuration. + *

+ * + * @param signingKey + * signing key (maybe null) + * @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 + * true to sign, false to not sign and + * null 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 000000000..eb5db6ad1 --- /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 a24cff1e4..9da7f1574 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 class JGitText extends TranslationBundle { /***/ 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 c30833d0a..a30f04253 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 static java.nio.charset.StandardCharsets.UTF_8; 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; @@ -155,6 +163,38 @@ public class CommitBuilder { committer = newCommitter; } + /** + * Set the GPG signature of this commit + *

+ * 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: + *

    + *
  1. call {@link #build()} without a signature set to obtain payload
  2. + *
  3. create {@link GpgSignature} from payload
  4. + *
  5. set {@link GpgSignature}
  6. + *
+ *

+ * + * @param newSignature + * the signature to set or null 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 null if the + * commit is not to be signed + * @since 5.3 + */ + public GpgSignature getGpgSignature() { + return gpgSignature; + } + /** * Get the ancestors of this commit. * @@ -316,6 +356,13 @@ public class CommitBuilder { 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(' '); @@ -338,6 +385,50 @@ public class CommitBuilder { return os.toByteArray(); } + /** + * Writes signature to output as per gpgsig + * header. + *

+ * CRLF and CR will be sanitized to LF and signature will have a hanging + * indent of one space starting with line two. + *

+ * + * @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. * @@ -377,6 +468,10 @@ public class CommitBuilder { 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 000000000..663f850f0 --- /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. + *

+ * This returns the ASCII Armor as per + * https://tools.ietf.org/html/rfc4880#section-6.2. + *

+ * + * @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 000000000..e509c9783 --- /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 null. + * + * @return the default signer, or null. + */ + public static GpgSigner getDefault() { + return defaultSigner; + } + + /** + * Set the default signer. + * + * @param signer + * the new default signer, may be null to select no + * default. + */ + public static void setDefault(GpgSigner signer) { + GpgSigner.defaultSigner = signer; + } + + /** + * Signs the specified commit. + * + *

+ * 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)}). + *

+ *

+ * Any existing signature on the commit must be discarded prior obtaining + * the payload via {@link CommitBuilder#build()}. + *

+ * + * @param commit + * 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) + */ + public abstract void sign(@NonNull CommitBuilder commit, + String gpgSigningKey); + +}