View Javadoc
1   /*
2    * Copyright (C) 2015, Google Inc.
3    *
4    * This program and the accompanying materials are made available
5    * under the terms of the Eclipse Distribution License v1.0 which
6    * accompanies this distribution, is reproduced below, and is
7    * available at http://www.eclipse.org/org/documents/edl-v10.php
8    *
9    * All rights reserved.
10   *
11   * Redistribution and use in source and binary forms, with or
12   * without modification, are permitted provided that the following
13   * conditions are met:
14   *
15   * - Redistributions of source code must retain the above copyright
16   *	 notice, this list of conditions and the following disclaimer.
17   *
18   * - Redistributions in binary form must reproduce the above
19   *	 copyright notice, this list of conditions and the following
20   *	 disclaimer in the documentation and/or other materials provided
21   *	 with the distribution.
22   *
23   * - Neither the name of the Eclipse Foundation, Inc. nor the
24   *	 names of its contributors may be used to endorse or promote
25   *	 products derived from this software without specific prior
26   *	 written permission.
27   *
28   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
29   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
30   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
31   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
32   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
33   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
34   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
35   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
36   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
37   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
38   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
39   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
40   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41   */
42  
43  package org.eclipse.jgit.transport;
44  
45  import static org.junit.Assert.assertEquals;
46  import static org.junit.Assert.assertFalse;
47  import static org.junit.Assert.assertNotEquals;
48  import static org.junit.Assert.assertNotNull;
49  import static org.junit.Assert.assertNull;
50  import static org.junit.Assert.assertTrue;
51  import static org.junit.Assert.fail;
52  
53  import java.io.ByteArrayInputStream;
54  import java.io.EOFException;
55  import java.io.IOException;
56  import java.io.InputStreamReader;
57  import java.io.Reader;
58  import java.io.StringReader;
59  
60  import org.eclipse.jgit.errors.PackProtocolException;
61  import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
62  import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
63  import org.eclipse.jgit.lib.Config;
64  import org.eclipse.jgit.lib.Constants;
65  import org.eclipse.jgit.lib.ObjectId;
66  import org.eclipse.jgit.lib.Repository;
67  import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
68  import org.junit.Before;
69  import org.junit.Test;
70  
71  /** Test for push certificate parsing. */
72  public class PushCertificateParserTest {
73  	// Example push certificate generated by C git 2.2.0.
74  	private static final String INPUT = "001ccertificate version 0.1\n"
75  			+ "0041pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
76  			+ "0024pushee git://localhost/repo.git\n"
77  			+ "002anonce 1433954361-bde756572d665bba81d8\n"
78  			+ "0005\n"
79  			+ "00680000000000000000000000000000000000000000"
80  			+ " 6c2b981a177396fb47345b7df3e4d3f854c6bea7"
81  			+ " refs/heads/master\n"
82  			+ "0022-----BEGIN PGP SIGNATURE-----\n"
83  			+ "0016Version: GnuPG v1\n"
84  			+ "0005\n"
85  			+ "0045iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
86  			+ "00459tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
87  			+ "0045htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
88  			+ "00454ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
89  			+ "0045IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
90  			+ "0045+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
91  			+ "000a=XFeC\n"
92  			+ "0020-----END PGP SIGNATURE-----\n"
93  			+ "0012push-cert-end\n";
94  
95  	// Same push certificate, with all trailing newlines stripped.
96  	// (Note that the canonical signed payload is the same, so the same signature
97  	// is still valid.)
98  	private static final String INPUT_NO_NEWLINES = "001bcertificate version 0.1"
99  			+ "0040pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700"
100 			+ "0023pushee git://localhost/repo.git"
101 			+ "0029nonce 1433954361-bde756572d665bba81d8"
102 			+ "0004"
103 			+ "00670000000000000000000000000000000000000000"
104 			+ " 6c2b981a177396fb47345b7df3e4d3f854c6bea7"
105 			+ " refs/heads/master"
106 			+ "0021-----BEGIN PGP SIGNATURE-----"
107 			+ "0015Version: GnuPG v1"
108 			+ "0004"
109 			+ "0044iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa"
110 			+ "00449tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7"
111 			+ "0044htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V"
112 			+ "00444ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG"
113 			+ "0044IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY"
114 			+ "0044+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ="
115 			+ "0009=XFeC"
116 			+ "001f-----END PGP SIGNATURE-----"
117 			+ "0011push-cert-end";
118 
119 	private Repository db;
120 
121 	@Before
122 	public void setUp() {
123 		db = new InMemoryRepository(new DfsRepositoryDescription("repo"));
124 	}
125 
126 	private static SignedPushConfig newEnabledConfig() {
127 		Config cfg = new Config();
128 		cfg.setString("receive", null, "certnonceseed", "sekret");
129 		return SignedPushConfig.KEY.parse(cfg);
130 	}
131 
132 	private static SignedPushConfig newDisabledConfig() {
133 		return SignedPushConfig.KEY.parse(new Config());
134 	}
135 
136 	@Test
137 	public void noCert() throws Exception {
138 		PushCertificateParser parser =
139 				new PushCertificateParser(db, newEnabledConfig());
140 		assertTrue(parser.enabled());
141 		assertNull(parser.build());
142 
143 		ObjectId oldId = ObjectId.zeroId();
144 		ObjectId newId =
145 				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
146 		String line = oldId.name() + " " + newId.name() + " refs/heads/master";
147 		ReceiveCommand cmd = BaseReceivePack.parseCommand(line);
148 
149 		parser.addCommand(cmd);
150 		parser.addCommand(line);
151 		assertNull(parser.build());
152 	}
153 
154 	@Test
155 	public void disabled() throws Exception {
156 		PacketLineIn pckIn = newPacketLineIn(INPUT);
157 		PushCertificateParser parser =
158 				new PushCertificateParser(db, newDisabledConfig());
159 		assertFalse(parser.enabled());
160 		assertNull(parser.build());
161 
162 		parser.receiveHeader(pckIn, false);
163 		parser.addCommand(pckIn.readString());
164 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
165 		parser.receiveSignature(pckIn);
166 		assertNull(parser.build());
167 	}
168 
169 	@Test
170 	public void disabledParserStillRequiresCorrectSyntax() throws Exception {
171 		PacketLineIn pckIn = newPacketLineIn("001ccertificate version XYZ\n");
172 		PushCertificateParser parser =
173 				new PushCertificateParser(db, newDisabledConfig());
174 		assertFalse(parser.enabled());
175 		try {
176 			parser.receiveHeader(pckIn, false);
177 			fail("Expected PackProtocolException");
178 		} catch (PackProtocolException e) {
179 			assertEquals(
180 					"Push certificate has missing or invalid value for certificate"
181 						+ " version: XYZ",
182 					e.getMessage());
183 		}
184 		assertNull(parser.build());
185 	}
186 
187 	@Test
188 	public void parseCertFromPktLine() throws Exception {
189 		PacketLineIn pckIn = newPacketLineIn(INPUT);
190 		PushCertificateParser parser =
191 				new PushCertificateParser(db, newEnabledConfig());
192 		parser.receiveHeader(pckIn, false);
193 		parser.addCommand(pckIn.readString());
194 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
195 		parser.receiveSignature(pckIn);
196 
197 		PushCertificate cert = parser.build();
198 		assertEquals("0.1", cert.getVersion());
199 		assertEquals("Dave Borowitz", cert.getPusherIdent().getName());
200 		assertEquals("dborowitz@google.com",
201 				cert.getPusherIdent().getEmailAddress());
202 		assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime());
203 		assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset());
204 		assertEquals("git://localhost/repo.git", cert.getPushee());
205 		assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce());
206 
207 		assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce());
208 		assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus());
209 
210 		assertEquals(1, cert.getCommands().size());
211 		ReceiveCommand cmd = cert.getCommands().get(0);
212 		assertEquals("refs/heads/master", cmd.getRefName());
213 		assertEquals(ObjectId.zeroId(), cmd.getOldId());
214 		assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7",
215 				cmd.getNewId().name());
216 
217 		assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText());
218 		assertEquals(concatPacketLines(INPUT, 0, 17), cert.toTextWithSignature());
219 
220 		String signature = concatPacketLines(INPUT, 6, 17);
221 		assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE));
222 		assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n"));
223 		assertEquals(signature, cert.getSignature());
224 	}
225 
226 	@Test
227 	public void parseCertFromPktLineNoNewlines() throws Exception {
228 		PacketLineIn pckIn = newPacketLineIn(INPUT_NO_NEWLINES);
229 		PushCertificateParser parser =
230 				new PushCertificateParser(db, newEnabledConfig());
231 		parser.receiveHeader(pckIn, false);
232 		parser.addCommand(pckIn.readString());
233 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
234 		parser.receiveSignature(pckIn);
235 
236 		PushCertificate cert = parser.build();
237 		assertEquals("0.1", cert.getVersion());
238 		assertEquals("Dave Borowitz", cert.getPusherIdent().getName());
239 		assertEquals("dborowitz@google.com",
240 				cert.getPusherIdent().getEmailAddress());
241 		assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime());
242 		assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset());
243 		assertEquals("git://localhost/repo.git", cert.getPushee());
244 		assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce());
245 
246 		assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce());
247 		assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus());
248 
249 		assertEquals(1, cert.getCommands().size());
250 		ReceiveCommand cmd = cert.getCommands().get(0);
251 		assertEquals("refs/heads/master", cmd.getRefName());
252 		assertEquals(ObjectId.zeroId(), cmd.getOldId());
253 		assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7",
254 				cmd.getNewId().name());
255 
256 		// Canonical signed payload has reinserted newlines.
257 		assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText());
258 
259 		String signature = concatPacketLines(INPUT, 6, 17);
260 		assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE));
261 		assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n"));
262 		assertEquals(signature, cert.getSignature());
263 	}
264 
265 	@Test
266 	public void testConcatPacketLines() throws Exception {
267 		String input = "000bline 1\n000bline 2\n000bline 3\n";
268 		assertEquals("line 1\n", concatPacketLines(input, 0, 1));
269 		assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2));
270 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3));
271 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4));
272 	}
273 
274 	@Test
275 	public void testConcatPacketLinesInsertsNewlines() throws Exception {
276 		String input = "000bline 1\n000aline 2000bline 3\n";
277 		assertEquals("line 1\n", concatPacketLines(input, 0, 1));
278 		assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2));
279 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3));
280 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4));
281 	}
282 
283 	@Test
284 	public void testParseReader() throws Exception {
285 		Reader reader = new StringReader(concatPacketLines(INPUT, 0, 18));
286 		PushCertificate streamCert = PushCertificateParser.fromReader(reader);
287 
288 		PacketLineIn pckIn = newPacketLineIn(INPUT);
289 		PushCertificateParser pckParser =
290 				new PushCertificateParser(db, newEnabledConfig());
291 		pckParser.receiveHeader(pckIn, false);
292 		pckParser.addCommand(pckIn.readString());
293 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
294 		pckParser.receiveSignature(pckIn);
295 		PushCertificate pckCert = pckParser.build();
296 
297 		// Nonce status is unsolicited since this was not parsed in the context of
298 		// the wire protocol; as a result, certs are not actually equal.
299 		assertEquals(NonceStatus.UNSOLICITED, streamCert.getNonceStatus());
300 
301 		assertEquals(pckCert.getVersion(), streamCert.getVersion());
302 		assertEquals(pckCert.getPusherIdent().getName(),
303 				streamCert.getPusherIdent().getName());
304 		assertEquals(pckCert.getPusherIdent().getEmailAddress(),
305 				streamCert.getPusherIdent().getEmailAddress());
306 		assertEquals(pckCert.getPusherIdent().getWhen().getTime(),
307 				streamCert.getPusherIdent().getWhen().getTime());
308 		assertEquals(pckCert.getPusherIdent().getTimeZoneOffset(),
309 				streamCert.getPusherIdent().getTimeZoneOffset());
310 		assertEquals(pckCert.getPushee(), streamCert.getPushee());
311 		assertEquals(pckCert.getNonce(), streamCert.getNonce());
312 		assertEquals(pckCert.getSignature(), streamCert.getSignature());
313 		assertEquals(pckCert.toText(), streamCert.toText());
314 
315 		assertEquals(pckCert.getCommands().size(), streamCert.getCommands().size());
316 		ReceiveCommand pckCmd = pckCert.getCommands().get(0);
317 		ReceiveCommand streamCmd = streamCert.getCommands().get(0);
318 		assertEquals(pckCmd.getRefName(), streamCmd.getRefName());
319 		assertEquals(pckCmd.getOldId(), streamCmd.getOldId());
320 		assertEquals(pckCmd.getNewId().name(), streamCmd.getNewId().name());
321 	}
322 
323 	@Test
324 	public void testParseString() throws Exception {
325 		String str = concatPacketLines(INPUT, 0, 18);
326 		assertEquals(
327 				PushCertificateParser.fromReader(new StringReader(str)),
328 				PushCertificateParser.fromString(str));
329 	}
330 
331 	@Test
332 	public void testParseMultipleFromStream() throws Exception {
333 		String input = concatPacketLines(INPUT, 0, 17);
334 		assertFalse(input.contains(PushCertificateParser.END_CERT));
335 		input += input;
336 		Reader reader = new InputStreamReader(
337 				new ByteArrayInputStream(Constants.encode(input)));
338 
339 		assertNotNull(PushCertificateParser.fromReader(reader));
340 		assertNotNull(PushCertificateParser.fromReader(reader));
341 		assertEquals(-1, reader.read());
342 		assertNull(PushCertificateParser.fromReader(reader));
343 	}
344 
345 	@Test
346 	public void testMissingPusheeField() throws Exception {
347 		// Omit pushee line from existing cert. (This means the signature would not
348 		// match, but we're not verifying it here.)
349 		String input = INPUT.replace("0024pushee git://localhost/repo.git\n", "");
350 		assertFalse(input.contains(PushCertificateParser.PUSHEE));
351 
352 		PacketLineIn pckIn = newPacketLineIn(input);
353 		PushCertificateParser parser =
354 				new PushCertificateParser(db, newEnabledConfig());
355 		parser.receiveHeader(pckIn, false);
356 		parser.addCommand(pckIn.readString());
357 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
358 		parser.receiveSignature(pckIn);
359 
360 		PushCertificate cert = parser.build();
361 		assertEquals("0.1", cert.getVersion());
362 		assertNull(cert.getPushee());
363 		assertFalse(cert.toText().contains(PushCertificateParser.PUSHEE));
364 	}
365 
366 	private static String concatPacketLines(String input, int begin, int end)
367 			throws IOException {
368 		StringBuilder result = new StringBuilder();
369 		int i = 0;
370 		PacketLineIn pckIn = newPacketLineIn(input);
371 		while (i < end) {
372 			String line;
373 			try {
374 				line = pckIn.readString();
375 			} catch (EOFException e) {
376 				break;
377 			}
378 			if (++i > begin) {
379 				result.append(line).append('\n');
380 			}
381 		}
382 		return result.toString();
383 	}
384 
385 	private static PacketLineIn newPacketLineIn(String input) {
386 		return new PacketLineIn(new ByteArrayInputStream(Constants.encode(input)));
387 	}
388 }