Browse Source

Rewrite push certificate parsing

- Consistently return structured data, such as actual ReceiveCommands,
  which is more useful for callers that are doing things other than
  verifying the signature, e.g. recording the set of commands.
- Store the certificate version field, as this is required to be part
  of the signed payload.
- Add a toText() method to recreate the actual payload for signature
  verification. This requires keeping track of the un-chomped command
  strings from the original protocol stream.
- Separate the parser from the certificate itself, so the actual
  PushCertificate object can be immutable. Make a fair attempt at deep
  immutability, but this is not possible with the current mutable
  ReceiveCommand structure.
- Use more detailed error messages that don't involve NON-NLS strings.
- Document null return values more thoroughly. Instead of having the
  undocumented behavior of throwing NPE from certain methods if they
  are not first guarded by enabled(), eliminate enabled() and return
  null from those methods.
- Add tests for parsing a push cert from a section of pkt-line stream
  using a real live stream captured with Wireshark (which, it should
  be noted, uncovered several simply incorrect statements in C git's
  Documentation/technical/pack-protocol.txt).

This is a slightly breaking API change to classes that were
technically public and technically released in 4.0. However, it is
highly unlikely that people were actually depending on public
behavior, since there were no public methods to create
PushCertificates with anything other than null field values, or a
PushCertificateParser that did anything other than infinite loop or
throw exceptions when reading.

Change-Id: I5382193347a8eb1811032d9b32af9651871372d0
stable-4.1
Dave Borowitz 10 years ago
parent
commit
a85e817dc2
  1. 160
      org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java
  2. 5
      org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
  3. 5
      org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
  4. 33
      org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
  5. 150
      org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java
  6. 205
      org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java

160
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java

@ -0,0 +1,160 @@
/*
* Copyright (C) 2015, Google Inc.
*
* 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.transport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.BaseReceivePack.ReceiveConfig;
import org.junit.Test;
/** Test for push certificate parsing. */
public class PushCertificateParserTest {
@Test
public void parseCertFromPktLine() throws Exception {
// Example push certificate generated by C git 2.2.0.
String input = "001ccertificate version 0.1\n"
+ "0041pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
+ "0024pushee git://localhost/repo.git\n"
+ "002anonce 1433954361-bde756572d665bba81d8\n"
+ "0005\n"
+ "00680000000000000000000000000000000000000000"
+ " 6c2b981a177396fb47345b7df3e4d3f854c6bea7"
+ " refs/heads/master\n"
+ "0022-----BEGIN PGP SIGNATURE-----\n"
+ "0016Version: GnuPG v1\n"
+ "0005\n"
+ "0045iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
+ "00459tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
+ "0045htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
+ "00454ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
+ "0045IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
+ "0045+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
+ "000a=XFeC\n"
+ "0020-----END PGP SIGNATURE-----\n"
+ "0012push-cert-end\n";
PacketLineIn pckIn = newPacketLineIn(input);
Config cfg = new Config();
cfg.setString("receive", null, "certnonceseed", "sekret");
Repository db = new InMemoryRepository(
new DfsRepositoryDescription("repo"));
PushCertificateParser parser = new PushCertificateParser(
db, new ReceiveConfig(cfg));
parser.receiveHeader(pckIn, false);
parser.addCommand(pckIn.readStringRaw());
assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readStringRaw());
parser.receiveSignature(pckIn);
PushCertificate cert = parser.build();
assertEquals("0.1", cert.getVersion());
assertEquals("Dave Borowitz", cert.getPusherIdent().getName());
assertEquals("dborowitz@google.com",
cert.getPusherIdent().getEmailAddress());
assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime());
assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset());
assertEquals("git://localhost/repo.git", cert.getPushee());
assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce());
assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce());
assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus());
assertEquals(1, cert.getCommands().size());
ReceiveCommand cmd = cert.getCommands().get(0);
assertEquals("refs/heads/master", cmd.getRefName());
assertEquals(ObjectId.zeroId(), cmd.getOldId());
assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7",
cmd.getNewId().name());
assertEquals(concatPacketLines(input, 0, 6), cert.toText());
String signature = concatPacketLines(input, 7, 16);
assertFalse(signature.contains(PushCertificateParser.BEGIN_SIGNATURE));
assertFalse(signature.contains(PushCertificateParser.END_SIGNATURE));
assertEquals(signature, cert.getSignature());
}
@Test
public void testConcatPacketLines() throws Exception {
String input = "000bline 1\n000bline 2\n000bline 3\n";
assertEquals("line 1\n", concatPacketLines(input, 0, 1));
assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2));
assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3));
assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4));
}
private static String concatPacketLines(String input, int begin, int end)
throws IOException {
StringBuilder result = new StringBuilder();
int i = 0;
PacketLineIn pckIn = newPacketLineIn(input);
while (i < end) {
String line;
try {
line = pckIn.readStringRaw();
} catch (EOFException e) {
break;
}
if (++i > begin) {
result.append(line);
}
}
return result.toString();
}
private static PacketLineIn newPacketLineIn(String input) {
return new PacketLineIn(new ByteArrayInputStream(Constants.encode(input)));
}
}

