diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/util/sha1/shattered-1.pdf b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/util/sha1/shattered-1.pdf new file mode 100644 index 000000000..ba9aaa145 Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/util/sha1/shattered-1.pdf differ diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/util/sha1/shattered-2.pdf b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/util/sha1/shattered-2.pdf new file mode 100644 index 000000000..b621eeccd Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/util/sha1/shattered-2.pdf differ diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/sha1/SHA1Test.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/sha1/SHA1Test.java index 8642db79e..07789898a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/sha1/SHA1Test.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/sha1/SHA1Test.java @@ -44,12 +44,20 @@ package org.eclipse.jgit.util.sha1; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.util.IO; import org.junit.Test; public class SHA1Test { @@ -124,4 +132,107 @@ public class SHA1Test { assertEquals(exp, s1); assertEquals(exp, s2); } + + @Test + public void shatteredCollision() + throws IOException, NoSuchAlgorithmException { + byte[] pdf1 = read("shattered-1.pdf", 422435); + byte[] pdf2 = read("shattered-2.pdf", 422435); + MessageDigest md; + SHA1 s; + + // SHAttered attack generated these PDFs to have identical SHA-1. + ObjectId bad = ObjectId + .fromString("38762cf7f55934b34d179ae6a4c80cadccbb7f0a"); + md = MessageDigest.getInstance("SHA-1"); + md.update(pdf1); + assertEquals("shattered-1 collides", bad, + ObjectId.fromRaw(md.digest())); + s = SHA1.newInstance().setDetectCollision(false); + s.update(pdf1); + assertEquals("shattered-1 collides", bad, s.toObjectId()); + + md = MessageDigest.getInstance("SHA-1"); + md.update(pdf2); + assertEquals("shattered-2 collides", bad, + ObjectId.fromRaw(md.digest())); + s = SHA1.newInstance().setDetectCollision(false); + s.update(pdf2); + assertEquals("shattered-2 collides", bad, s.toObjectId()); + + // SHA1 with detectCollision shouldn't be fooled. + s = SHA1.newInstance().setDetectCollision(true); + s.update(pdf1); + try { + s.digest(); + fail("expected " + Sha1CollisionException.class.getSimpleName()); + } catch (Sha1CollisionException e) { + assertEquals(e.getMessage(), + "SHA-1 collision detected on " + bad.name()); + } + + s = SHA1.newInstance().setDetectCollision(true); + s.update(pdf2); + try { + s.digest(); + fail("expected " + Sha1CollisionException.class.getSimpleName()); + } catch (Sha1CollisionException e) { + assertEquals(e.getMessage(), + "SHA-1 collision detected on " + bad.name()); + } + } + + @Test + public void shatteredStoredInGitBlob() throws IOException { + byte[] pdf1 = read("shattered-1.pdf", 422435); + byte[] pdf2 = read("shattered-2.pdf", 422435); + + // Although the prior test detects the chance of a collision, adding + // the Git blob header permutes the data enough for this specific + // attack example to not be detected as a collision. (A different file + // pair that takes the Git header into account however, would.) + ObjectId id1 = blob(pdf1, SHA1.newInstance().setDetectCollision(true)); + ObjectId id2 = blob(pdf2, SHA1.newInstance().setDetectCollision(true)); + + assertEquals( + ObjectId.fromString("ba9aaa145ccd24ef760cf31c74d8f7ca1a2e47b0"), + id1); + assertEquals( + ObjectId.fromString("b621eeccd5c7edac9b7dcba35a8d5afd075e24f2"), + id2); + } + + @Test + public void detectsShatteredByDefault() throws IOException { + assumeTrue(System.getProperty("org.eclipse.jgit.util.sha1.detectCollision") == null); + assumeTrue(System.getProperty("org.eclipse.jgit.util.sha1.safeHash") == null); + + byte[] pdf1 = read("shattered-1.pdf", 422435); + SHA1 s = SHA1.newInstance(); + s.update(pdf1); + try { + s.digest(); + fail("expected " + Sha1CollisionException.class.getSimpleName()); + } catch (Sha1CollisionException e) { + assertTrue("shattered-1 detected", true); + } + } + + private static ObjectId blob(byte[] pdf1, SHA1 s) { + s.update(Constants.encodedTypeString(Constants.OBJ_BLOB)); + s.update((byte) ' '); + s.update(Constants.encodeASCII(pdf1.length)); + s.update((byte) 0); + s.update(pdf1); + return s.toObjectId(); + } + + private byte[] read(String name, int sizeHint) throws IOException { + try (InputStream in = getClass().getResourceAsStream(name)) { + ByteBuffer buf = IO.readWholeStream(in, sizeHint); + byte[] r = new byte[buf.remaining()]; + buf.get(r); + return r; + } + } } diff --git a/org.eclipse.jgit/about.html b/org.eclipse.jgit/about.html index 01a267187..f971af18d 100644 --- a/org.eclipse.jgit/about.html +++ b/org.eclipse.jgit/about.html @@ -21,6 +21,10 @@ margin-top: 0.05em; margin-bottom: 0.05em; } + .ubc-name { + margin-left: 0.5in; + white-space: pre; + } @@ -54,6 +58,39 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+SHA-1 UbcCheck - MIT
+ +Copyright (c) 2017:
+Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
+