/*
 * Copyright (C) 2015, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.transport;

import static org.eclipse.jgit.transport.BaseReceivePack.parseCommand;
import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT;

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.PushCertificate.NonceStatus;
import org.eclipse.jgit.util.IO;

Parser for signed push certificates.
Since:4.0
/** * Parser for signed push certificates. * * @since 4.0 */
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_0_1 = "0.1"; //$NON-NLS-1$ private static interface StringReader {
Throws:
Returns:the next string from the input, up to an optional newline, with newline stripped if present
/** * @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; } @Override public String read() throws IOException { return pckIn.readString(); } } private static class StreamReader implements StringReader { private final Reader reader; 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.

Differences from the PacketLineIn receiver methods:

  • Does not use pkt-line framing.
  • Reads an entire cert in one call rather than depending on a loop in the caller.
  • Does not assume a "push-cert-end" line.
Params:
  • r – input reader; consumed only up until the end of the next signature in the input.
Throws:
Returns:the parsed certificate, or null if the reader was at EOF.
Since:4.1
/** * 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.
Params:
  • str – input string.
Throws:
See Also:
  • fromReader(Reader)
Returns:the parsed certificate.
Since:4.1
/** * 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.
/** The nonce that was sent to the client. */
private String sentNonce;
The nonce the pusher signed.

This may vary from sentNonce; 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.
/** 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.
/** * The maximum time difference which is acceptable between advertised nonce * and received signed nonce. */
private final int nonceSlopLimit; private final boolean enabled; private final NonceGenerator nonceGenerator; private final List<ReceiveCommand> commands = new ArrayList<>();

Constructor for PushCertificateParser.

Params:
  • into – destination repository for the push.
  • cfg – configuration for signed push.
Since:4.1
/** * <p>Constructor for PushCertificateParser.</p> * * @param into * destination repository for the push. * @param cfg * configuration for signed push. * @since 4.1 */
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; } private PushCertificateParser() { db = null; nonceSlopLimit = 0; nonceGenerator = null; enabled = true; }
Parse a push certificate from a reader.
Params:
  • r – input reader; consumed only up until the end of the next signature in the input.
Throws:
See Also:
  • fromReader(Reader)
Returns:the parsed certificate, or null if the reader was at EOF.
Since:4.1
/** * 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(); }
Build the parsed certificate
Throws:
  • IOException – if the push certificate has missing or invalid fields.
Returns:the parsed certificate, or null if push certificates are disabled.
Since:4.1
/** * 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.
Returns:if the repository is configured to use signed pushes in this context.
Since:4.0
/** * 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 enabled; }
Get the whole string for the nonce to be included into the capability advertisement
Returns:the whole string for the nonce to be included into the capability advertisement, or null if push certificates are disabled.
Since:4.0
/** * 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, or null if push certificates are disabled. * @since 4.0 */
public String getAdvertiseNonce() { 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 static String parseHeader(StringReader reader, String header) throws IOException { 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 capabilities has already been handled by the caller.

Params:
  • pckIn – where we take the push certificate header from.
  • stateless – affects nonce verification. When stateless = true the NonceGenerator will allow for some time skew caused by clients disconnected and reconnecting in the stateless smart HTTP protocol.
Throws:
  • IOException – if the certificate from the client is badly malformed or the client disconnects before sending the entire certificate.
Since:4.0
/** * Receive a list of commands from the input encapsulated in a push * certificate. * <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. * * @param pckIn * where we take the push certificate header from. * @param stateless * affects nonce verification. When {@code stateless = true} the * {@code NonceGenerator} will allow for some time skew caused by * clients disconnected and reconnecting in the stateless smart * HTTP protocol. * @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 { 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); } 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 PackProtocolException( JGitText.get().pushCertificateInvalidHeader, eof); } }
Read the PGP signature.

This method assumes the line "-----BEGIN PGP SIGNATURE-----" has already been parsed, and continues parsing until an "-----END PGP SIGNATURE-----" is found, followed by "push-cert-end".

Params:
  • pckIn – where we read the signature from.
Throws:
Since:4.0
/** * 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 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(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 PackProtocolException( JGitText.get().pushCertificateInvalidSignature, eof); } }
Add a command to the signature.
Params:
  • cmd – the command.
Since:4.1
/** * 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.
Params:
  • line – the line read from the wire that produced this command, with optional trailing newline already trimmed.
Throws:
Since:4.0
/** * 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 line) throws PackProtocolException { commands.add(parseCommand(line)); } }