| /* |
| * 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)); |
| } |
| } |