/* * Copyright (C) 2015, Google Inc. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.eclipse.jgit.transport; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.PushCertificate.NonceStatus; import org.junit.Before; import org.junit.Test; /** Test for push certificate parsing. */ public class PushCertificateParserTest { // Example push certificate generated by C git 2.2.0. private static final String INPUT = "001ccertificate version 0.1\n" + "0041pusher Dave Borowitz 1433954361 -0700\n" + "0024pushee git://localhost/repo.git\n" + "002anonce 1433954361-bde756572d665bba81d8\n" + "0005\n" + "00680000000000000000000000000000000000000000" + " 6c2b981a177396fb47345b7df3e4d3f854c6bea7" + " refs/heads/master\n" + "0022-----BEGIN PGP SIGNATURE-----\n" + "0016Version: GnuPG v1\n" + "0005\n" + "0045iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n" + "00459tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n" + "0045htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n" + "00454ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n" + "0045IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n" + "0045+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n" + "000a=XFeC\n" + "0020-----END PGP SIGNATURE-----\n" + "0012push-cert-end\n"; // Same push certificate, with all trailing newlines stripped. // (Note that the canonical signed payload is the same, so the same signature // is still valid.) private static final String INPUT_NO_NEWLINES = "001bcertificate version 0.1" + "0040pusher Dave Borowitz 1433954361 -0700" + "0023pushee git://localhost/repo.git" + "0029nonce 1433954361-bde756572d665bba81d8" + "0004" + "00670000000000000000000000000000000000000000" + " 6c2b981a177396fb47345b7df3e4d3f854c6bea7" + " refs/heads/master" + "0021-----BEGIN PGP SIGNATURE-----" + "0015Version: GnuPG v1" + "0004" + "0044iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa" + "00449tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7" + "0044htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V" + "00444ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG" + "0044IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY" + "0044+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=" + "0009=XFeC" + "001f-----END PGP SIGNATURE-----" + "0011push-cert-end"; private Repository db; @Before public void setUp() { db = new InMemoryRepository(new DfsRepositoryDescription("repo")); } private static SignedPushConfig newEnabledConfig() { Config cfg = new Config(); cfg.setString("receive", null, "certnonceseed", "sekret"); return SignedPushConfig.KEY.parse(cfg); } private static SignedPushConfig newDisabledConfig() { return SignedPushConfig.KEY.parse(new Config()); } @Test public void noCert() throws Exception { PushCertificateParser parser = new PushCertificateParser(db, newEnabledConfig()); assertTrue(parser.enabled()); assertNull(parser.build()); ObjectId oldId = ObjectId.zeroId(); ObjectId newId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); String line = oldId.name() + " " + newId.name() + " refs/heads/master"; ReceiveCommand cmd = BaseReceivePack.parseCommand(line); parser.addCommand(cmd); parser.addCommand(line); assertNull(parser.build()); } @Test public void disabled() throws Exception { PacketLineIn pckIn = newPacketLineIn(INPUT); PushCertificateParser parser = new PushCertificateParser(db, newDisabledConfig()); assertFalse(parser.enabled()); assertNull(parser.build()); parser.receiveHeader(pckIn, false); parser.addCommand(pckIn.readString()); assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); parser.receiveSignature(pckIn); assertNull(parser.build()); } @Test public void disabledParserStillRequiresCorrectSyntax() throws Exception { PacketLineIn pckIn = newPacketLineIn("001ccertificate version XYZ\n"); PushCertificateParser parser = new PushCertificateParser(db, newDisabledConfig()); assertFalse(parser.enabled()); try { parser.receiveHeader(pckIn, false); fail("Expected PackProtocolException"); } catch (PackProtocolException e) { assertEquals( "Push certificate has missing or invalid value for certificate" + " version: XYZ", e.getMessage()); } assertNull(parser.build()); } @Test public void parseCertFromPktLine() throws Exception { PacketLineIn pckIn = newPacketLineIn(INPUT); PushCertificateParser parser = new PushCertificateParser(db, newEnabledConfig()); parser.receiveHeader(pckIn, false); parser.addCommand(pckIn.readString()); assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); parser.receiveSignature(pckIn); PushCertificate cert = parser.build(); assertEquals("0.1", cert.getVersion()); assertEquals("Dave Borowitz", cert.getPusherIdent().getName()); assertEquals("dborowitz@google.com", cert.getPusherIdent().getEmailAddress()); assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime()); assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset()); assertEquals("git://localhost/repo.git", cert.getPushee()); assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce()); assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce()); assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus()); assertEquals(1, cert.getCommands().size()); ReceiveCommand cmd = cert.getCommands().get(0); assertEquals("refs/heads/master", cmd.getRefName()); assertEquals(ObjectId.zeroId(), cmd.getOldId()); assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7", cmd.getNewId().name()); assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText()); assertEquals(concatPacketLines(INPUT, 0, 17), cert.toTextWithSignature()); String signature = concatPacketLines(INPUT, 6, 17); assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE)); assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n")); assertEquals(signature, cert.getSignature()); } @Test public void parseCertFromPktLineNoNewlines() throws Exception { PacketLineIn pckIn = newPacketLineIn(INPUT_NO_NEWLINES); PushCertificateParser parser = new PushCertificateParser(db, newEnabledConfig()); parser.receiveHeader(pckIn, false); parser.addCommand(pckIn.readString()); assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); parser.receiveSignature(pckIn); PushCertificate cert = parser.build(); assertEquals("0.1", cert.getVersion()); assertEquals("Dave Borowitz", cert.getPusherIdent().getName()); assertEquals("dborowitz@google.com", cert.getPusherIdent().getEmailAddress()); assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime()); assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset()); assertEquals("git://localhost/repo.git", cert.getPushee()); assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce()); assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce()); assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus()); assertEquals(1, cert.getCommands().size()); ReceiveCommand cmd = cert.getCommands().get(0); assertEquals("refs/heads/master", cmd.getRefName()); assertEquals(ObjectId.zeroId(), cmd.getOldId()); assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7", cmd.getNewId().name()); // Canonical signed payload has reinserted newlines. assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText()); String signature = concatPacketLines(INPUT, 6, 17); assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE)); assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n")); assertEquals(signature, cert.getSignature()); } @Test public void testConcatPacketLines() throws Exception { String input = "000bline 1\n000bline 2\n000bline 3\n"; assertEquals("line 1\n", concatPacketLines(input, 0, 1)); assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2)); assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3)); assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4)); } @Test public void testConcatPacketLinesInsertsNewlines() throws Exception { String input = "000bline 1\n000aline 2000bline 3\n"; assertEquals("line 1\n", concatPacketLines(input, 0, 1)); assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2)); assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3)); assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4)); } @Test public void testParseReader() throws Exception { Reader reader = new StringReader(concatPacketLines(INPUT, 0, 18)); PushCertificate streamCert = PushCertificateParser.fromReader(reader); PacketLineIn pckIn = newPacketLineIn(INPUT); PushCertificateParser pckParser = new PushCertificateParser(db, newEnabledConfig()); pckParser.receiveHeader(pckIn, false); pckParser.addCommand(pckIn.readString()); assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); pckParser.receiveSignature(pckIn); PushCertificate pckCert = pckParser.build(); // Nonce status is unsolicited since this was not parsed in the context of // the wire protocol; as a result, certs are not actually equal. assertEquals(NonceStatus.UNSOLICITED, streamCert.getNonceStatus()); assertEquals(pckCert.getVersion(), streamCert.getVersion()); assertEquals(pckCert.getPusherIdent().getName(), streamCert.getPusherIdent().getName()); assertEquals(pckCert.getPusherIdent().getEmailAddress(), streamCert.getPusherIdent().getEmailAddress()); assertEquals(pckCert.getPusherIdent().getWhen().getTime(), streamCert.getPusherIdent().getWhen().getTime()); assertEquals(pckCert.getPusherIdent().getTimeZoneOffset(), streamCert.getPusherIdent().getTimeZoneOffset()); assertEquals(pckCert.getPushee(), streamCert.getPushee()); assertEquals(pckCert.getNonce(), streamCert.getNonce()); assertEquals(pckCert.getSignature(), streamCert.getSignature()); assertEquals(pckCert.toText(), streamCert.toText()); assertEquals(pckCert.getCommands().size(), streamCert.getCommands().size()); ReceiveCommand pckCmd = pckCert.getCommands().get(0); ReceiveCommand streamCmd = streamCert.getCommands().get(0); assertEquals(pckCmd.getRefName(), streamCmd.getRefName()); assertEquals(pckCmd.getOldId(), streamCmd.getOldId()); assertEquals(pckCmd.getNewId().name(), streamCmd.getNewId().name()); } @Test public void testParseString() throws Exception { String str = concatPacketLines(INPUT, 0, 18); assertEquals( PushCertificateParser.fromReader(new StringReader(str)), PushCertificateParser.fromString(str)); } @Test public void testParseMultipleFromStream() throws Exception { String input = concatPacketLines(INPUT, 0, 17); assertFalse(input.contains(PushCertificateParser.END_CERT)); input += input; Reader reader = new InputStreamReader( new ByteArrayInputStream(Constants.encode(input)), UTF_8); assertNotNull(PushCertificateParser.fromReader(reader)); assertNotNull(PushCertificateParser.fromReader(reader)); assertEquals(-1, reader.read()); assertNull(PushCertificateParser.fromReader(reader)); } @Test public void testMissingPusheeField() throws Exception { // Omit pushee line from existing cert. (This means the signature would not // match, but we're not verifying it here.) String input = INPUT.replace("0024pushee git://localhost/repo.git\n", ""); assertFalse(input.contains(PushCertificateParser.PUSHEE)); PacketLineIn pckIn = newPacketLineIn(input); PushCertificateParser parser = new PushCertificateParser(db, newEnabledConfig()); parser.receiveHeader(pckIn, false); parser.addCommand(pckIn.readString()); assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); parser.receiveSignature(pckIn); PushCertificate cert = parser.build(); assertEquals("0.1", cert.getVersion()); assertNull(cert.getPushee()); assertFalse(cert.toText().contains(PushCertificateParser.PUSHEE)); } private static String concatPacketLines(String input, int begin, int end) throws IOException { StringBuilder result = new StringBuilder(); int i = 0; PacketLineIn pckIn = newPacketLineIn(input); while (i < end) { String line; try { line = pckIn.readString(); } catch (EOFException e) { break; } if (++i > begin) { result.append(line).append('\n'); } } return result.toString(); } private static PacketLineIn newPacketLineIn(String input) { return new PacketLineIn(new ByteArrayInputStream(Constants.encode(input))); } }