aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java459
1 files changed, 349 insertions, 110 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java
index 4bb3d6bf82..463d05393f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java
@@ -1,137 +1,323 @@
/*
- * Copyright (C) 2015, Google Inc.
- * and other copyright owners as documented in the project's IP log.
+ * Copyright (C) 2015, Google Inc. and others
*
- * 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
+ * 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.
*
- * 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.
+ * SPDX-License-Identifier: BSD-3-Clause
*/
+
package org.eclipse.jgit.transport;
import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT;
+import static org.eclipse.jgit.transport.ReceivePack.parseCommand;
import java.io.EOFException;
import java.io.IOException;
+import java.io.Reader;
import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.errors.PackProtocolException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.BaseReceivePack.ReceiveConfig;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+import org.eclipse.jgit.util.IO;
/**
- * Parser for Push certificates
+ * Parser for signed push certificates.
*
* @since 4.0
*/
-public class PushCertificateParser extends PushCertificate {
+public class PushCertificateParser {
+ static final String BEGIN_SIGNATURE =
+ "-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$
+ static final String END_SIGNATURE =
+ "-----END PGP SIGNATURE-----"; //$NON-NLS-1$
+
+ static final String VERSION = "certificate version"; //$NON-NLS-1$
+
+ static final String PUSHER = "pusher"; //$NON-NLS-1$
+
+ static final String PUSHEE = "pushee"; //$NON-NLS-1$
+
+ static final String NONCE = "nonce"; //$NON-NLS-1$
+
+ static final String END_CERT = "push-cert-end"; //$NON-NLS-1$
- private static final String VERSION = "version "; //$NON-NLS-1$
+ private static final String VERSION_0_1 = "0.1"; //$NON-NLS-1$
- private static final String PUSHER = "pusher"; //$NON-NLS-1$
+ private static interface StringReader {
+ /**
+ * @return the next string from the input, up to an optional newline, with
+ * newline stripped if present
+ *
+ * @throws EOFException
+ * if EOF was reached.
+ * @throws IOException
+ * if an error occurred during reading.
+ */
+ String read() throws EOFException, IOException;
+ }
+
+ private static class PacketLineReader implements StringReader {
+ private final PacketLineIn pckIn;
+
+ private PacketLineReader(PacketLineIn pckIn) {
+ this.pckIn = pckIn;
+ }
- private static final String PUSHEE = "pushee"; //$NON-NLS-1$
+ @Override
+ public String read() throws IOException {
+ return pckIn.readString();
+ }
+ }
- private static final String NONCE = "nonce"; //$NON-NLS-1$
+ private static class StreamReader implements StringReader {
+ private final Reader reader;
- /** The individual certificate which is presented to the client */
+ private StreamReader(Reader reader) {
+ this.reader = reader;
+ }
+
+ @Override
+ public String read() throws IOException {
+ // Presize for a command containing 2 SHA-1s and some refname.
+ String line = IO.readLine(reader, 41 * 2 + 64);
+ if (line.isEmpty()) {
+ throw new EOFException();
+ } else if (line.charAt(line.length() - 1) == '\n') {
+ line = line.substring(0, line.length() - 1);
+ }
+ return line;
+ }
+ }
+
+ /**
+ * Parse a push certificate from a reader.
+ * <p>
+ * Differences from the {@link org.eclipse.jgit.transport.PacketLineIn}
+ * receiver methods:
+ * <ul>
+ * <li>Does not use pkt-line framing.</li>
+ * <li>Reads an entire cert in one call rather than depending on a loop in
+ * the caller.</li>
+ * <li>Does not assume a {@code "push-cert-end"} line.</li>
+ * </ul>
+ *
+ * @param r
+ * input reader; consumed only up until the end of the next
+ * signature in the input.
+ * @return the parsed certificate, or null if the reader was at EOF.
+ * @throws org.eclipse.jgit.errors.PackProtocolException
+ * if the certificate is malformed.
+ * @throws java.io.IOException
+ * if there was an error reading from the input.
+ * @since 4.1
+ */
+ public static PushCertificate fromReader(Reader r)
+ throws PackProtocolException, IOException {
+ return new PushCertificateParser().parse(r);
+ }
+
+ /**
+ * Parse a push certificate from a string.
+ *
+ * @see #fromReader(Reader)
+ * @param str
+ * input string.
+ * @return the parsed certificate.
+ * @throws org.eclipse.jgit.errors.PackProtocolException
+ * if the certificate is malformed.
+ * @throws java.io.IOException
+ * if there was an error reading from the input.
+ * @since 4.1
+ */
+ public static PushCertificate fromString(String str)
+ throws PackProtocolException, IOException {
+ return fromReader(new java.io.StringReader(str));
+ }
+
+ private boolean received;
+ private String version;
+ private PushCertificateIdent pusher;
+ private String pushee;
+
+ /** The nonce that was sent to the client. */
private String sentNonce;
/**
- * The nonce the pusher signed. This may vary from pushCertNonce See
- * git-core documentation for reasons.
+ * The nonce the pusher signed.
+ * <p>
+ * This may vary from {@link #sentNonce}; see git-core documentation for
+ * reasons.
*/
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
* and received signed nonce.
*/
- private int nonceSlopLimit;
+ private final int nonceSlopLimit;
- NonceGenerator nonceGenerator;
+ private final boolean enabled;
+ private final NonceGenerator nonceGenerator;
+ private final List<ReceiveCommand> commands = new ArrayList<>();
/**
- * used to build up commandlist
+ * <p>Constructor for PushCertificateParser.</p>
+ *
+ * @param into
+ * destination repository for the push.
+ * @param cfg
+ * configuration for signed push.
+ * @since 4.1
*/
- StringBuilder commandlistBuilder;
+ public PushCertificateParser(Repository into, SignedPushConfig cfg) {
+ if (cfg != null) {
+ nonceSlopLimit = cfg.getCertNonceSlopLimit();
+ nonceGenerator = cfg.getNonceGenerator();
+ } else {
+ nonceSlopLimit = 0;
+ nonceGenerator = null;
+ }
+ db = into;
+ enabled = nonceGenerator != null;
+ }
- /** Database we write the push certificate into. */
- private Repository db;
+ private PushCertificateParser() {
+ db = null;
+ nonceSlopLimit = 0;
+ nonceGenerator = null;
+ enabled = true;
+ }
- PushCertificateParser(Repository into, ReceiveConfig cfg) {
- nonceSlopLimit = cfg.certNonceSlopLimit;
- nonceGenerator = cfg.certNonceSeed != null
- ? new HMACSHA1NonceGenerator(cfg.certNonceSeed)
- : null;
- db = into;
+ /**
+ * Parse a push certificate from a reader.
+ *
+ * @see #fromReader(Reader)
+ * @param r
+ * input reader; consumed only up until the end of the next
+ * signature in the input.
+ * @return the parsed certificate, or null if the reader was at EOF.
+ * @throws org.eclipse.jgit.errors.PackProtocolException
+ * if the certificate is malformed.
+ * @throws java.io.IOException
+ * if there was an error reading from the input.
+ * @since 4.1
+ */
+ public PushCertificate parse(Reader r)
+ throws PackProtocolException, IOException {
+ StreamReader reader = new StreamReader(r);
+ receiveHeader(reader, true);
+ String line;
+ try {
+ while (!(line = reader.read()).isEmpty()) {
+ if (line.equals(BEGIN_SIGNATURE)) {
+ receiveSignature(reader);
+ break;
+ }
+ addCommand(line);
+ }
+ } catch (EOFException e) {
+ // EOF reached, but might have been at a valid state. Let build call below
+ // sort it out.
+ }
+ return build();
}
/**
- * @return if the server is configured to use signed pushes.
+ * Build the parsed certificate
+ *
+ * @return the parsed certificate, or null if push certificates are
+ * disabled.
+ * @throws java.io.IOException
+ * if the push certificate has missing or invalid fields.
+ * @since 4.1
+ */
+ public PushCertificate build() throws IOException {
+ if (!received || !enabled) {
+ return null;
+ }
+ try {
+ return new PushCertificate(version, pusher, pushee, receivedNonce,
+ nonceStatus, Collections.unmodifiableList(commands), signature);
+ } catch (IllegalArgumentException e) {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Whether the repository is configured to use signed pushes in this
+ * context.
+ *
+ * @return if the repository is configured to use signed pushes in this
+ * context.
+ * @since 4.0
*/
public boolean enabled() {
- return nonceGenerator != null;
+ return enabled;
}
/**
+ * Get the whole string for the nonce to be included into the capability
+ * advertisement
+ *
* @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() {
- sentNonce = nonceGenerator.createNonce(db,
- TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
- return CAPABILITY_PUSH_CERT + "=" + sentNonce; //$NON-NLS-1$
+ String nonce = sentNonce();
+ if (nonce == null) {
+ return null;
+ }
+ return CAPABILITY_PUSH_CERT + '=' + nonce;
+ }
+
+ private String sentNonce() {
+ if (sentNonce == null && nonceGenerator != null) {
+ sentNonce = nonceGenerator.createNonce(db,
+ TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
+ }
+ return sentNonce;
}
- private String parseNextLine(PacketLineIn pckIn, String startingWith)
+ private static String parseHeader(StringReader reader, String header)
throws IOException {
- String s = pckIn.readString();
- if (!s.startsWith(startingWith))
- throw new IOException(MessageFormat.format(
- JGitText.get().errorInvalidPushCert,
- "expected " + startingWith)); //$NON-NLS-1$
- return s.substring(startingWith.length());
+ return parseHeader(reader.read(), header);
+ }
+
+ private static String parseHeader(String s, String header)
+ throws IOException {
+ if (s.isEmpty()) {
+ throw new EOFException();
+ }
+ if (s.length() <= header.length()
+ || !s.startsWith(header)
+ || s.charAt(header.length()) != ' ') {
+ throw new PackProtocolException(MessageFormat.format(
+ JGitText.get().pushCertificateInvalidField, header));
+ }
+ return s.substring(header.length() + 1);
}
/**
* Receive a list of commands from the input encapsulated in a push
- * certificate. This method doesn't parse the first line "push-cert \NUL
- * &lt;capabilities&gt;", but assumes the first line including the
+ * certificate.
+ * <p>
+ * This method doesn't parse the first line {@code "push-cert \NUL
+ * <capabilities>"}, but assumes the first line including the
* capabilities has already been handled by the caller.
*
* @param pckIn
@@ -141,65 +327,118 @@ public class PushCertificateParser extends PushCertificate {
* {@code NonceGenerator} will allow for some time skew caused by
* clients disconnected and reconnecting in the stateless smart
* HTTP protocol.
- * @throws IOException
+ * @throws java.io.IOException
* if the certificate from the client is badly malformed or the
* client disconnects before sending the entire certificate.
+ * @since 4.0
*/
public void receiveHeader(PacketLineIn pckIn, boolean stateless)
throws IOException {
+ receiveHeader(new PacketLineReader(pckIn), stateless);
+ }
+
+ private void receiveHeader(StringReader reader, boolean stateless)
+ throws IOException {
try {
- String version = parseNextLine(pckIn, VERSION);
- if (!version.equals("0.1")) { //$NON-NLS-1$
- throw new IOException(MessageFormat.format(
- JGitText.get().errorInvalidPushCert,
- "version not supported")); //$NON-NLS-1$
+ try {
+ version = parseHeader(reader, VERSION);
+ } catch (EOFException e) {
+ return;
+ }
+ received = true;
+ if (!version.equals(VERSION_0_1)) {
+ throw new PackProtocolException(MessageFormat.format(
+ JGitText.get().pushCertificateInvalidFieldValue, VERSION, version));
+ }
+ String rawPusher = parseHeader(reader, PUSHER);
+ pusher = PushCertificateIdent.parse(rawPusher);
+ if (pusher == null) {
+ throw new PackProtocolException(MessageFormat.format(
+ JGitText.get().pushCertificateInvalidFieldValue,
+ PUSHER, rawPusher));
+ }
+ String next = reader.read();
+ if (next.startsWith(PUSHEE)) {
+ pushee = parseHeader(next, PUSHEE);
+ receivedNonce = parseHeader(reader, NONCE);
+ } else {
+ receivedNonce = parseHeader(next, NONCE);
}
- pusher = parseNextLine(pckIn, PUSHER);
- pushee = parseNextLine(pckIn, PUSHEE);
- receivedNonce = parseNextLine(pckIn, NONCE);
- // an empty line
- if (!pckIn.readString().isEmpty()) {
- throw new IOException(MessageFormat.format(
- JGitText.get().errorInvalidPushCert,
- "expected empty line after header")); //$NON-NLS-1$
+ nonceStatus = nonceGenerator != null
+ ? nonceGenerator.verify(
+ receivedNonce, sentNonce(), db, stateless, nonceSlopLimit)
+ : NonceStatus.UNSOLICITED;
+ // An empty line.
+ if (!reader.read().isEmpty()) {
+ throw new PackProtocolException(
+ JGitText.get().pushCertificateInvalidHeader);
}
} catch (EOFException eof) {
- throw new IOException(MessageFormat.format(
- JGitText.get().errorInvalidPushCert,
- "broken push certificate header")); //$NON-NLS-1$
+ throw new PackProtocolException(
+ JGitText.get().pushCertificateInvalidHeader, eof);
}
- nonceStatus = nonceGenerator.verify(receivedNonce, sentNonce, db,
- stateless, nonceSlopLimit);
}
/**
- * Reads the gpg signature. This method assumes the line "-----BEGIN PGP
- * SIGNATURE-----\n" has already been parsed and continues parsing until an
- * "-----END PGP SIGNATURE-----\n" is found.
+ * Read the PGP signature.
+ * <p>
+ * This method assumes the line
+ * {@code "-----BEGIN PGP SIGNATURE-----"} has already been parsed,
+ * and continues parsing until an {@code "-----END PGP SIGNATURE-----"} is
+ * found, followed by {@code "push-cert-end"}.
*
* @param pckIn
* where we read the signature from.
- * @throws IOException
+ * @throws java.io.IOException
+ * if the signature is invalid.
+ * @since 4.0
*/
public void receiveSignature(PacketLineIn pckIn) throws IOException {
+ StringReader reader = new PacketLineReader(pckIn);
+ receiveSignature(reader);
+ if (!reader.read().equals(END_CERT)) {
+ throw new PackProtocolException(
+ JGitText.get().pushCertificateInvalidSignature);
+ }
+ }
+
+ private void receiveSignature(StringReader reader) throws IOException {
+ received = true;
try {
- StringBuilder sig = new StringBuilder();
- String line = pckIn.readStringRaw();
- while (!line.equals("-----END PGP SIGNATURE-----\n")) //$NON-NLS-1$
- sig.append(line);
- signature = sig.toString();
- commandList = commandlistBuilder.toString();
+ StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE).append('\n');
+ String line;
+ while (!(line = reader.read()).equals(END_SIGNATURE)) {
+ sig.append(line).append('\n');
+ }
+ signature = sig.append(END_SIGNATURE).append('\n').toString();
} catch (EOFException eof) {
- throw new IOException(MessageFormat.format(
- JGitText.get().errorInvalidPushCert,
- "broken push certificate signature")); //$NON-NLS-1$
+ throw new PackProtocolException(
+ JGitText.get().pushCertificateInvalidSignature, eof);
}
}
/**
- * @param rawLine
+ * Add a command to the signature.
+ *
+ * @param cmd
+ * the command.
+ * @since 4.1
+ */
+ public void addCommand(ReceiveCommand cmd) {
+ commands.add(cmd);
+ }
+
+ /**
+ * Add a command to the signature.
+ *
+ * @param line
+ * the line read from the wire that produced this
+ * command, with optional trailing newline already trimmed.
+ * @throws org.eclipse.jgit.errors.PackProtocolException
+ * if the raw line cannot be parsed to a command.
+ * @since 4.0
*/
- public void addCommand(String rawLine) {
- commandlistBuilder.append(rawLine);
+ public void addCommand(String line) throws PackProtocolException {
+ commands.add(parseCommand(line));
}
}