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