Browse Source

Enable GpgSigner to also sign tags

Factor out a common ObjectBuilder as super class of CommitBuilder
and TagBuilder, and make the GpgSigner work on ObjectBuilder.

In order not to break API, add the new method for signing an
ObjectBuilder in a new interface GpgObjectSigner.

The signature for a tag is just tacked onto the end of the tag
message. The message of a signed tag must end in LF.

Bug: 386908
Change-Id: I5e021e3c927f4051825cd7355b129113b949455e
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
tags/v5.11.0.202102031030-m2
Thomas Wolf 3 years ago
parent
commit
5abd8a4feb

+ 13
- 3
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java View File

@@ -38,6 +38,8 @@ import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.GpgObjectSigner;
import org.eclipse.jgit.lib.ObjectBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.util.StringUtils;
@@ -45,7 +47,8 @@ import org.eclipse.jgit.util.StringUtils;
/**
* GPG Signer using BouncyCastle library
*/
public class BouncyCastleGpgSigner extends GpgSigner {
public class BouncyCastleGpgSigner extends GpgSigner
implements GpgObjectSigner {

private static void registerBouncyCastleProviderIfNecessary() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
@@ -98,6 +101,13 @@ public class BouncyCastleGpgSigner extends GpgSigner {
public void sign(@NonNull CommitBuilder commit,
@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
CredentialsProvider credentialsProvider) throws CanceledException {
signObject(commit, gpgSigningKey, committer, credentialsProvider);
}

@Override
public void signObject(@NonNull ObjectBuilder object,
@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
CredentialsProvider credentialsProvider) throws CanceledException {
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
@@ -158,10 +168,10 @@ public class BouncyCastleGpgSigner extends GpgSigner {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (BCPGOutputStream out = new BCPGOutputStream(
new ArmoredOutputStream(buffer))) {
signatureGenerator.update(commit.build());
signatureGenerator.update(object.build());
signatureGenerator.generate().encode(out);
}
commit.setGpgSignature(new GpgSignature(buffer.toByteArray()));
object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
} catch (PGPException | IOException | NoSuchAlgorithmException
| NoSuchProviderException | URISyntaxException e) {
throw new JGitInternalException(e.getMessage(), e);

+ 4
- 4
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018, Salesforce. and others
* Copyright (C) 2018, 2020 Salesforce. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -53,7 +53,7 @@ public class CommitBuilderTest {
private void assertGpgSignatureStringOutcome(String signature,
String expectedOutcome) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
CommitBuilder.writeGpgSignatureString(signature, out);
ObjectBuilder.writeMultiLineHeader(signature, out, true);
String formatted_signature = new String(out.toByteArray(), US_ASCII);
assertEquals(expectedOutcome, formatted_signature);
}
@@ -85,8 +85,8 @@ public class CommitBuilderTest {
String signature = "Ü Ä";
IllegalArgumentException e = assertThrows(
IllegalArgumentException.class,
() -> CommitBuilder.writeGpgSignatureString(signature,
new ByteArrayOutputStream()));
() -> ObjectBuilder.writeMultiLineHeader(signature,
new ByteArrayOutputStream(), true));
String message = MessageFormat.format(JGitText.get().notASCIIString,
signature);
assertEquals(message, e.getMessage());

+ 173
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.lib;

import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.RawParseUtils;
import org.junit.Test;

public class TagBuilderTest {

// @formatter:off
private static final 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-----";

// @formatter:on

private static final String TAGGER_LINE = "A U. Thor <a_u_thor@example.com> 1218123387 +0700";

private static final PersonIdent TAGGER = RawParseUtils
.parsePersonIdent(TAGGER_LINE);

@Test
public void testTagSimple() throws Exception {
TagBuilder t = new TagBuilder();
t.setTag("sometag");
t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
t.setEncoding(US_ASCII);
t.setMessage("Short message only");
t.setTagger(TAGGER);
String tag = new String(t.build(), UTF_8);
String expected = "object 0000000000000000000000000000000000000000\n"
+ "type commit\n" //
+ "tag sometag\n" //
+ "tagger " + TAGGER_LINE + '\n' //
+ "encoding US-ASCII\n" //
+ '\n' //
+ "Short message only";
assertEquals(expected, tag);
}

@Test
public void testTagWithSignatureShortMessageEndsInLF() throws Exception {
TagBuilder t = new TagBuilder();
t.setTag("sometag");
t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
t.setEncoding(US_ASCII);
t.setMessage("Short message only\n");
t.setTagger(TAGGER);
t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
String tag = new String(t.build(), UTF_8);
String expected = "object 0000000000000000000000000000000000000000\n"
+ "type commit\n" //
+ "tag sometag\n" //
+ "tagger " + TAGGER_LINE + '\n' //
+ "encoding US-ASCII\n" //
+ '\n' //
+ "Short message only\n" //
+ SIGNATURE + '\n';
assertEquals(expected, tag);
}

@Test
public void testTagWithSignatureMessageNoLF() {
TagBuilder t = new TagBuilder();
t.setTag("sometag");
t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
t.setEncoding(US_ASCII);
t.setMessage("A message\n\nthat does not end in LF");
t.setTagger(TAGGER);
t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
Throwable ex = assertThrows(Throwable.class, t::build);
assertEquals(JGitText.get().signedTagMessageNoLf, ex.getMessage());
}

@Test
public void testTagWithSignatureNoParagraphsMessage() throws Exception {
TagBuilder t = new TagBuilder();
t.setTag("sometag");
t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
t.setEncoding(US_ASCII);
t.setMessage("A strange\ntag message\n");
t.setTagger(TAGGER);
t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
String tag = new String(t.build(), UTF_8);
String expected = "object 0000000000000000000000000000000000000000\n"
+ "type commit\n" //
+ "tag sometag\n" //
+ "tagger " + TAGGER_LINE + '\n' //
+ "encoding US-ASCII\n" //
+ '\n' //
+ "A strange\ntag message\n" //
+ SIGNATURE + '\n';
assertEquals(expected, tag);
}

@Test
public void testTagWithSignatureLongMessage() throws Exception {
TagBuilder t = new TagBuilder();
t.setTag("sometag");
t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
t.setMessage("Short message\n\nFollowed by explanations.\n");
t.setTagger(TAGGER);
t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
String tag = new String(t.build(), UTF_8);
String expected = "object 0000000000000000000000000000000000000000\n"
+ "type commit\n" //
+ "tag sometag\n" //
+ "tagger " + TAGGER_LINE + '\n' //
+ '\n' //
+ "Short message\n\nFollowed by explanations.\n" //
+ SIGNATURE + '\n';
assertEquals(expected, tag);
}

@Test
public void testTagWithSignatureEmptyMessage() throws Exception {
TagBuilder t = new TagBuilder();
t.setTag("sometag");
t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
t.setTagger(TAGGER);
t.setMessage("");
String emptyMsg = new String(t.build(), UTF_8);
t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
String tag = new String(t.build(), UTF_8);
String expected = "object 0000000000000000000000000000000000000000\n"
+ "type commit\n" //
+ "tag sometag\n" //
+ "tagger " + TAGGER_LINE + '\n' //
+ '\n';
assertEquals(expected, emptyMsg);
assertEquals(expected + SIGNATURE + '\n', tag);
}

@Test
public void testTagWithSignatureOnly() throws Exception {
TagBuilder t = new TagBuilder();
t.setTag("sometag");
t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
t.setTagger(TAGGER);
String emptyMsg = new String(t.build(), UTF_8);
t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
String tag = new String(t.build(), UTF_8);
String expected = "object 0000000000000000000000000000000000000000\n"
+ "type commit\n" //
+ "tag sometag\n" //
+ "tagger " + TAGGER_LINE + '\n' //
+ '\n';
assertEquals(expected, emptyMsg);
assertEquals(expected + SIGNATURE + '\n', tag);
}

}

+ 113
- 6
org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2008-2010, Google Inc. and others
* Copyright (C) 2008, 2020, Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,6 +11,7 @@
package org.eclipse.jgit.revwalk;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -18,6 +19,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;

import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -117,6 +119,7 @@ public class RevTagParseTest extends RepositoryTestCase {
assertNotNull(c.getTagName());
assertEquals(name, c.getTagName());
assertEquals("", c.getFullMessage());
assertNull(c.getRawGpgSignature());

final PersonIdent cTagger = c.getTaggerIdent();
assertNotNull(cTagger);
@@ -128,13 +131,12 @@ public class RevTagParseTest extends RepositoryTestCase {
public void testParseOldStyleNoTagger() throws Exception {
final ObjectId treeId = id("9788669ad918b6fcce64af8882fc9a81cb6aba67");
final String name = "v1.2.3.4.5";
final String message = "test\n" //
+ "\n" //
+ "-----BEGIN PGP SIGNATURE-----\n" //
final String fakeSignature = "-----BEGIN PGP SIGNATURE-----\n" //
+ "Version: GnuPG v1.4.1 (GNU/Linux)\n" //
+ "\n" //
+ "iD8DBQBC0b9oF3Y\n" //
+ "-----END PGP SIGNATURE------n";
+ "-----END PGP SIGNATURE-----";
final String message = "test\n\n" + fakeSignature + '\n';

final StringBuilder body = new StringBuilder();

@@ -167,6 +169,8 @@ public class RevTagParseTest extends RepositoryTestCase {
assertEquals(name, c.getTagName());
assertEquals("test", c.getShortMessage());
assertEquals(message, c.getFullMessage());
assertEquals(fakeSignature + '\n',
new String(c.getRawGpgSignature(), US_ASCII));

assertNull(c.getTaggerIdent());
}
@@ -385,6 +389,108 @@ public class RevTagParseTest extends RepositoryTestCase {
assertEquals("message\n", t.getFullMessage());
}

@Test
public void testParse_gpgSignature() throws Exception {
final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+ "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+ "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+ "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+ "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+ "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+ "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+ "=TClh\n" + "-----END PGP SIGNATURE-----";
ByteArrayOutputStream b = new ByteArrayOutputStream();
b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
.getBytes(UTF_8));
b.write("type tree\n".getBytes(UTF_8));
b.write("tag v1.0\n".getBytes(UTF_8));
b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
b.write('\n');
b.write("message\n\n".getBytes(UTF_8));
b.write(signature.getBytes(US_ASCII));
b.write('\n');

RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
try (RevWalk rw = new RevWalk(db)) {
t.parseCanonical(rw, b.toByteArray());
}

assertEquals("t", t.getTaggerIdent().getName());
assertEquals("message", t.getShortMessage());
assertEquals("message\n\n" + signature + '\n', t.getFullMessage());
String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
assertEquals(signature + '\n', gpgSig);
}

@Test
public void testParse_gpgSignature2() throws Exception {
final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+ "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+ "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+ "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+ "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+ "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+ "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+ "=TClh\n" + "-----END PGP SIGNATURE-----";
ByteArrayOutputStream b = new ByteArrayOutputStream();
b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
.getBytes(UTF_8));
b.write("type tree\n".getBytes(UTF_8));
b.write("tag v1.0\n".getBytes(UTF_8));
b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
b.write('\n');
String message = "message\n\n" + signature.replace("xXXy", "aAAb")
+ '\n';
b.write(message.getBytes(UTF_8));
b.write(signature.getBytes(US_ASCII));
b.write('\n');

RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
try (RevWalk rw = new RevWalk(db)) {
t.parseCanonical(rw, b.toByteArray());
}

assertEquals("t", t.getTaggerIdent().getName());
assertEquals("message", t.getShortMessage());
assertEquals(message + signature + '\n', t.getFullMessage());
String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
assertEquals(signature + '\n', gpgSig);
}

@Test
public void testParse_gpgSignature3() throws Exception {
final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+ "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+ "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+ "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+ "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+ "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+ "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+ "=TClh\n" + "-----END PGP SIGNATURE-----";
ByteArrayOutputStream b = new ByteArrayOutputStream();
b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
.getBytes(UTF_8));
b.write("type tree\n".getBytes(UTF_8));
b.write("tag v1.0\n".getBytes(UTF_8));
b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
b.write('\n');
String message = "message\n\n-----BEGIN PGP SIGNATURE-----\n";
b.write(message.getBytes(UTF_8));
b.write(signature.getBytes(US_ASCII));
b.write('\n');

RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
try (RevWalk rw = new RevWalk(db)) {
t.parseCanonical(rw, b.toByteArray());
}

assertEquals("t", t.getTaggerIdent().getName());
assertEquals("message", t.getShortMessage());
assertEquals(message + signature + '\n', t.getFullMessage());
String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
assertEquals(signature + '\n', gpgSig);
}

@Test
public void testParse_NoMessage() throws Exception {
final String msg = "";
@@ -447,7 +553,8 @@ public class RevTagParseTest extends RepositoryTestCase {
}

@Test
public void testParse_PublicParseMethod() throws CorruptObjectException {
public void testParse_PublicParseMethod()
throws CorruptObjectException, UnsupportedEncodingException {
TagBuilder src = new TagBuilder();
try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
src.setObjectId(fmt.idFor(Constants.OBJ_TREE, new byte[] {}),

+ 1
- 0
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties View File

@@ -617,6 +617,7 @@ shortCompressedStreamAt=Short compressed stream at {0}
shortReadOfBlock=Short read of block.
shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section.
shortSkipOfBlock=Short skip of block.
signedTagMessageNoLf=A non-empty message of a signed tag must end in LF.
signingNotSupportedOnTag=Signing isn't supported on tag operations yet.
signingServiceUnavailable=Signing service is not available
similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100.

+ 1
- 0
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java View File

@@ -645,6 +645,7 @@ public class JGitText extends TranslationBundle {
/***/ public String shortReadOfBlock;
/***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes;
/***/ public String shortSkipOfBlock;
/***/ public String signedTagMessageNoLf;
/***/ public String signingNotSupportedOnTag;
/***/ public String signingServiceUnavailable;
/***/ public String similarityScoreMustBeWithinBounds;

+ 21
- 163
org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java View File

@@ -1,7 +1,7 @@
/*
* Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
* Copyright (C) 2006-2007, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> and others
* Copyright (C) 2006, 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2006, 2020, Shawn O. Pearce <spearce@spearce.org> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -16,14 +16,11 @@ 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;
import org.eclipse.jgit.util.References;

/**
@@ -37,7 +34,7 @@ import org.eclipse.jgit.util.References;
* and obtain a {@link org.eclipse.jgit.revwalk.RevCommit} instance by calling
* {@link org.eclipse.jgit.revwalk.RevWalk#parseCommit(AnyObjectId)}.
*/
public class CommitBuilder {
public class CommitBuilder extends ObjectBuilder {
private static final ObjectId[] EMPTY_OBJECTID_LIST = new ObjectId[0];

private static final byte[] htree = Constants.encodeASCII("tree"); //$NON-NLS-1$
@@ -50,28 +47,17 @@ public class CommitBuilder {

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;

private ObjectId[] parentIds;

private PersonIdent author;

private PersonIdent committer;

private GpgSignature gpgSignature;

private String message;

private Charset encoding;

/**
* Initialize an empty commit.
*/
public CommitBuilder() {
parentIds = EMPTY_OBJECTID_LIST;
encoding = UTF_8;
}

/**
@@ -98,8 +84,9 @@ public class CommitBuilder {
*
* @return the author of this commit (who wrote it).
*/
@Override
public PersonIdent getAuthor() {
return author;
return super.getAuthor();
}

/**
@@ -108,8 +95,9 @@ public class CommitBuilder {
* @param newAuthor
* the new author. Should not be null.
*/
@Override
public void setAuthor(PersonIdent newAuthor) {
author = newAuthor;
super.setAuthor(newAuthor);
}

/**
@@ -131,38 +119,6 @@ public class CommitBuilder {
committer = 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.
*
@@ -238,25 +194,6 @@ public class CommitBuilder {
}
}

/**
* Get the complete commit message.
*
* @return the complete commit message.
*/
public String getMessage() {
return message;
}

/**
* Set the commit message.
*
* @param newMessage
* the commit message. Should not be null.
*/
public void setMessage(String newMessage) {
message = newMessage;
}

/**
* Set the encoding for the commit information.
*
@@ -267,37 +204,10 @@ public class CommitBuilder {
*/
@Deprecated
public void setEncoding(String encodingName) {
encoding = Charset.forName(encodingName);
}

/**
* Set the encoding for the commit information.
*
* @param enc
* the encoding to use.
*/
public void setEncoding(Charset enc) {
encoding = enc;
setEncoding(Charset.forName(encodingName));
}

/**
* Get the encoding that should be used for the commit message text.
*
* @return the encoding that should be used for the commit message text.
*/
public Charset getEncoding() {
return encoding;
}

/**
* Format this builder's state as a commit object.
*
* @return this object in the canonical commit format, suitable for storage
* in a repository.
* @throws java.io.UnsupportedEncodingException
* the encoding specified by {@link #getEncoding()} is not
* supported by this Java runtime.
*/
@Override
public byte[] build() throws UnsupportedEncodingException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
OutputStreamWriter w = new OutputStreamWriter(os, getEncoding());
@@ -326,19 +236,16 @@ public class CommitBuilder {
w.flush();
os.write('\n');

if (getGpgSignature() != null) {
GpgSignature signature = getGpgSignature();
if (signature != null) {
os.write(hgpgsig);
os.write(' ');
writeGpgSignatureString(getGpgSignature().toExternalString(), os);
writeMultiLineHeader(signature.toExternalString(), os,
true);
os.write('\n');
}

if (!References.isSameObject(getEncoding(), UTF_8)) {
os.write(hencoding);
os.write(' ');
os.write(Constants.encodeASCII(getEncoding().name()));
os.write('\n');
}
writeEncoding(getEncoding(), os);

os.write('\n');

@@ -355,58 +262,6 @@ public class CommitBuilder {
return os.toByteArray();
}

/**
* 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. A trailing line break is
* <em>not</em> written; the caller is supposed to terminate the GPG
* signature header by writing a single newline.
* </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 {
int length = in.length();
for (int i = 0; i < length; ++i) {
char ch = in.charAt(i);
switch (ch) {
case '\r':
if (i + 1 < length && in.charAt(i + 1) == '\n') {
++i;
}
if (i + 1 < length) {
out.write('\n');
out.write(' ');
}
break;
case '\n':
if (i + 1 < length) {
out.write('\n');
out.write(' ');
}
break;
default:
// sanity check
if (ch > 127)
throw new IllegalArgumentException(MessageFormat
.format(JGitText.get().notASCIIString, in));
out.write(ch);
break;
}
}
}

/**
* Format this builder's state as a commit object.
*
@@ -439,7 +294,7 @@ public class CommitBuilder {
}

r.append("author ");
r.append(author != null ? author.toString() : "NOT_SET");
r.append(getAuthor() != null ? getAuthor().toString() : "NOT_SET");
r.append("\n");

r.append("committer ");
@@ -447,17 +302,20 @@ public class CommitBuilder {
r.append("\n");

r.append("gpgSignature ");
r.append(gpgSignature != null ? gpgSignature.toString() : "NOT_SET");
GpgSignature signature = getGpgSignature();
r.append(signature != null ? signature.toString()
: "NOT_SET");
r.append("\n");

if (encoding != null && !References.isSameObject(encoding, UTF_8)) {
Charset encoding = getEncoding();
if (!References.isSameObject(encoding, UTF_8)) {
r.append("encoding ");
r.append(encoding.name());
r.append("\n");
}

r.append("\n");
r.append(message != null ? message : "");
r.append(getMessage() != null ? getMessage() : "");
r.append("}");
return r.toString();
}

+ 59
- 0
org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.lib;

import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.transport.CredentialsProvider;

/**
* Creates GPG signatures for Git objects.
*
* @since 5.11
*/
public interface GpgObjectSigner {

/**
* Signs the specified object.
*
* <p>
* Implementors should obtain the payload for signing from the specified
* object via {@link ObjectBuilder#build()} and create a proper
* {@link GpgSignature}. The generated signature must be set on the
* specified {@code object} (see
* {@link ObjectBuilder#setGpgSignature(GpgSignature)}).
* </p>
* <p>
* Any existing signature on the object must be discarded prior obtaining
* the payload via {@link ObjectBuilder#build()}.
* </p>
*
* @param object
* the object to sign (must not be {@code null} and must be
* complete to allow proper calculation of payload)
* @param gpgSigningKey
* the signing key to locate (passed as is to the GPG signing
* tool as is; eg., value of <code>user.signingkey</code>)
* @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)
* @throws CanceledException
* when signing was canceled (eg., user aborted when entering
* passphrase)
*/
void signObject(@NonNull ObjectBuilder object,
@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
CredentialsProvider credentialsProvider) throws CanceledException;

}

+ 225
- 0
org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java View File

@@ -0,0 +1,225 @@
/*
* Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.lib;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Objects;

import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.References;

/**
* Common base class for {@link CommitBuilder} and {@link TagBuilder}.
*
* @since 5.11
*/
public abstract class ObjectBuilder {

/** Byte representation of "encoding". */
private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$

private PersonIdent author;

private GpgSignature gpgSignature;

private String message;

private Charset encoding = StandardCharsets.UTF_8;

/**
* Retrieves the author of this object.
*
* @return the author of this object, or {@code null} if not set yet
*/
protected PersonIdent getAuthor() {
return author;
}

/**
* Sets the author (name, email address, and date) of this object.
*
* @param newAuthor
* the new author, must be non-{@code null}
*/
protected void setAuthor(PersonIdent newAuthor) {
author = Objects.requireNonNull(newAuthor);
}

/**
* Sets the GPG signature of this object.
* <p>
* Note, the signature set here will change the payload of the object, 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 gpgSignature
* the signature to set or {@code null} to unset
* @since 5.3
*/
public void setGpgSignature(@Nullable GpgSignature gpgSignature) {
this.gpgSignature = gpgSignature;
}

/**
* Retrieves the GPG signature of this object.
*
* @return the GPG signature of this object, or {@code null} if the object
* is not signed
* @since 5.3
*/
@Nullable
public GpgSignature getGpgSignature() {
return gpgSignature;
}

/**
* Retrieves the complete message of the object.
*
* @return the complete message; can be {@code null}.
*/
@Nullable
public String getMessage() {
return message;
}

/**
* Sets the message (commit message, or message of an annotated tag).
*
* @param message
* the message.
*/
public void setMessage(@Nullable String message) {
this.message = message;
}

/**
* Retrieves the encoding that should be used for the message text.
*
* @return the encoding that should be used for the message text.
*/
@NonNull
public Charset getEncoding() {
return encoding;
}

/**
* Sets the encoding for the object message.
*
* @param encoding
* the encoding to use.
*/
public void setEncoding(@NonNull Charset encoding) {
this.encoding = encoding;
}

/**
* Format this builder's state as a git object.
*
* @return this object in the canonical git format, suitable for storage in
* a repository.
* @throws java.io.UnsupportedEncodingException
* the encoding specified by {@link #getEncoding()} is not
* supported by this Java runtime.
*/
@NonNull
public abstract byte[] build() throws UnsupportedEncodingException;

/**
* 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. A trailing line break is
* <em>not</em> written; the caller is supposed to terminate the GPG
* signature header by writing a single newline.
* </p>
*
* @param in
* signature string with line breaks
* @param out
* output stream
* @param enforceAscii
* whether to throw {@link IllegalArgumentException} if non-ASCII
* characters are encountered
* @throws IOException
* thrown by the output stream
* @throws IllegalArgumentException
* if the signature string contains non 7-bit ASCII chars and
* {@code enforceAscii == true}
*/
static void writeMultiLineHeader(@NonNull String in,
@NonNull OutputStream out, boolean enforceAscii)
throws IOException, IllegalArgumentException {
int length = in.length();
for (int i = 0; i < length; ++i) {
char ch = in.charAt(i);
switch (ch) {
case '\r':
if (i + 1 < length && in.charAt(i + 1) == '\n') {
++i;
}
if (i + 1 < length) {
out.write('\n');
out.write(' ');
}
break;
case '\n':
if (i + 1 < length) {
out.write('\n');
out.write(' ');
}
break;
default:
// sanity check
if (ch > 127 && enforceAscii)
throw new IllegalArgumentException(MessageFormat
.format(JGitText.get().notASCIIString, in));
out.write(ch);
break;
}
}
}

/**
* Writes an "encoding" header.
*
* @param encoding
* to write
* @param out
* to write to
* @throws IOException
* if writing fails
*/
static void writeEncoding(@NonNull Charset encoding,
@NonNull OutputStream out) throws IOException {
if (!References.isSameObject(encoding, UTF_8)) {
out.write(hencoding);
out.write(' ');
out.write(Constants.encodeASCII(encoding.name()));
out.write('\n');
}
}
}

+ 89
- 48
org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java View File

@@ -1,7 +1,7 @@
/*
* Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2006, 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others
* Copyright (C) 2010, 2020, Chris Aniszczyk <caniszczyk@gmail.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -17,8 +17,13 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;

import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.util.References;

/**
* Mutable builder to construct an annotated tag recording a project state.
@@ -30,17 +35,22 @@ import org.eclipse.jgit.revwalk.RevObject;
* and obtain a {@link org.eclipse.jgit.revwalk.RevTag} instance by calling
* {@link org.eclipse.jgit.revwalk.RevWalk#parseTag(AnyObjectId)}.
*/
public class TagBuilder {
public class TagBuilder extends ObjectBuilder {

private static final byte[] hobject = Constants.encodeASCII("object"); //$NON-NLS-1$

private static final byte[] htype = Constants.encodeASCII("type"); //$NON-NLS-1$

private static final byte[] htag = Constants.encodeASCII("tag"); //$NON-NLS-1$

private static final byte[] htagger = Constants.encodeASCII("tagger"); //$NON-NLS-1$

private ObjectId object;

private int type = Constants.OBJ_BAD;

private String tag;

private PersonIdent tagger;

private String message;

/**
* Get the type of object this tag refers to.
*
@@ -109,7 +119,7 @@ public class TagBuilder {
* @return creator of this tag. May be null.
*/
public PersonIdent getTagger() {
return tagger;
return getAuthor();
}

/**
@@ -119,26 +129,7 @@ public class TagBuilder {
* the creator. May be null.
*/
public void setTagger(PersonIdent taggerIdent) {
tagger = taggerIdent;
}

/**
* Get the complete commit message.
*
* @return the complete commit message.
*/
public String getMessage() {
return message;
}

/**
* Set the tag's message.
*
* @param newMessage
* the tag's message.
*/
public void setMessage(String newMessage) {
message = newMessage;
setAuthor(taggerIdent);
}

/**
@@ -147,31 +138,65 @@ public class TagBuilder {
* @return this object in the canonical annotated tag format, suitable for
* storage in a repository.
*/
public byte[] build() {
@Override
public byte[] build() throws UnsupportedEncodingException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (OutputStreamWriter w = new OutputStreamWriter(os,
UTF_8)) {
w.write("object "); //$NON-NLS-1$
getObjectId().copyTo(w);
w.write('\n');
getEncoding())) {

w.write("type "); //$NON-NLS-1$
w.write(Constants.typeString(getObjectType()));
w.write("\n"); //$NON-NLS-1$
os.write(hobject);
os.write(' ');
getObjectId().copyTo(os);
os.write('\n');

w.write("tag "); //$NON-NLS-1$
os.write(htype);
os.write(' ');
os.write(Constants
.encodeASCII(Constants.typeString(getObjectType())));
os.write('\n');

os.write(htag);
os.write(' ');
w.write(getTag());
w.write("\n"); //$NON-NLS-1$
w.flush();
os.write('\n');

if (getTagger() != null) {
w.write("tagger "); //$NON-NLS-1$
os.write(htagger);
os.write(' ');
w.write(getTagger().toExternalString());
w.write('\n');
w.flush();
os.write('\n');
}

writeEncoding(getEncoding(), os);

os.write('\n');
String msg = getMessage();
if (msg != null) {
w.write(msg);
w.flush();
}

w.write('\n');
if (getMessage() != null)
w.write(getMessage());
GpgSignature signature = getGpgSignature();
if (signature != null) {
if (msg != null && !msg.isEmpty() && !msg.endsWith("\n")) { //$NON-NLS-1$
// If signed, the message *must* end with a linefeed
// character, otherwise signature verification will fail.
// (The signature will have been computed over the payload
// containing the message without LF, but will be verified
// against a payload with the LF.) The signature must start
// on a new line.
throw new JGitInternalException(
JGitText.get().signedTagMessageNoLf);
}
String externalForm = signature.toExternalString();
w.write(externalForm);
w.flush();
if (!externalForm.endsWith("\n")) { //$NON-NLS-1$
os.write('\n');
}
}
} catch (IOException err) {
// This should never occur, the only way to get it above is
// for the ByteArrayOutputStream to throw, but it doesn't.
@@ -185,10 +210,17 @@ public class TagBuilder {
* Format this builder's state as an annotated tag object.
*
* @return this object in the canonical annotated tag format, suitable for
* storage in a repository.
* storage in a repository, or {@code null} if the tag cannot be
* encoded
* @deprecated since 5.11; use {@link #build()} instead
*/
@Deprecated
public byte[] toByteArray() {
return build();
try {
return build();
} catch (UnsupportedEncodingException e) {
return null;
}
}

/** {@inheritDoc} */
@@ -211,14 +243,23 @@ public class TagBuilder {
r.append(tag != null ? tag : "NOT_SET");
r.append("\n");

if (tagger != null) {
if (getTagger() != null) {
r.append("tagger ");
r.append(tagger);
r.append(getTagger());
r.append("\n");
}

Charset encoding = getEncoding();
if (!References.isSameObject(encoding, UTF_8)) {
r.append("encoding ");
r.append(encoding.name());
r.append("\n");
}

r.append("\n");
r.append(message != null ? message : "");
r.append(getMessage() != null ? getMessage() : "");
GpgSignature signature = getGpgSignature();
r.append(signature != null ? signature.toExternalString() : "");
r.append("}");
return r.toString();
}

+ 62
- 0
org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java View File

@@ -18,7 +18,9 @@ import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;

import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -35,6 +37,10 @@ import org.eclipse.jgit.util.StringUtils;
* An annotated tag.
*/
public class RevTag extends RevObject {

private static final byte[] hSignature = Constants
.encodeASCII("-----BEGIN PGP SIGNATURE-----"); //$NON-NLS-1$

/**
* Parse an annotated tag from its canonical format.
*
@@ -171,6 +177,62 @@ public class RevTag extends RevObject {
return RawParseUtils.parsePersonIdent(raw, nameB);
}

private static int nextStart(byte[] prefix, byte[] buffer, int from) {
int stop = buffer.length - prefix.length + 1;
int ptr = from;
if (ptr > 0) {
ptr = RawParseUtils.nextLF(buffer, ptr - 1);
}
while (ptr < stop) {
int lineStart = ptr;
boolean found = true;
for (byte element : prefix) {
if (element != buffer[ptr++]) {
found = false;
break;
}
}
if (found) {
return lineStart;
}
do {
ptr = RawParseUtils.nextLF(buffer, ptr);
} while (ptr < stop && buffer[ptr] == '\n');
}
return -1;
}

/**
* Parse the GPG signature from the raw buffer.
*
* @return contents of the GPG signature; {@code null} if the tag was not
* signed.
* @since 5.11
*/
@Nullable
public final byte[] getRawGpgSignature() {
byte[] raw = buffer;
int msgB = RawParseUtils.tagMessage(raw, 0);
if (msgB < 0) {
return null;
}
// Find the last signature start and return the rest
int start = nextStart(hSignature, raw, msgB);
if (start < 0) {
return null;
}
int next = RawParseUtils.nextLF(raw, start);
while (next < raw.length) {
int newStart = nextStart(hSignature, raw, next);
if (newStart < 0) {
break;
}
start = newStart;
next = RawParseUtils.nextLF(raw, start);
}
return Arrays.copyOfRange(raw, start, raw.length);
}

/**
* Parse the complete tag message and decode it to a string.
* <p>

Loading…
Cancel
Save