| /* |
| * 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 <dborowitz@google.com> 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 <dborowitz@google.com> 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 = ReceivePack.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))); |
| } |
| } |