PushCertificateParser.java

  1. /*
  2.  * Copyright (C) 2015, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.transport;

  11. import static org.eclipse.jgit.transport.ReceivePack.parseCommand;
  12. import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT;

  13. import java.io.EOFException;
  14. import java.io.IOException;
  15. import java.io.Reader;
  16. import java.text.MessageFormat;
  17. import java.util.ArrayList;
  18. import java.util.Collections;
  19. import java.util.List;
  20. import java.util.concurrent.TimeUnit;

  21. import org.eclipse.jgit.errors.PackProtocolException;
  22. import org.eclipse.jgit.internal.JGitText;
  23. import org.eclipse.jgit.lib.Repository;
  24. import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
  25. import org.eclipse.jgit.util.IO;

  26. /**
  27.  * Parser for signed push certificates.
  28.  *
  29.  * @since 4.0
  30.  */
  31. public class PushCertificateParser {
  32.     static final String BEGIN_SIGNATURE =
  33.             "-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$
  34.     static final String END_SIGNATURE =
  35.             "-----END PGP SIGNATURE-----"; //$NON-NLS-1$

  36.     static final String VERSION = "certificate version"; //$NON-NLS-1$

  37.     static final String PUSHER = "pusher"; //$NON-NLS-1$

  38.     static final String PUSHEE = "pushee"; //$NON-NLS-1$

  39.     static final String NONCE = "nonce"; //$NON-NLS-1$

  40.     static final String END_CERT = "push-cert-end"; //$NON-NLS-1$

  41.     private static final String VERSION_0_1 = "0.1"; //$NON-NLS-1$

  42.     private static interface StringReader {
  43.         /**
  44.          * @return the next string from the input, up to an optional newline, with
  45.          *         newline stripped if present
  46.          *
  47.          * @throws EOFException
  48.          *             if EOF was reached.
  49.          * @throws IOException
  50.          *             if an error occurred during reading.
  51.          */
  52.         String read() throws EOFException, IOException;
  53.     }

  54.     private static class PacketLineReader implements StringReader {
  55.         private final PacketLineIn pckIn;

  56.         private PacketLineReader(PacketLineIn pckIn) {
  57.             this.pckIn = pckIn;
  58.         }

  59.         @Override
  60.         public String read() throws IOException {
  61.             return pckIn.readString();
  62.         }
  63.     }

  64.     private static class StreamReader implements StringReader {
  65.         private final Reader reader;

  66.         private StreamReader(Reader reader) {
  67.             this.reader = reader;
  68.         }

  69.         @Override
  70.         public String read() throws IOException {
  71.             // Presize for a command containing 2 SHA-1s and some refname.
  72.             String line = IO.readLine(reader, 41 * 2 + 64);
  73.             if (line.isEmpty()) {
  74.                 throw new EOFException();
  75.             } else if (line.charAt(line.length() - 1) == '\n') {
  76.                 line = line.substring(0, line.length() - 1);
  77.             }
  78.             return line;
  79.         }
  80.     }

  81.     /**
  82.      * Parse a push certificate from a reader.
  83.      * <p>
  84.      * Differences from the {@link org.eclipse.jgit.transport.PacketLineIn}
  85.      * receiver methods:
  86.      * <ul>
  87.      * <li>Does not use pkt-line framing.</li>
  88.      * <li>Reads an entire cert in one call rather than depending on a loop in
  89.      * the caller.</li>
  90.      * <li>Does not assume a {@code "push-cert-end"} line.</li>
  91.      * </ul>
  92.      *
  93.      * @param r
  94.      *            input reader; consumed only up until the end of the next
  95.      *            signature in the input.
  96.      * @return the parsed certificate, or null if the reader was at EOF.
  97.      * @throws org.eclipse.jgit.errors.PackProtocolException
  98.      *             if the certificate is malformed.
  99.      * @throws java.io.IOException
  100.      *             if there was an error reading from the input.
  101.      * @since 4.1
  102.      */
  103.     public static PushCertificate fromReader(Reader r)
  104.             throws PackProtocolException, IOException {
  105.         return new PushCertificateParser().parse(r);
  106.     }

  107.     /**
  108.      * Parse a push certificate from a string.
  109.      *
  110.      * @see #fromReader(Reader)
  111.      * @param str
  112.      *            input string.
  113.      * @return the parsed certificate.
  114.      * @throws org.eclipse.jgit.errors.PackProtocolException
  115.      *             if the certificate is malformed.
  116.      * @throws java.io.IOException
  117.      *             if there was an error reading from the input.
  118.      * @since 4.1
  119.      */
  120.     public static PushCertificate fromString(String str)
  121.             throws PackProtocolException, IOException {
  122.         return fromReader(new java.io.StringReader(str));
  123.     }

  124.     private boolean received;
  125.     private String version;
  126.     private PushCertificateIdent pusher;
  127.     private String pushee;

  128.     /** The nonce that was sent to the client. */
  129.     private String sentNonce;

  130.     /**
  131.      * The nonce the pusher signed.
  132.      * <p>
  133.      * This may vary from {@link #sentNonce}; see git-core documentation for
  134.      * reasons.
  135.      */
  136.     private String receivedNonce;

  137.     private NonceStatus nonceStatus;
  138.     private String signature;

  139.     /** Database we write the push certificate into. */
  140.     private final Repository db;

  141.     /**
  142.      * The maximum time difference which is acceptable between advertised nonce
  143.      * and received signed nonce.
  144.      */
  145.     private final int nonceSlopLimit;

  146.     private final boolean enabled;
  147.     private final NonceGenerator nonceGenerator;
  148.     private final List<ReceiveCommand> commands = new ArrayList<>();

  149.     /**
  150.      * <p>Constructor for PushCertificateParser.</p>
  151.      *
  152.      * @param into
  153.      *            destination repository for the push.
  154.      * @param cfg
  155.      *            configuration for signed push.
  156.      * @since 4.1
  157.      */
  158.     public PushCertificateParser(Repository into, SignedPushConfig cfg) {
  159.         if (cfg != null) {
  160.             nonceSlopLimit = cfg.getCertNonceSlopLimit();
  161.             nonceGenerator = cfg.getNonceGenerator();
  162.         } else {
  163.             nonceSlopLimit = 0;
  164.             nonceGenerator = null;
  165.         }
  166.         db = into;
  167.         enabled = nonceGenerator != null;
  168.     }

  169.     private PushCertificateParser() {
  170.         db = null;
  171.         nonceSlopLimit = 0;
  172.         nonceGenerator = null;
  173.         enabled = true;
  174.     }

  175.     /**
  176.      * Parse a push certificate from a reader.
  177.      *
  178.      * @see #fromReader(Reader)
  179.      * @param r
  180.      *            input reader; consumed only up until the end of the next
  181.      *            signature in the input.
  182.      * @return the parsed certificate, or null if the reader was at EOF.
  183.      * @throws org.eclipse.jgit.errors.PackProtocolException
  184.      *             if the certificate is malformed.
  185.      * @throws java.io.IOException
  186.      *             if there was an error reading from the input.
  187.      * @since 4.1
  188.      */
  189.     public PushCertificate parse(Reader r)
  190.             throws PackProtocolException, IOException {
  191.         StreamReader reader = new StreamReader(r);
  192.         receiveHeader(reader, true);
  193.         String line;
  194.         try {
  195.             while (!(line = reader.read()).isEmpty()) {
  196.                 if (line.equals(BEGIN_SIGNATURE)) {
  197.                     receiveSignature(reader);
  198.                     break;
  199.                 }
  200.                 addCommand(line);
  201.             }
  202.         } catch (EOFException e) {
  203.             // EOF reached, but might have been at a valid state. Let build call below
  204.             // sort it out.
  205.         }
  206.         return build();
  207.     }

  208.     /**
  209.      * Build the parsed certificate
  210.      *
  211.      * @return the parsed certificate, or null if push certificates are
  212.      *         disabled.
  213.      * @throws java.io.IOException
  214.      *             if the push certificate has missing or invalid fields.
  215.      * @since 4.1
  216.      */
  217.     public PushCertificate build() throws IOException {
  218.         if (!received || !enabled) {
  219.             return null;
  220.         }
  221.         try {
  222.             return new PushCertificate(version, pusher, pushee, receivedNonce,
  223.                     nonceStatus, Collections.unmodifiableList(commands), signature);
  224.         } catch (IllegalArgumentException e) {
  225.             throw new IOException(e.getMessage(), e);
  226.         }
  227.     }

  228.     /**
  229.      * Whether the repository is configured to use signed pushes in this
  230.      * context.
  231.      *
  232.      * @return if the repository is configured to use signed pushes in this
  233.      *         context.
  234.      * @since 4.0
  235.      */
  236.     public boolean enabled() {
  237.         return enabled;
  238.     }

  239.     /**
  240.      * Get the whole string for the nonce to be included into the capability
  241.      * advertisement
  242.      *
  243.      * @return the whole string for the nonce to be included into the capability
  244.      *         advertisement, or null if push certificates are disabled.
  245.      * @since 4.0
  246.      */
  247.     public String getAdvertiseNonce() {
  248.         String nonce = sentNonce();
  249.         if (nonce == null) {
  250.             return null;
  251.         }
  252.         return CAPABILITY_PUSH_CERT + '=' + nonce;
  253.     }

  254.     private String sentNonce() {
  255.         if (sentNonce == null && nonceGenerator != null) {
  256.             sentNonce = nonceGenerator.createNonce(db,
  257.                     TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
  258.         }
  259.         return sentNonce;
  260.     }

  261.     private static String parseHeader(StringReader reader, String header)
  262.             throws IOException {
  263.         return parseHeader(reader.read(), header);
  264.     }

  265.     private static String parseHeader(String s, String header)
  266.             throws IOException {
  267.         if (s.isEmpty()) {
  268.             throw new EOFException();
  269.         }
  270.         if (s.length() <= header.length()
  271.                 || !s.startsWith(header)
  272.                 || s.charAt(header.length()) != ' ') {
  273.             throw new PackProtocolException(MessageFormat.format(
  274.                     JGitText.get().pushCertificateInvalidField, header));
  275.         }
  276.         return s.substring(header.length() + 1);
  277.     }

  278.     /**
  279.      * Receive a list of commands from the input encapsulated in a push
  280.      * certificate.
  281.      * <p>
  282.      * This method doesn't parse the first line {@code "push-cert \NUL
  283.      * &lt;capabilities&gt;"}, but assumes the first line including the
  284.      * capabilities has already been handled by the caller.
  285.      *
  286.      * @param pckIn
  287.      *            where we take the push certificate header from.
  288.      * @param stateless
  289.      *            affects nonce verification. When {@code stateless = true} the
  290.      *            {@code NonceGenerator} will allow for some time skew caused by
  291.      *            clients disconnected and reconnecting in the stateless smart
  292.      *            HTTP protocol.
  293.      * @throws java.io.IOException
  294.      *             if the certificate from the client is badly malformed or the
  295.      *             client disconnects before sending the entire certificate.
  296.      * @since 4.0
  297.      */
  298.     public void receiveHeader(PacketLineIn pckIn, boolean stateless)
  299.             throws IOException {
  300.         receiveHeader(new PacketLineReader(pckIn), stateless);
  301.     }

  302.     private void receiveHeader(StringReader reader, boolean stateless)
  303.             throws IOException {
  304.         try {
  305.             try {
  306.                 version = parseHeader(reader, VERSION);
  307.             } catch (EOFException e) {
  308.                 return;
  309.             }
  310.             received = true;
  311.             if (!version.equals(VERSION_0_1)) {
  312.                 throw new PackProtocolException(MessageFormat.format(
  313.                         JGitText.get().pushCertificateInvalidFieldValue, VERSION, version));
  314.             }
  315.             String rawPusher = parseHeader(reader, PUSHER);
  316.             pusher = PushCertificateIdent.parse(rawPusher);
  317.             if (pusher == null) {
  318.                 throw new PackProtocolException(MessageFormat.format(
  319.                         JGitText.get().pushCertificateInvalidFieldValue,
  320.                         PUSHER, rawPusher));
  321.             }
  322.             String next = reader.read();
  323.             if (next.startsWith(PUSHEE)) {
  324.                 pushee = parseHeader(next, PUSHEE);
  325.                 receivedNonce = parseHeader(reader, NONCE);
  326.             } else {
  327.                 receivedNonce = parseHeader(next, NONCE);
  328.             }
  329.             nonceStatus = nonceGenerator != null
  330.                     ? nonceGenerator.verify(
  331.                         receivedNonce, sentNonce(), db, stateless, nonceSlopLimit)
  332.                     : NonceStatus.UNSOLICITED;
  333.             // An empty line.
  334.             if (!reader.read().isEmpty()) {
  335.                 throw new PackProtocolException(
  336.                         JGitText.get().pushCertificateInvalidHeader);
  337.             }
  338.         } catch (EOFException eof) {
  339.             throw new PackProtocolException(
  340.                     JGitText.get().pushCertificateInvalidHeader, eof);
  341.         }
  342.     }

  343.     /**
  344.      * Read the PGP signature.
  345.      * <p>
  346.      * This method assumes the line
  347.      * {@code "-----BEGIN PGP SIGNATURE-----"} has already been parsed,
  348.      * and continues parsing until an {@code "-----END PGP SIGNATURE-----"} is
  349.      * found, followed by {@code "push-cert-end"}.
  350.      *
  351.      * @param pckIn
  352.      *            where we read the signature from.
  353.      * @throws java.io.IOException
  354.      *             if the signature is invalid.
  355.      * @since 4.0
  356.      */
  357.     public void receiveSignature(PacketLineIn pckIn) throws IOException {
  358.         StringReader reader = new PacketLineReader(pckIn);
  359.         receiveSignature(reader);
  360.         if (!reader.read().equals(END_CERT)) {
  361.             throw new PackProtocolException(
  362.                     JGitText.get().pushCertificateInvalidSignature);
  363.         }
  364.     }

  365.     private void receiveSignature(StringReader reader) throws IOException {
  366.         received = true;
  367.         try {
  368.             StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE).append('\n');
  369.             String line;
  370.             while (!(line = reader.read()).equals(END_SIGNATURE)) {
  371.                 sig.append(line).append('\n');
  372.             }
  373.             signature = sig.append(END_SIGNATURE).append('\n').toString();
  374.         } catch (EOFException eof) {
  375.             throw new PackProtocolException(
  376.                     JGitText.get().pushCertificateInvalidSignature, eof);
  377.         }
  378.     }

  379.     /**
  380.      * Add a command to the signature.
  381.      *
  382.      * @param cmd
  383.      *            the command.
  384.      * @since 4.1
  385.      */
  386.     public void addCommand(ReceiveCommand cmd) {
  387.         commands.add(cmd);
  388.     }

  389.     /**
  390.      * Add a command to the signature.
  391.      *
  392.      * @param line
  393.      *            the line read from the wire that produced this
  394.      *            command, with optional trailing newline already trimmed.
  395.      * @throws org.eclipse.jgit.errors.PackProtocolException
  396.      *             if the raw line cannot be parsed to a command.
  397.      * @since 4.0
  398.      */
  399.     public void addCommand(String line) throws PackProtocolException {
  400.         commands.add(parseCommand(line));
  401.     }
  402. }