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 org.eclipse.jgit.transport.PacketLineIn}
137 	 * receiver methods:
138 	 * <ul>
139 	 * <li>Does not use pkt-line framing.</li>
140 	 * <li>Reads an entire cert in one call rather than depending on a loop in
141 	 * the caller.</li>
142 	 * <li>Does not assume a {@code "push-cert-end"} line.</li>
143 	 * </ul>
144 	 *
145 	 * @param r
146 	 *            input reader; consumed only up until the end of the next
147 	 *            signature in the input.
148 	 * @return the parsed certificate, or null if the reader was at EOF.
149 	 * @throws org.eclipse.jgit.errors.PackProtocolException
150 	 *             if the certificate is malformed.
151 	 * @throws java.io.IOException
152 	 *             if there was an error reading from the input.
153 	 * @since 4.1
154 	 */
155 	public static PushCertificate fromReader(Reader r)
156 			throws PackProtocolException, IOException {
157 		return new PushCertificateParser().parse(r);
158 	}
159 
160 	/**
161 	 * Parse a push certificate from a string.
162 	 *
163 	 * @see #fromReader(Reader)
164 	 * @param str
165 	 *            input string.
166 	 * @return the parsed certificate.
167 	 * @throws org.eclipse.jgit.errors.PackProtocolException
168 	 *             if the certificate is malformed.
169 	 * @throws java.io.IOException
170 	 *             if there was an error reading from the input.
171 	 * @since 4.1
172 	 */
173 	public static PushCertificate fromString(String str)
174 			throws PackProtocolException, IOException {
175 		return fromReader(new java.io.StringReader(str));
176 	}
177 
178 	private boolean received;
179 	private String version;
180 	private PushCertificateIdent pusher;
181 	private String pushee;
182 
183 	/** The nonce that was sent to the client. */
184 	private String sentNonce;
185 
186 	/**
187 	 * The nonce the pusher signed.
188 	 * <p>
189 	 * This may vary from {@link #sentNonce}; see git-core documentation for
190 	 * reasons.
191 	 */
192 	private String receivedNonce;
193 
194 	private NonceStatus nonceStatus;
195 	private String signature;
196 
197 	/** Database we write the push certificate into. */
198 	private final Repository db;
199 
200 	/**
201 	 * The maximum time difference which is acceptable between advertised nonce
202 	 * and received signed nonce.
203 	 */
204 	private final int nonceSlopLimit;
205 
206 	private final boolean enabled;
207 	private final NonceGenerator nonceGenerator;
208 	private final List<ReceiveCommand> commands = new ArrayList<>();
209 
210 	/**
211 	 * <p>Constructor for PushCertificateParser.</p>
212 	 *
213 	 * @param into
214 	 *            destination repository for the push.
215 	 * @param cfg
216 	 *            configuration for signed push.
217 	 * @since 4.1
218 	 */
219 	public PushCertificateParser(Repository into, SignedPushConfig cfg) {
220 		if (cfg != null) {
221 			nonceSlopLimit = cfg.getCertNonceSlopLimit();
222 			nonceGenerator = cfg.getNonceGenerator();
223 		} else {
224 			nonceSlopLimit = 0;
225 			nonceGenerator = null;
226 		}
227 		db = into;
228 		enabled = nonceGenerator != null;
229 	}
230 
231 	private PushCertificateParser() {
232 		db = null;
233 		nonceSlopLimit = 0;
234 		nonceGenerator = null;
235 		enabled = true;
236 	}
237 
238 	/**
239 	 * Parse a push certificate from a reader.
240 	 *
241 	 * @see #fromReader(Reader)
242 	 * @param r
243 	 *            input reader; consumed only up until the end of the next
244 	 *            signature in the input.
245 	 * @return the parsed certificate, or null if the reader was at EOF.
246 	 * @throws org.eclipse.jgit.errors.PackProtocolException
247 	 *             if the certificate is malformed.
248 	 * @throws java.io.IOException
249 	 *             if there was an error reading from the input.
250 	 * @since 4.1
251 	 */
252 	public PushCertificate parse(Reader r)
253 			throws PackProtocolException, IOException {
254 		StreamReader reader = new StreamReader(r);
255 		receiveHeader(reader, true);
256 		String line;
257 		try {
258 			while (!(line = reader.read()).isEmpty()) {
259 				if (line.equals(BEGIN_SIGNATURE)) {
260 					receiveSignature(reader);
261 					break;
262 				}
263 				addCommand(line);
264 			}
265 		} catch (EOFException e) {
266 			// EOF reached, but might have been at a valid state. Let build call below
267 			// sort it out.
268 		}
269 		return build();
270 	}
271 
272 	/**
273 	 * Build the parsed certificate
274 	 *
275 	 * @return the parsed certificate, or null if push certificates are
276 	 *         disabled.
277 	 * @throws java.io.IOException
278 	 *             if the push certificate has missing or invalid fields.
279 	 * @since 4.1
280 	 */
281 	public PushCertificate build() throws IOException {
282 		if (!received || !enabled) {
283 			return null;
284 		}
285 		try {
286 			return new PushCertificate(version, pusher, pushee, receivedNonce,
287 					nonceStatus, Collections.unmodifiableList(commands), signature);
288 		} catch (IllegalArgumentException e) {
289 			throw new IOException(e.getMessage(), e);
290 		}
291 	}
292 
293 	/**
294 	 * Whether the repository is configured to use signed pushes in this
295 	 * context.
296 	 *
297 	 * @return if the repository is configured to use signed pushes in this
298 	 *         context.
299 	 * @since 4.0
300 	 */
301 	public boolean enabled() {
302 		return enabled;
303 	}
304 
305 	/**
306 	 * Get the whole string for the nonce to be included into the capability
307 	 * advertisement
308 	 *
309 	 * @return the whole string for the nonce to be included into the capability
310 	 *         advertisement, or null if push certificates are disabled.
311 	 * @since 4.0
312 	 */
313 	public String getAdvertiseNonce() {
314 		String nonce = sentNonce();
315 		if (nonce == null) {
316 			return null;
317 		}
318 		return CAPABILITY_PUSH_CERT + '=' + nonce;
319 	}
320 
321 	private String sentNonce() {
322 		if (sentNonce == null && nonceGenerator != null) {
323 			sentNonce = nonceGenerator.createNonce(db,
324 					TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
325 		}
326 		return sentNonce;
327 	}
328 
329 	private static String parseHeader(StringReader reader, String header)
330 			throws IOException {
331 		return parseHeader(reader.read(), header);
332 	}
333 
334 	private static String parseHeader(String s, String header)
335 			throws IOException {
336 		if (s.isEmpty()) {
337 			throw new EOFException();
338 		}
339 		if (s.length() <= header.length()
340 				|| !s.startsWith(header)
341 				|| s.charAt(header.length()) != ' ') {
342 			throw new PackProtocolException(MessageFormat.format(
343 					JGitText.get().pushCertificateInvalidField, header));
344 		}
345 		return s.substring(header.length() + 1);
346 	}
347 
348 	/**
349 	 * Receive a list of commands from the input encapsulated in a push
350 	 * certificate.
351 	 * <p>
352 	 * This method doesn't parse the first line {@code "push-cert \NUL
353 	 * &lt;capabilities&gt;"}, but assumes the first line including the
354 	 * capabilities has already been handled by the caller.
355 	 *
356 	 * @param pckIn
357 	 *            where we take the push certificate header from.
358 	 * @param stateless
359 	 *            affects nonce verification. When {@code stateless = true} the
360 	 *            {@code NonceGenerator} will allow for some time skew caused by
361 	 *            clients disconnected and reconnecting in the stateless smart
362 	 *            HTTP protocol.
363 	 * @throws java.io.IOException
364 	 *             if the certificate from the client is badly malformed or the
365 	 *             client disconnects before sending the entire certificate.
366 	 * @since 4.0
367 	 */
368 	public void receiveHeader(PacketLineIn pckIn, boolean stateless)
369 			throws IOException {
370 		receiveHeader(new PacketLineReader(pckIn), stateless);
371 	}
372 
373 	private void receiveHeader(StringReader reader, boolean stateless)
374 			throws IOException {
375 		try {
376 			try {
377 				version = parseHeader(reader, VERSION);
378 			} catch (EOFException e) {
379 				return;
380 			}
381 			received = true;
382 			if (!version.equals(VERSION_0_1)) {
383 				throw new PackProtocolException(MessageFormat.format(
384 						JGitText.get().pushCertificateInvalidFieldValue, VERSION, version));
385 			}
386 			String rawPusher = parseHeader(reader, PUSHER);
387 			pusher = PushCertificateIdent.parse(rawPusher);
388 			if (pusher == null) {
389 				throw new PackProtocolException(MessageFormat.format(
390 						JGitText.get().pushCertificateInvalidFieldValue,
391 						PUSHER, rawPusher));
392 			}
393 			String next = reader.read();
394 			if (next.startsWith(PUSHEE)) {
395 				pushee = parseHeader(next, PUSHEE);
396 				receivedNonce = parseHeader(reader, NONCE);
397 			} else {
398 				receivedNonce = parseHeader(next, NONCE);
399 			}
400 			nonceStatus = nonceGenerator != null
401 					? nonceGenerator.verify(
402 						receivedNonce, sentNonce(), db, stateless, nonceSlopLimit)
403 					: NonceStatus.UNSOLICITED;
404 			// An empty line.
405 			if (!reader.read().isEmpty()) {
406 				throw new PackProtocolException(
407 						JGitText.get().pushCertificateInvalidHeader);
408 			}
409 		} catch (EOFException eof) {
410 			throw new PackProtocolException(
411 					JGitText.get().pushCertificateInvalidHeader, eof);
412 		}
413 	}
414 
415 	/**
416 	 * Read the PGP signature.
417 	 * <p>
418 	 * This method assumes the line
419 	 * {@code "-----BEGIN PGP SIGNATURE-----"} has already been parsed,
420 	 * and continues parsing until an {@code "-----END PGP SIGNATURE-----"} is
421 	 * found, followed by {@code "push-cert-end"}.
422 	 *
423 	 * @param pckIn
424 	 *            where we read the signature from.
425 	 * @throws java.io.IOException
426 	 *             if the signature is invalid.
427 	 * @since 4.0
428 	 */
429 	public void receiveSignature(PacketLineIn pckIn) throws IOException {
430 		StringReader reader = new PacketLineReader(pckIn);
431 		receiveSignature(reader);
432 		if (!reader.read().equals(END_CERT)) {
433 			throw new PackProtocolException(
434 					JGitText.get().pushCertificateInvalidSignature);
435 		}
436 	}
437 
438 	private void receiveSignature(StringReader reader) throws IOException {
439 		received = true;
440 		try {
441 			StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE).append('\n');
442 			String line;
443 			while (!(line = reader.read()).equals(END_SIGNATURE)) {
444 				sig.append(line).append('\n');
445 			}
446 			signature = sig.append(END_SIGNATURE).append('\n').toString();
447 		} catch (EOFException eof) {
448 			throw new PackProtocolException(
449 					JGitText.get().pushCertificateInvalidSignature, eof);
450 		}
451 	}
452 
453 	/**
454 	 * Add a command to the signature.
455 	 *
456 	 * @param cmd
457 	 *            the command.
458 	 * @since 4.1
459 	 */
460 	public void addCommand(ReceiveCommand cmd) {
461 		commands.add(cmd);
462 	}
463 
464 	/**
465 	 * Add a command to the signature.
466 	 *
467 	 * @param line
468 	 *            the line read from the wire that produced this
469 	 *            command, with optional trailing newline already trimmed.
470 	 * @throws org.eclipse.jgit.errors.PackProtocolException
471 	 *             if the raw line cannot be parsed to a command.
472 	 * @since 4.0
473 	 */
474 	public void addCommand(String line) throws PackProtocolException {
475 		commands.add(parseCommand(line));
476 	}
477 }