5
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties

@ -237,7 +237,6 @@ errorDecodingFromFile=Error decoding from file {0}
errorEncodingFromFile=Error encoding from file {0} errorEncodingFromFile=Error encoding from file {0}
errorInBase64CodeReadingStream=Error in Base64 code reading stream. errorInBase64CodeReadingStream=Error in Base64 code reading stream.
errorInPackedRefs=error in packed-refs errorInPackedRefs=error in packed-refs
errorInvalidPushCert=error: invalid protocol: {0}
errorInvalidProtocolWantedOldNewRef=error: invalid protocol: wanted 'old new ref' errorInvalidProtocolWantedOldNewRef=error: invalid protocol: wanted 'old new ref'
errorListing=Error listing {0} errorListing=Error listing {0}
errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on the remote end: {0} errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on the remote end: {0}
@ -473,6 +472,10 @@ pruneLooseUnreferencedObjects=Prune loose, unreferenced objects
pullOnRepoWithoutHEADCurrentlyNotSupported=Pull on repository without HEAD currently not supported pullOnRepoWithoutHEADCurrentlyNotSupported=Pull on repository without HEAD currently not supported
pullTaskName=Pull pullTaskName=Pull
pushCancelled=push cancelled pushCancelled=push cancelled
pushCertificateInvalidField=Push certificate has missing or invalid value for {0}
pushCertificateInvalidFieldValue=Push certificate has missing or invalid value for {0}: {1}
pushCertificateInvalidHeader=Push certificate has invalid header format
pushCertificateInvalidSignature=Push certificate has invalid signature format
pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport
pushNotPermitted=push not permitted pushNotPermitted=push not permitted
rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry

5
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java

