View Javadoc
1   /*
2    * Copyright (C) 2015, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import static org.eclipse.jgit.transport.BaseReceivePack.parseCommand;
47  import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT;
48  
49  import java.io.EOFException;
50  import java.io.IOException;
51  import java.io.Reader;
52  import java.text.MessageFormat;
53  import java.util.ArrayList;
54  import java.util.Collections;
55  import java.util.List;
56  import java.util.concurrent.TimeUnit;
57  
58  import org.eclipse.jgit.errors.PackProtocolException;
59  import org.eclipse.jgit.internal.JGitText;
60  import org.eclipse.jgit.lib.Repository;
61  import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
62  import org.eclipse.jgit.util.IO;
63  
64  /**
65   * Parser for signed push certificates.
66   *
67   * @since 4.0
68   */
69  public class PushCertificateParser {
70  	static final String BEGIN_SIGNATURE =
71  			"-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$
72  	static final String END_SIGNATURE =
73  			"-----END PGP SIGNATURE-----"; //$NON-NLS-1$
74  
75  	static final String VERSION = "certificate version"; //$NON-NLS-1$
76  
77  	static final String PUSHER = "pusher"; //$NON-NLS-1$
78  
79  	static final String PUSHEE = "pushee"; //$NON-NLS-1$
80  
81  	static final String NONCE = "nonce"; //$NON-NLS-1$
82  
83  	static final String END_CERT = "push-cert-end"; //$NON-NLS-1$
84  
85  	private static final String VERSION_0_1 = "0.1"; //$NON-NLS-1$
86  
87  	private static interface StringReader {
88  		/**
89  		 * @return the next string from the input, up to an optional newline, with
90  		 *         newline stripped if present
91  		 *
92  		 * @throws EOFException
93  		 *             if EOF was reached.
94  		 * @throws IOException
95  		 *             if an error occurred during reading.
96  		 */
97  		String read() throws EOFException, IOException;
98  	}
99  
100 	private static class PacketLineReader implements StringReader {
101 		private final PacketLineIn pckIn;
102 
103 		private PacketLineReader(PacketLineIn pckIn) {
104 			this.pckIn = pckIn;
105 		}
106 
107 		@Override
108 		public String read() throws IOException {
109 			return pckIn.readString();
110 		}
111 	}
112 
113 	private static class StreamReader implements StringReader {
114 		private final Reader reader;
115 
116 		private StreamReader(Reader reader) {
117 			this.reader = reader;
118 		}
119 
120 		@Override
121 		public String read() throws IOException {
122 			// Presize for a command containing 2 SHA-1s and some refname.
123 			String line = IO.readLine(reader, 41 * 2 + 64);
124 			if (line.isEmpty()) {
125 				throw new EOFException();
126 			} else if (line.charAt(line.length() - 1) == '\n') {
127 				line = line.substring(0, line.length() - 1);
128 			}
129 			return line;
130 		}
131 	}
132 
133 	/**
134 	 * Parse a push certificate from a reader.
135 	 * <p>
136 	 * Differences from the {@link PacketLineIn} receiver methods:
137 	 * <ul>
138 	 * <li>Does not use pkt-line framing.</li>
139 	 * <li>Reads an entire cert in one call rather than depending on a loop in
140 	 *   the caller.</li>
141 	 * <li>Does not assume a {@code "push-cert-end"} line.</li>
142 	 * </ul>
143 	 *
144 	 * @param r
145 	 *            input reader; consumed only up until the end of the next
146 	 *            signature in the input.
147 	 * @return the parsed certificate, or null if the reader was at EOF.
148 	 * @throws PackProtocolException
149 	 *             if the certificate is malformed.
150 	 * @throws IOException
151 	 *             if there was an error reading from the input.
152 	 * @since 4.1
153 	 */
154 	public static PushCertificate fromReader(Reader r)
155 			throws PackProtocolException, IOException {
156 		return new PushCertificateParser().parse(r);
157 	}
158 
159 	/**
160 	 * Parse a push certificate from a string.
161 	 *
162 	 * @see #fromReader(Reader)
163 	 * @param str
164 	 *            input string.
165 	 * @return the parsed certificate.
166 	 * @throws PackProtocolException
167 	 *             if the certificate is malformed.
168 	 * @throws IOException
169 	 *             if there was an error reading from the input.
170 	 * @since 4.1
171 	 */
172 	public static PushCertificate fromString(String str)
173 			throws PackProtocolException, IOException {
174 		return fromReader(new java.io.StringReader(str));
175 	}
176 
177 	private boolean received;
178 	private String version;
179 	private PushCertificateIdent pusher;
180 	private String pushee;
181 
182 	/** The nonce that was sent to the client. */
183 	private String sentNonce;
184 
185 	/**
186 	 * The nonce the pusher signed.
187 	 * <p>
188 	 * This may vary from {@link #sentNonce}; see git-core documentation for
189 	 * reasons.
190 	 */
191 	private String receivedNonce;
192 
193 	private NonceStatus nonceStatus;
194 	private String signature;
195 
196 	/** Database we write the push certificate into. */
197 	private final Repository db;
198 
199 	/**
200 	 * The maximum time difference which is acceptable between advertised nonce
201 	 * and received signed nonce.
202 	 */
203 	private final int nonceSlopLimit;
204 
205 	private final boolean enabled;
206 	private final NonceGenerator nonceGenerator;
207 	private final List<ReceiveCommand> commands = new ArrayList<>();
208 
209 	/**
210 	 * @param into
211 	 *            destination repository for the push.
212 	 * @param cfg
213 	 *            configuration for signed push.
214 	 * @since 4.1
215 	 */
216 	public PushCertificateParser(Repository into, SignedPushConfig cfg) {
217 		if (cfg != null) {
218 			nonceSlopLimit = cfg.getCertNonceSlopLimit();
219 			nonceGenerator = cfg.getNonceGenerator();
220 		} else {
221 			nonceSlopLimit = 0;
222 			nonceGenerator = null;
223 		}
224 		db = into;
225 		enabled = nonceGenerator != null;
226 	}
227 
228 	private PushCertificateParser() {
229 		db = null;
230 		nonceSlopLimit = 0;
231 		nonceGenerator = null;
232 		enabled = true;
233 	}
234 
235 	/**
236 	 * Parse a push certificate from a reader.
237 	 *
238 	 * @see #fromReader(Reader)
239 	 * @param r
240 	 *            input reader; consumed only up until the end of the next
241 	 *            signature in the input.
242 	 * @return the parsed certificate, or null if the reader was at EOF.
243 	 * @throws PackProtocolException
244 	 *             if the certificate is malformed.
245 	 * @throws IOException
246 	 *             if there was an error reading from the input.
247 	 * @since 4.1
248 	 */
249 	public PushCertificate parse(Reader r)
250 			throws PackProtocolException, IOException {
251 		StreamReader reader = new StreamReader(r);
252 		receiveHeader(reader, true);
253 		String line;
254 		try {
255 			while (!(line = reader.read()).isEmpty()) {
256 				if (line.equals(BEGIN_SIGNATURE)) {
257 					receiveSignature(reader);
258 					break;
259 				}
260 				addCommand(line);
261 			}
262 		} catch (EOFException e) {
263 			// EOF reached, but might have been at a valid state. Let build call below
264 			// sort it out.
265 		}
266 		return build();
267 	}
268 
269 	/**
270 	 * @return the parsed certificate, or null if push certificates are disabled.
271 	 * @throws IOException
272 	 *             if the push certificate has missing or invalid fields.
273 	 * @since 4.1
274 	 */
275 	public PushCertificate build() throws IOException {
276 		if (!received || !enabled) {
277 			return null;
278 		}
279 		try {
280 			return new PushCertificate(version, pusher, pushee, receivedNonce,
281 					nonceStatus, Collections.unmodifiableList(commands), signature);
282 		} catch (IllegalArgumentException e) {
283 			throw new IOException(e.getMessage(), e);
284 		}
285 	}
286 
287 	/**
288 	 * @return if the repository is configured to use signed pushes in this
289 	 *         context.
290 	 * @since 4.0
291 	 */
292 	public boolean enabled() {
293 		return enabled;
294 	}
295 
296 	/**
297 	 * @return the whole string for the nonce to be included into the capability
298 	 *         advertisement, or null if push certificates are disabled.
299 	 * @since 4.0
300 	 */
301 	public String getAdvertiseNonce() {
302 		String nonce = sentNonce();
303 		if (nonce == null) {
304 			return null;
305 		}
306 		return CAPABILITY_PUSH_CERT + '=' + nonce;
307 	}
308 
309 	private String sentNonce() {
310 		if (sentNonce == null && nonceGenerator != null) {
311 			sentNonce = nonceGenerator.createNonce(db,
312 					TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
313 		}
314 		return sentNonce;
315 	}
316 
317 	private static String parseHeader(StringReader reader, String header)
318 			throws IOException {
319 		return parseHeader(reader.read(), header);
320 	}
321 
322 	private static String parseHeader(String s, String header)
323 			throws IOException {
324 		if (s.isEmpty()) {
325 			throw new EOFException();
326 		}
327 		if (s.length() <= header.length()
328 				|| !s.startsWith(header)
329 				|| s.charAt(header.length()) != ' ') {
330 			throw new PackProtocolException(MessageFormat.format(
331 					JGitText.get().pushCertificateInvalidField, header));
332 		}
333 		return s.substring(header.length() + 1);
334 	}
335 
336 	/**
337 	 * Receive a list of commands from the input encapsulated in a push
338 	 * certificate.
339 	 * <p>
340 	 * This method doesn't parse the first line {@code "push-cert \NUL
341 	 * &lt;capabilities&gt;"}, but assumes the first line including the
342 	 * capabilities has already been handled by the caller.
343 	 *
344 	 * @param pckIn
345 	 *            where we take the push certificate header from.
346 	 * @param stateless
347 	 *            affects nonce verification. When {@code stateless = true} the
348 	 *            {@code NonceGenerator} will allow for some time skew caused by
349 	 *            clients disconnected and reconnecting in the stateless smart
350 	 *            HTTP protocol.
351 	 * @throws IOException
352 	 *             if the certificate from the client is badly malformed or the
353 	 *             client disconnects before sending the entire certificate.
354 	 * @since 4.0
355 	 */
356 	public void receiveHeader(PacketLineIn pckIn, boolean stateless)
357 			throws IOException {
358 		receiveHeader(new PacketLineReader(pckIn), stateless);
359 	}
360 
361 	private void receiveHeader(StringReader reader, boolean stateless)
362 			throws IOException {
363 		try {
364 			try {
365 				version = parseHeader(reader, VERSION);
366 			} catch (EOFException e) {
367 				return;
368 			}
369 			received = true;
370 			if (!version.equals(VERSION_0_1)) {
371 				throw new PackProtocolException(MessageFormat.format(
372 						JGitText.get().pushCertificateInvalidFieldValue, VERSION, version));
373 			}
374 			String rawPusher = parseHeader(reader, PUSHER);
375 			pusher = PushCertificateIdent.parse(rawPusher);
376 			if (pusher == null) {
377 				throw new PackProtocolException(MessageFormat.format(
378 						JGitText.get().pushCertificateInvalidFieldValue,
379 						PUSHER, rawPusher));
380 			}
381 			String next = reader.read();
382 			if (next.startsWith(PUSHEE)) {
383 				pushee = parseHeader(next, PUSHEE);
384 				receivedNonce = parseHeader(reader, NONCE);
385 			} else {
386 				receivedNonce = parseHeader(next, NONCE);
387 			}
388 			nonceStatus = nonceGenerator != null
389 					? nonceGenerator.verify(
390 						receivedNonce, sentNonce(), db, stateless, nonceSlopLimit)
391 					: NonceStatus.UNSOLICITED;
392 			// An empty line.
393 			if (!reader.read().isEmpty()) {
394 				throw new PackProtocolException(
395 						JGitText.get().pushCertificateInvalidHeader);
396 			}
397 		} catch (EOFException eof) {
398 			throw new PackProtocolException(
399 					JGitText.get().pushCertificateInvalidHeader, eof);
400 		}
401 	}
402 
403 	/**
404 	 * Read the PGP signature.
405 	 * <p>
406 	 * This method assumes the line
407 	 * {@code "-----BEGIN PGP SIGNATURE-----"} has already been parsed,
408 	 * and continues parsing until an {@code "-----END PGP SIGNATURE-----"} is
409 	 * found, followed by {@code "push-cert-end"}.
410 	 *
411 	 * @param pckIn
412 	 *            where we read the signature from.
413 	 * @throws IOException
414 	 *             if the signature is invalid.
415 	 * @since 4.0
416 	 */
417 	public void receiveSignature(PacketLineIn pckIn) throws IOException {
418 		StringReader reader = new PacketLineReader(pckIn);
419 		receiveSignature(reader);
420 		if (!reader.read().equals(END_CERT)) {
421 			throw new PackProtocolException(
422 					JGitText.get().pushCertificateInvalidSignature);
423 		}
424 	}
425 
426 	private void receiveSignature(StringReader reader) throws IOException {
427 		received = true;
428 		try {
429 			StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE).append('\n');
430 			String line;
431 			while (!(line = reader.read()).equals(END_SIGNATURE)) {
432 				sig.append(line).append('\n');
433 			}
434 			signature = sig.append(END_SIGNATURE).append('\n').toString();
435 		} catch (EOFException eof) {
436 			throw new PackProtocolException(
437 					JGitText.get().pushCertificateInvalidSignature, eof);
438 		}
439 	}
440 
441 	/**
442 	 * Add a command to the signature.
443 	 *
444 	 * @param cmd
445 	 *            the command.
446 	 * @since 4.1
447 	 */
448 	public void addCommand(ReceiveCommand cmd) {
449 		commands.add(cmd);
450 	}
451 
452 	/**
453 	 * Add a command to the signature.
454 	 *
455 	 * @param line
456 	 *            the line read from the wire that produced this
457 	 *            command, with optional trailing newline already trimmed.
458 	 * @throws PackProtocolException
459 	 *             if the raw line cannot be parsed to a command.
460 	 * @since 4.0
461 	 */
462 	public void addCommand(String line) throws PackProtocolException {
463 		commands.add(parseCommand(line));
464 	}
465 }