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 java.nio.charset.StandardCharsets.UTF_8;
46  import static org.junit.Assert.assertEquals;
47  import static org.junit.Assert.assertFalse;
48  import static org.junit.Assert.assertNotEquals;
49  import static org.junit.Assert.assertNotNull;
50  import static org.junit.Assert.assertNull;
51  import static org.junit.Assert.assertTrue;
52  import static org.junit.Assert.fail;
53  
54  import java.io.ByteArrayInputStream;
55  import java.io.EOFException;
56  import java.io.IOException;
57  import java.io.InputStreamReader;
58  import java.io.Reader;
59  import java.io.StringReader;
60  
61  import org.eclipse.jgit.errors.PackProtocolException;
62  import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
63  import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
64  import org.eclipse.jgit.lib.Config;
65  import org.eclipse.jgit.lib.Constants;
66  import org.eclipse.jgit.lib.ObjectId;
67  import org.eclipse.jgit.lib.Repository;
68  import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
69  import org.junit.Before;
70  import org.junit.Test;
71  
72  /** Test for push certificate parsing. */
73  public class PushCertificateParserTest {
74  	// Example push certificate generated by C git 2.2.0.
75  	private static final String INPUT = "001ccertificate version 0.1\n"
76  			+ "0041pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
77  			+ "0024pushee git://localhost/repo.git\n"
78  			+ "002anonce 1433954361-bde756572d665bba81d8\n"
79  			+ "0005\n"
80  			+ "00680000000000000000000000000000000000000000"
81  			+ " 6c2b981a177396fb47345b7df3e4d3f854c6bea7"
82  			+ " refs/heads/master\n"
83  			+ "0022-----BEGIN PGP SIGNATURE-----\n"
84  			+ "0016Version: GnuPG v1\n"
85  			+ "0005\n"
86  			+ "0045iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
87  			+ "00459tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
88  			+ "0045htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
89  			+ "00454ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
90  			+ "0045IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
91  			+ "0045+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
92  			+ "000a=XFeC\n"
93  			+ "0020-----END PGP SIGNATURE-----\n"
94  			+ "0012push-cert-end\n";
95  
96  	// Same push certificate, with all trailing newlines stripped.
97  	// (Note that the canonical signed payload is the same, so the same signature
98  	// is still valid.)
99  	private static final String INPUT_NO_NEWLINES = "001bcertificate version 0.1"
100 			+ "0040pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700"
101 			+ "0023pushee git://localhost/repo.git"
102 			+ "0029nonce 1433954361-bde756572d665bba81d8"
103 			+ "0004"
104 			+ "00670000000000000000000000000000000000000000"
105 			+ " 6c2b981a177396fb47345b7df3e4d3f854c6bea7"
106 			+ " refs/heads/master"
107 			+ "0021-----BEGIN PGP SIGNATURE-----"
108 			+ "0015Version: GnuPG v1"
109 			+ "0004"
110 			+ "0044iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa"
111 			+ "00449tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7"
112 			+ "0044htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V"
113 			+ "00444ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG"
114 			+ "0044IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY"
115 			+ "0044+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ="
116 			+ "0009=XFeC"
117 			+ "001f-----END PGP SIGNATURE-----"
118 			+ "0011push-cert-end";
119 
120 	private Repository db;
121 
122 	@Before
123 	public void setUp() {
124 		db = new InMemoryRepository(new DfsRepositoryDescription("repo"));
125 	}
126 
127 	private static SignedPushConfig newEnabledConfig() {
128 		Config cfg = new Config();
129 		cfg.setString("receive", null, "certnonceseed", "sekret");
130 		return SignedPushConfig.KEY.parse(cfg);
131 	}
132 
133 	private static SignedPushConfig newDisabledConfig() {
134 		return SignedPushConfig.KEY.parse(new Config());
135 	}
136 
137 	@Test
138 	public void noCert() throws Exception {
139 		PushCertificateParser parser =
140 				new PushCertificateParser(db, newEnabledConfig());
141 		assertTrue(parser.enabled());
142 		assertNull(parser.build());
143 
144 		ObjectId oldId = ObjectId.zeroId();
145 		ObjectId newId =
146 				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
147 		String line = oldId.name() + " " + newId.name() + " refs/heads/master";
148 		ReceiveCommand cmd = ReceivePack.parseCommand(line);
149 
150 		parser.addCommand(cmd);
151 		parser.addCommand(line);
152 		assertNull(parser.build());
153 	}
154 
155 	@Test
156 	public void disabled() throws Exception {
157 		PacketLineIn pckIn = newPacketLineIn(INPUT);
158 		PushCertificateParser parser =
159 				new PushCertificateParser(db, newDisabledConfig());
160 		assertFalse(parser.enabled());
161 		assertNull(parser.build());
162 
163 		parser.receiveHeader(pckIn, false);
164 		parser.addCommand(pckIn.readString());
165 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
166 		parser.receiveSignature(pckIn);
167 		assertNull(parser.build());
168 	}
169 
170 	@Test
171 	public void disabledParserStillRequiresCorrectSyntax() throws Exception {
172 		PacketLineIn pckIn = newPacketLineIn("001ccertificate version XYZ\n");
173 		PushCertificateParser parser =
174 				new PushCertificateParser(db, newDisabledConfig());
175 		assertFalse(parser.enabled());
176 		try {
177 			parser.receiveHeader(pckIn, false);
178 			fail("Expected PackProtocolException");
179 		} catch (PackProtocolException e) {
180 			assertEquals(
181 					"Push certificate has missing or invalid value for certificate"
182 						+ " version: XYZ",
183 					e.getMessage());
184 		}
185 		assertNull(parser.build());
186 	}
187 
188 	@Test
189 	public void parseCertFromPktLine() throws Exception {
190 		PacketLineIn pckIn = newPacketLineIn(INPUT);
191 		PushCertificateParser parser =
192 				new PushCertificateParser(db, newEnabledConfig());
193 		parser.receiveHeader(pckIn, false);
194 		parser.addCommand(pckIn.readString());
195 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
196 		parser.receiveSignature(pckIn);
197 
198 		PushCertificate cert = parser.build();
199 		assertEquals("0.1", cert.getVersion());
200 		assertEquals("Dave Borowitz", cert.getPusherIdent().getName());
201 		assertEquals("dborowitz@google.com",
202 				cert.getPusherIdent().getEmailAddress());
203 		assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime());
204 		assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset());
205 		assertEquals("git://localhost/repo.git", cert.getPushee());
206 		assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce());
207 
208 		assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce());
209 		assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus());
210 
211 		assertEquals(1, cert.getCommands().size());
212 		ReceiveCommand cmd = cert.getCommands().get(0);
213 		assertEquals("refs/heads/master", cmd.getRefName());
214 		assertEquals(ObjectId.zeroId(), cmd.getOldId());
215 		assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7",
216 				cmd.getNewId().name());
217 
218 		assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText());
219 		assertEquals(concatPacketLines(INPUT, 0, 17), cert.toTextWithSignature());
220 
221 		String signature = concatPacketLines(INPUT, 6, 17);
222 		assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE));
223 		assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n"));
224 		assertEquals(signature, cert.getSignature());
225 	}
226 
227 	@Test
228 	public void parseCertFromPktLineNoNewlines() throws Exception {
229 		PacketLineIn pckIn = newPacketLineIn(INPUT_NO_NEWLINES);
230 		PushCertificateParser parser =
231 				new PushCertificateParser(db, newEnabledConfig());
232 		parser.receiveHeader(pckIn, false);
233 		parser.addCommand(pckIn.readString());
234 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
235 		parser.receiveSignature(pckIn);
236 
237 		PushCertificate cert = parser.build();
238 		assertEquals("0.1", cert.getVersion());
239 		assertEquals("Dave Borowitz", cert.getPusherIdent().getName());
240 		assertEquals("dborowitz@google.com",
241 				cert.getPusherIdent().getEmailAddress());
242 		assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime());
243 		assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset());
244 		assertEquals("git://localhost/repo.git", cert.getPushee());
245 		assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce());
246 
247 		assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce());
248 		assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus());
249 
250 		assertEquals(1, cert.getCommands().size());
251 		ReceiveCommand cmd = cert.getCommands().get(0);
252 		assertEquals("refs/heads/master", cmd.getRefName());
253 		assertEquals(ObjectId.zeroId(), cmd.getOldId());
254 		assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7",
255 				cmd.getNewId().name());
256 
257 		// Canonical signed payload has reinserted newlines.
258 		assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText());
259 
260 		String signature = concatPacketLines(INPUT, 6, 17);
261 		assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE));
262 		assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n"));
263 		assertEquals(signature, cert.getSignature());
264 	}
265 
266 	@Test
267 	public void testConcatPacketLines() throws Exception {
268 		String input = "000bline 1\n000bline 2\n000bline 3\n";
269 		assertEquals("line 1\n", concatPacketLines(input, 0, 1));
270 		assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2));
271 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3));
272 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4));
273 	}
274 
275 	@Test
276 	public void testConcatPacketLinesInsertsNewlines() throws Exception {
277 		String input = "000bline 1\n000aline 2000bline 3\n";
278 		assertEquals("line 1\n", concatPacketLines(input, 0, 1));
279 		assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2));
280 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3));
281 		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4));
282 	}
283 
284 	@Test
285 	public void testParseReader() throws Exception {
286 		Reader reader = new StringReader(concatPacketLines(INPUT, 0, 18));
287 		PushCertificate streamCert = PushCertificateParser.fromReader(reader);
288 
289 		PacketLineIn pckIn = newPacketLineIn(INPUT);
290 		PushCertificateParser pckParser =
291 				new PushCertificateParser(db, newEnabledConfig());
292 		pckParser.receiveHeader(pckIn, false);
293 		pckParser.addCommand(pckIn.readString());
294 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
295 		pckParser.receiveSignature(pckIn);
296 		PushCertificate pckCert = pckParser.build();
297 
298 		// Nonce status is unsolicited since this was not parsed in the context of
299 		// the wire protocol; as a result, certs are not actually equal.
300 		assertEquals(NonceStatus.UNSOLICITED, streamCert.getNonceStatus());
301 
302 		assertEquals(pckCert.getVersion(), streamCert.getVersion());
303 		assertEquals(pckCert.getPusherIdent().getName(),
304 				streamCert.getPusherIdent().getName());
305 		assertEquals(pckCert.getPusherIdent().getEmailAddress(),
306 				streamCert.getPusherIdent().getEmailAddress());
307 		assertEquals(pckCert.getPusherIdent().getWhen().getTime(),
308 				streamCert.getPusherIdent().getWhen().getTime());
309 		assertEquals(pckCert.getPusherIdent().getTimeZoneOffset(),
310 				streamCert.getPusherIdent().getTimeZoneOffset());
311 		assertEquals(pckCert.getPushee(), streamCert.getPushee());
312 		assertEquals(pckCert.getNonce(), streamCert.getNonce());
313 		assertEquals(pckCert.getSignature(), streamCert.getSignature());
314 		assertEquals(pckCert.toText(), streamCert.toText());
315 
316 		assertEquals(pckCert.getCommands().size(), streamCert.getCommands().size());
317 		ReceiveCommand pckCmd = pckCert.getCommands().get(0);
318 		ReceiveCommand streamCmd = streamCert.getCommands().get(0);
319 		assertEquals(pckCmd.getRefName(), streamCmd.getRefName());
320 		assertEquals(pckCmd.getOldId(), streamCmd.getOldId());
321 		assertEquals(pckCmd.getNewId().name(), streamCmd.getNewId().name());
322 	}
323 
324 	@Test
325 	public void testParseString() throws Exception {
326 		String str = concatPacketLines(INPUT, 0, 18);
327 		assertEquals(
328 				PushCertificateParser.fromReader(new StringReader(str)),
329 				PushCertificateParser.fromString(str));
330 	}
331 
332 	@Test
333 	public void testParseMultipleFromStream() throws Exception {
334 		String input = concatPacketLines(INPUT, 0, 17);
335 		assertFalse(input.contains(PushCertificateParser.END_CERT));
336 		input += input;
337 		Reader reader = new InputStreamReader(
338 				new ByteArrayInputStream(Constants.encode(input)), UTF_8);
339 
340 		assertNotNull(PushCertificateParser.fromReader(reader));
341 		assertNotNull(PushCertificateParser.fromReader(reader));
342 		assertEquals(-1, reader.read());
343 		assertNull(PushCertificateParser.fromReader(reader));
344 	}
345 
346 	@Test
347 	public void testMissingPusheeField() throws Exception {
348 		// Omit pushee line from existing cert. (This means the signature would not
349 		// match, but we're not verifying it here.)
350 		String input = INPUT.replace("0024pushee git://localhost/repo.git\n", "");
351 		assertFalse(input.contains(PushCertificateParser.PUSHEE));
352 
353 		PacketLineIn pckIn = newPacketLineIn(input);
354 		PushCertificateParser parser =
355 				new PushCertificateParser(db, newEnabledConfig());
356 		parser.receiveHeader(pckIn, false);
357 		parser.addCommand(pckIn.readString());
358 		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
359 		parser.receiveSignature(pckIn);
360 
361 		PushCertificate cert = parser.build();
362 		assertEquals("0.1", cert.getVersion());
363 		assertNull(cert.getPushee());
364 		assertFalse(cert.toText().contains(PushCertificateParser.PUSHEE));
365 	}
366 
367 	private static String concatPacketLines(String input, int begin, int end)
368 			throws IOException {
369 		StringBuilder result = new StringBuilder();
370 		int i = 0;
371 		PacketLineIn pckIn = newPacketLineIn(input);
372 		while (i < end) {
373 			String line;
374 			try {
375 				line = pckIn.readString();
376 			} catch (EOFException e) {
377 				break;
378 			}
379 			if (++i > begin) {
380 				result.append(line).append('\n');
381 			}
382 		}
383 		return result.toString();
384 	}
385 
386 	private static PacketLineIn newPacketLineIn(String input) {
387 		return new PacketLineIn(new ByteArrayInputStream(Constants.encode(input)));
388 	}
389 }