@ -296,7 +296,6 @@ public class JGitText extends TranslationBundle {
/***/ public String errorEncodingFromFile; /***/ public String errorEncodingFromFile;
/***/ public String errorInBase64CodeReadingStream; /***/ public String errorInBase64CodeReadingStream;
/***/ public String errorInPackedRefs; /***/ public String errorInPackedRefs;
/***/ public String errorInvalidPushCert;
/***/ public String errorInvalidProtocolWantedOldNewRef; /***/ public String errorInvalidProtocolWantedOldNewRef;
/***/ public String errorListing; /***/ public String errorListing;
/***/ public String errorOccurredDuringUnpackingOnTheRemoteEnd; /***/ public String errorOccurredDuringUnpackingOnTheRemoteEnd;
@ -532,6 +531,10 @@ public class JGitText extends TranslationBundle {
/***/ public String pullOnRepoWithoutHEADCurrentlyNotSupported; /***/ public String pullOnRepoWithoutHEADCurrentlyNotSupported;
/***/ public String pullTaskName; /***/ public String pullTaskName;
/***/ public String pushCancelled; /***/ public String pushCancelled;
/***/ public String pushCertificateInvalidField;
/***/ public String pushCertificateInvalidFieldValue;
/***/ public String pushCertificateInvalidHeader;
/***/ public String pushCertificateInvalidSignature;
/***/ public String pushIsNotSupportedForBundleTransport; /***/ public String pushIsNotSupportedForBundleTransport;
/***/ public String pushNotPermitted; /***/ public String pushNotPermitted;
/***/ public String rawLogMessageDoesNotParseAsLogEntry; /***/ public String rawLogMessageDoesNotParseAsLogEntry;

33
org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java

@ -252,13 +252,19 @@ public abstract class BaseReceivePack {
/** The size of the received pack, including index size */ /** The size of the received pack, including index size */
private Long packSize; private Long packSize;
PushCertificateParser pushCertificateParser; private PushCertificateParser pushCertificateParser;
/** /**
* @return the push certificate used to verify the pushers identity. * Get the push certificate used to verify the pusher's identity.
* <p>
* Only valid after commands are read from the wire.
*
* @return the parsed certificate, or null if push certificates are disabled.
* @throws IOException if the certificate was present but invalid.
* @since 4.1
*/ */
PushCertificate getPushCertificate() { public PushCertificate getPushCertificate() throws IOException {
return pushCertificateParser; return pushCertificateParser.build();
} }
/** /**
@ -1014,9 +1020,10 @@ public abstract class BaseReceivePack {
adv.advertiseCapability(CAPABILITY_REPORT_STATUS); adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
if (allowQuiet) if (allowQuiet)
adv.advertiseCapability(CAPABILITY_QUIET); adv.advertiseCapability(CAPABILITY_QUIET);
if (pushCertificateParser.enabled()) String nonce = pushCertificateParser.getAdvertiseNonce();
adv.advertiseCapability( if (nonce != null) {
pushCertificateParser.getAdvertiseNonce()); adv.advertiseCapability(nonce);
}
if (db.getRefDatabase().performsAtomicTransactions()) if (db.getRefDatabase().performsAtomicTransactions())
adv.advertiseCapability(CAPABILITY_ATOMIC); adv.advertiseCapability(CAPABILITY_ATOMIC);
if (allowOfsDelta) if (allowOfsDelta)
@ -1065,13 +1072,8 @@ public abstract class BaseReceivePack {
!isBiDirectionalPipe()); !isBiDirectionalPipe());
} }
if (line.equals("-----BEGIN PGP SIGNATURE-----\n")) //$NON-NLS-1$ if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) {
pushCertificateParser.receiveSignature(pckIn); pushCertificateParser.receiveSignature(pckIn);
if (pushCertificateParser.enabled()) {
// Must use raw line with optional newline so signed payload can be
// reconstructed.
pushCertificateParser.addCommand(rawLine);
} }
if (line.length() < 83) { if (line.length() < 83) {
@ -1087,6 +1089,11 @@ public abstract class BaseReceivePack {
cmd.setRef(refs.get(cmd.getRefName())); cmd.setRef(refs.get(cmd.getRefName()));
} }
commands.add(cmd); commands.add(cmd);
if (pushCertificateParser.enabled()) {
// Must use raw line with optional newline so signed payload can be
// reconstructed.
pushCertificateParser.addCommand(cmd, rawLine);
}
} }
} }

150
org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java

@ -43,21 +43,26 @@
package org.eclipse.jgit.transport; package org.eclipse.jgit.transport;
import static org.eclipse.jgit.transport.PushCertificateParser.NONCE;
import static org.eclipse.jgit.transport.PushCertificateParser.PUSHEE;
import static org.eclipse.jgit.transport.PushCertificateParser.PUSHER;
import static org.eclipse.jgit.transport.PushCertificateParser.VERSION;
import java.text.MessageFormat;
import java.util.List;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.PersonIdent;
/** /**
* The required information to verify the push. * The required information to verify the push.
* <p>
* A valid certificate will not return null from any getter methods; callers may
* assume that any null value indicates a missing or invalid certificate.
* *
* @since 4.0 * @since 4.0
*/ */
public class PushCertificate { public class PushCertificate {
/** The tuple "name &lt;email&gt;" as presented in the push certificate. */
String pusher;
/** The remote URL the signed push goes to. */
String pushee;
/** What we think about the returned signed nonce. */
NonceStatus nonceStatus;
/** Verification result of the nonce returned during push. */ /** Verification result of the nonce returned during push. */
public enum NonceStatus { public enum NonceStatus {
/** Nonce was not expected, yet client sent one anyway. */ /** Nonce was not expected, yet client sent one anyway. */
@ -72,41 +77,136 @@ public class PushCertificate {
SLOP SLOP
} }
String commandList; private final String version;
String signature; private final PersonIdent pusher;
private final String pushee;
private final String nonce;
private final NonceStatus nonceStatus;
private final List<ReceiveCommand> commands;
private final String rawCommands;
private final String signature;
PushCertificate(String version, PersonIdent pusher, String pushee,
String nonce, NonceStatus nonceStatus, List<ReceiveCommand> commands,
String rawCommands, String signature) {
if (version == null || version.isEmpty()) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().pushCertificateInvalidField, VERSION));
}
if (pusher == null) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().pushCertificateInvalidField, PUSHER));
}
if (pushee == null || pushee.isEmpty()) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().pushCertificateInvalidField, PUSHEE));
}
if (nonce == null || nonce.isEmpty()) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().pushCertificateInvalidField, NONCE));
}
if (nonceStatus == null) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().pushCertificateInvalidField,
"nonce status")); //$NON-NLS-1$
}
if (commands == null || commands.isEmpty()
|| rawCommands == null || rawCommands.isEmpty()) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().pushCertificateInvalidField,
"command")); //$NON-NLS-1$
}
if (signature == null || signature.isEmpty()) {
throw new IllegalArgumentException(
JGitText.get().pushCertificateInvalidSignature);
}
this.version = version;
this.pusher = pusher;
this.pushee = pushee;
this.nonce = nonce;
this.nonceStatus = nonceStatus;
this.commands = commands;
this.rawCommands = rawCommands;
this.signature = signature;
}
/** /**
* @return the signature, consisting of the lines received between the lines * @return the certificate version string.
* '----BEGIN GPG SIGNATURE-----\n' and the '----END GPG * @since 4.1
* SIGNATURE-----\n'
*/ */
public String getSignature() { public String getVersion() {
return signature; return version;
} }
/** /**
* @return the list of commands as one string to be feed into the signature * @return the identity of the pusher who signed the cert, as a string.
* verifier. * @since 4.0
*/ */
public String getCommandList() { public String getPusher() {
return commandList; return pusher.toExternalString();
} }
/** /**
* @return the tuple "name &lt;email&gt;" as presented by the client in the * @return identity of the pusher who signed the cert.
* push certificate. * @since 4.1
*/ */
public String getPusher() { public PersonIdent getPusherIdent() {
return pusher; return pusher;
} }
/** @return URL of the repository the push was originally sent to. */ /**
* @return URL of the repository the push was originally sent to.
* @since 4.0
*/
public String getPushee() { public String getPushee() {
return pushee; return pushee;
} }
/** @return verification status of the nonce embedded in the certificate. */ /**
* @return the raw nonce value that was presented by the pusher.
* @since 4.0
*/
public String getNonce() {
return nonce;
}
/**
* @return verification status of the nonce embedded in the certificate.
* @since 4.0
*/
public NonceStatus getNonceStatus() { public NonceStatus getNonceStatus() {
return nonceStatus; return nonceStatus;
} }
/**
* @return the list of commands as one string to be feed into the signature
* verifier.
* @since 4.1
*/
public List<ReceiveCommand> getCommands() {
return commands;
}
/**
* @return the raw signature, consisting of the lines received between the
* lines {@code "----BEGIN GPG SIGNATURE-----\n"} and
* {@code "----END GPG SIGNATURE-----\n}", exclusive
* @since 4.0
*/
public String getSignature() {
return signature;
}
/** @return text payload of the certificate for the signature verifier. */
public String toText() {
return new StringBuilder()
.append(VERSION).append(' ').append(version).append('\n')
.append(PUSHER).append(' ').append(pusher.toExternalString())
.append('\n')
.append(PUSHEE).append(' ').append(pushee).append('\n')
.append(NONCE).append(' ').append(nonce).append('\n')
.append('\n')
.append(rawCommands)
.toString();
}
} }

