PushCertificateParser.java
- /*
- * Copyright (C) 2015, 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
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.transport;
- import static org.eclipse.jgit.transport.ReceivePack.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
- */
- 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 {
- /**
- * @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.
- * <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.
- * <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 final int nonceSlopLimit;
- private final boolean enabled;
- private final NonceGenerator nonceGenerator;
- private final List<ReceiveCommand> commands = new ArrayList<>();
- /**
- * <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.
- *
- * @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
- *
- * @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 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, 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.
- * <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
- * 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.
- * <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.
- *
- * @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 line) throws PackProtocolException {
- commands.add(parseCommand(line));
- }
- }