diff options
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.java | 459 |
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 - * <capabilities>", 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)); } } |