205
org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java

@ -42,56 +42,78 @@
*/ */
package org.eclipse.jgit.transport; package org.eclipse.jgit.transport;
import static org.eclipse.jgit.transport.BaseReceivePack.chomp;
import static org.eclipse.jgit.transport.BaseReceivePack.parseCommand;
import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.BaseReceivePack.ReceiveConfig; import org.eclipse.jgit.transport.BaseReceivePack.ReceiveConfig;
import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
import org.eclipse.jgit.util.RawParseUtils;
/** /**
* Parser for Push certificates * Parser for signed push certificates.
* *
* @since 4.0 * @since 4.0
*/ */
public class PushCertificateParser extends PushCertificate { public class PushCertificateParser {
static final String BEGIN_SIGNATURE =
"-----BEGIN PGP SIGNATURE-----\n"; //$NON-NLS-1$
static final String END_SIGNATURE =
"-----END PGP SIGNATURE-----\n"; //$NON-NLS-1$
private static final String VERSION = "version "; //$NON-NLS-1$ static final String VERSION = "certificate version"; //$NON-NLS-1$
private static final String PUSHER = "pusher"; //$NON-NLS-1$ static final String PUSHER = "pusher"; //$NON-NLS-1$
private static final String PUSHEE = "pushee"; //$NON-NLS-1$ static final String PUSHEE = "pushee"; //$NON-NLS-1$
private static final String NONCE = "nonce"; //$NON-NLS-1$ static final String NONCE = "nonce"; //$NON-NLS-1$
/** The individual certificate which is presented to the client */ private static final String VERSION_0_1 = "0.1"; //$NON-NLS-1$
private static final String END_CERT = "push-cert-end\n"; //$NON-NLS-1$
private String version;
private PersonIdent pusher;
private String pushee;
/** The nonce that was sent to the client. */
private String sentNonce; private String sentNonce;
/** /**
* The nonce the pusher signed. This may vary from pushCertNonce See * The nonce the pusher signed.
* git-core documentation for reasons. * <p>
* This may vary from {@link #sentNonce}; see git-core documentation for
* reasons.
*/ */
private String receivedNonce; private String receivedNonce;
private NonceStatus nonceStatus;
private String signature;
/** Database we write the push certificate into. */
private final Repository db;
/** /**
* The maximum time difference which is acceptable between advertised nonce * The maximum time difference which is acceptable between advertised nonce
* and received signed nonce. * and received signed nonce.
*/ */
private int nonceSlopLimit; private final int nonceSlopLimit;
NonceGenerator nonceGenerator; private final NonceGenerator nonceGenerator;
private final List<ReceiveCommand> commands;
/** private final StringBuilder rawCommands;
* used to build up commandlist
*/
StringBuilder commandlistBuilder;
/** Database we write the push certificate into. */
private Repository db;
PushCertificateParser(Repository into, ReceiveConfig cfg) { PushCertificateParser(Repository into, ReceiveConfig cfg) {
nonceSlopLimit = cfg.certNonceSlopLimit; nonceSlopLimit = cfg.certNonceSlopLimit;
@ -99,10 +121,32 @@ public class PushCertificateParser extends PushCertificate {
? new HMACSHA1NonceGenerator(cfg.certNonceSeed) ? new HMACSHA1NonceGenerator(cfg.certNonceSeed)
: null; : null;
db = into; db = into;
commands = new ArrayList<>();
rawCommands = new StringBuilder();
}
/**
* @return the parsed certificate, or null if push certificates are disabled.
* @throws IOException
* if the push certificate has missing or invalid fields.
* @since 4.1
*/
public PushCertificate build() throws IOException {
if (nonceGenerator == null) {
return null;
}
try {
return new PushCertificate(version, pusher, pushee, receivedNonce,
nonceStatus, Collections.unmodifiableList(commands),
rawCommands.toString(), signature);
} catch (IllegalArgumentException e) {
throw new IOException(e.getMessage(), e);
}
} }
/** /**
* @return if the server is configured to use signed pushes. * @return if the server is configured to use signed pushes.
* @since 4.0
*/ */
public boolean enabled() { public boolean enabled() {
return nonceGenerator != null; return nonceGenerator != null;
@ -110,28 +154,43 @@ public class PushCertificateParser extends PushCertificate {
/** /**
* @return the whole string for the nonce to be included into the capability * @return the whole string for the nonce to be included into the capability
* advertisement. * advertisement, or null if push certificates are disabled.
* @since 4.0
*/ */
public String getAdvertiseNonce() { public String getAdvertiseNonce() {
sentNonce = nonceGenerator.createNonce(db, String nonce = sentNonce();
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); if (nonce == null) {
return CAPABILITY_PUSH_CERT + "=" + sentNonce; //$NON-NLS-1$ return null;
}
return CAPABILITY_PUSH_CERT + '=' + nonce;
} }
private String parseNextLine(PacketLineIn pckIn, String startingWith) private String sentNonce() {
if (sentNonce == null && nonceGenerator != null) {
sentNonce = nonceGenerator.createNonce(db,
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
}
return sentNonce;
}
private static String parseHeader(PacketLineIn pckIn, String header)
throws IOException { throws IOException {
String s = pckIn.readString(); String s = pckIn.readString();
if (!s.startsWith(startingWith)) if (s.length() <= header.length()
|| !s.startsWith(header)
|| s.charAt(header.length()) != ' ') {
throw new IOException(MessageFormat.format( throw new IOException(MessageFormat.format(
JGitText.get().errorInvalidPushCert, JGitText.get().pushCertificateInvalidHeader, header));
"expected " + startingWith)); //$NON-NLS-1$ }
return s.substring(startingWith.length()); return s.substring(header.length() + 1);
} }
/** /**
* Receive a list of commands from the input encapsulated in a push * Receive a list of commands from the input encapsulated in a push
* certificate. This method doesn't parse the first line "push-cert \NUL * certificate.
* &lt;capabilities&gt;", but assumes the first line including the * <p>
* This method doesn't parse the first line {@code "push-cert \NUL
* &lt;capabilities&gt;"}, but assumes the first line including the
* capabilities has already been handled by the caller. * capabilities has already been handled by the caller.
* *
* @param pckIn * @param pckIn
@ -144,62 +203,96 @@ public class PushCertificateParser extends PushCertificate {
* @throws IOException * @throws IOException
* if the certificate from the client is badly malformed or the * if the certificate from the client is badly malformed or the
* client disconnects before sending the entire certificate. * client disconnects before sending the entire certificate.
* @since 4.0
*/ */
public void receiveHeader(PacketLineIn pckIn, boolean stateless) public void receiveHeader(PacketLineIn pckIn, boolean stateless)
throws IOException { throws IOException {
try { try {
String version = parseNextLine(pckIn, VERSION); version = parseHeader(pckIn, VERSION);
if (!version.equals("0.1")) { //$NON-NLS-1$ if (!version.equals(VERSION_0_1)) {
throw new IOException(MessageFormat.format( throw new IOException(MessageFormat.format(
JGitText.get().errorInvalidPushCert, JGitText.get().pushCertificateInvalidFieldValue, VERSION, version));
"version not supported")); //$NON-NLS-1$
} }
pusher = parseNextLine(pckIn, PUSHER); String pusherStr = parseHeader(pckIn, PUSHER);
pushee = parseNextLine(pckIn, PUSHEE); pusher = RawParseUtils.parsePersonIdent(pusherStr);
receivedNonce = parseNextLine(pckIn, NONCE); if (pusher == null) {
// an empty line
if (!pckIn.readString().isEmpty()) {
throw new IOException(MessageFormat.format( throw new IOException(MessageFormat.format(
JGitText.get().errorInvalidPushCert, JGitText.get().pushCertificateInvalidFieldValue,
"expected empty line after header")); //$NON-NLS-1$ PUSHER, pusherStr));
}
pushee = parseHeader(pckIn, PUSHEE);
receivedNonce = parseHeader(pckIn, NONCE);
// An empty line.
if (!pckIn.readString().isEmpty()) {
throw new IOException(
JGitText.get().pushCertificateInvalidHeader);
} }
} catch (EOFException eof) { } catch (EOFException eof) {
throw new IOException(MessageFormat.format( throw new IOException(
JGitText.get().errorInvalidPushCert, JGitText.get().pushCertificateInvalidHeader, eof);
"broken push certificate header")); //$NON-NLS-1$
} }
nonceStatus = nonceGenerator.verify(receivedNonce, sentNonce, db, nonceStatus = nonceGenerator != null
stateless, nonceSlopLimit); ? nonceGenerator.verify(
receivedNonce, sentNonce(), db, stateless, nonceSlopLimit)
: NonceStatus.UNSOLICITED;
} }
/** /**
* Reads the gpg signature. This method assumes the line "-----BEGIN PGP * Read the PGP signature.
* SIGNATURE-----\n" has already been parsed and continues parsing until an * <p>
* "-----END PGP SIGNATURE-----\n" is found. * This method assumes the line
* {@code "-----BEGIN PGP SIGNATURE-----\n"} has already been parsed,
* and continues parsing until an {@code "-----END PGP SIGNATURE-----\n"} is
* found, followed by {@code "push-cert-end\n"}.
* *
* @param pckIn * @param pckIn
* where we read the signature from. * where we read the signature from.
* @throws IOException * @throws IOException
* if the signature is invalid.
* @since 4.0
*/ */
public void receiveSignature(PacketLineIn pckIn) throws IOException { public void receiveSignature(PacketLineIn pckIn) throws IOException {
try { try {
StringBuilder sig = new StringBuilder(); StringBuilder sig = new StringBuilder();
String line = pckIn.readStringRaw(); String line;
while (!line.equals("-----END PGP SIGNATURE-----\n")) //$NON-NLS-1$ while (!(line = pckIn.readStringRaw()).equals(END_SIGNATURE)) {
sig.append(line); sig.append(line);
}
signature = sig.toString(); signature = sig.toString();
commandList = commandlistBuilder.toString(); if (!pckIn.readStringRaw().equals(END_CERT)) {
throw new IOException(JGitText.get().pushCertificateInvalidSignature);
}
} catch (EOFException eof) { } catch (EOFException eof) {
throw new IOException(MessageFormat.format( throw new IOException(
JGitText.get().errorInvalidPushCert, JGitText.get().pushCertificateInvalidSignature, eof);
"broken push certificate signature")); //$NON-NLS-1$
} }
} }
/** /**
* Add a command to the signature.
*
* @param cmd
* the command.
* @param rawLine
* the exact line read from the wire that produced this
* command, including trailing newline if present.
* @since 4.1
*/
public void addCommand(ReceiveCommand cmd, String rawLine) {
commands.add(cmd);
rawCommands.append(rawLine);
}
/**
* Add a command to the signature.
*
* @param rawLine * @param rawLine
* the exact line read from the wire that produced this
* command, including trailing newline if present.
* @since 4.0
*/ */
public void addCommand(String rawLine) { public void addCommand(String rawLine) {
commandlistBuilder.append(rawLine); commands.add(parseCommand(chomp(rawLine)));
rawCommands.append(rawLine);
} }
} }

Loading…
Cancel
Save