diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 8d240d34e..804306538 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -49,6 +49,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.junit.time;version="[5.6.0,5.7.0)", org.eclipse.jgit.lfs;version="[5.6.0,5.7.0)", org.eclipse.jgit.lib;version="[5.6.0,5.7.0)", + org.eclipse.jgit.lib.internal;version="[5.6.0,5.7.0)", org.eclipse.jgit.merge;version="[5.6.0,5.7.0)", org.eclipse.jgit.nls;version="[5.6.0,5.7.0)", org.eclipse.jgit.notes;version="[5.6.0,5.7.0)", diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocatorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocatorTest.java new file mode 100644 index 000000000..220b2becb --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocatorTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2019, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * 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.lib.internal; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Locale; + +import org.junit.Test; + +public class BouncyCastleGpgKeyLocatorTest { + + private static final String USER_ID = "Heinrich Heine "; + + private static boolean match(String userId, String pattern) { + return BouncyCastleGpgKeyLocator.containsSigningKey(userId, pattern); + } + + @Test + public void testFullMatch() throws Exception { + assertTrue(match(USER_ID, + "=Heinrich Heine ")); + assertFalse(match(USER_ID, "=Heinrich Heine")); + assertFalse(match(USER_ID, "= ")); + assertFalse(match(USER_ID, "=heinrichh@uni-duesseldorf.de")); + } + + @Test + public void testEmpty() throws Exception { + assertFalse(match(USER_ID, "")); + assertFalse(match(USER_ID, null)); + assertFalse(match("", "")); + assertFalse(match(null, "")); + assertFalse(match(null, null)); + assertFalse(match("", "something")); + assertFalse(match(null, "something")); + } + + @Test + public void testFullEmail() throws Exception { + assertTrue(match(USER_ID, "")); + assertTrue(match(USER_ID + " ", "")); + assertFalse(match(USER_ID, "<>")); + assertFalse(match(USER_ID, "")); + assertFalse(match(USER_ID, "")); + assertFalse(match(USER_ID, "")); + assertFalse(match(USER_ID, "")); + assertFalse(match(USER_ID, "")); + assertFalse(match(USER_ID.substring(0, USER_ID.length() - 1), + "")); + assertFalse(match("", "<>")); + assertFalse(match("", "")); + } + + @Test + public void testPartialEmail() throws Exception { + assertTrue(match(USER_ID, "@heinrichh@uni-duesseldorf.de")); + assertTrue(match(USER_ID, "@heinrichh")); + assertTrue(match(USER_ID, "@duesseldorf")); + assertTrue(match(USER_ID, "@uni-d")); + assertTrue(match(USER_ID, "@h")); + assertTrue(match(USER_ID, "@.")); + assertTrue(match(USER_ID, "@h@u")); + assertFalse(match(USER_ID, "@ ")); + assertFalse(match(USER_ID, "@")); + assertFalse(match(USER_ID, "@Heine")); + assertFalse(match(USER_ID, "@HeinrichH")); + assertFalse(match(USER_ID, "@Heinrich")); + assertFalse(match("", "@")); + assertFalse(match("", "@h")); + } + + private void substringTests(String prefix) throws Exception { + assertTrue(match(USER_ID, prefix + "heinrichh@uni-duesseldorf.de")); + assertTrue(match(USER_ID, prefix + "heinrich")); + assertTrue(match(USER_ID, prefix + "HEIN")); + assertTrue(match(USER_ID, prefix + "Heine <")); + assertTrue(match(USER_ID, prefix + "UNI")); + assertTrue(match(USER_ID, prefix + "uni")); + assertTrue(match(USER_ID, prefix + "rich He")); + assertTrue(match(USER_ID, prefix + "h@u")); + assertTrue(match(USER_ID, prefix + USER_ID)); + assertTrue(match(USER_ID, prefix + USER_ID.toUpperCase(Locale.ROOT))); + assertFalse(match(USER_ID, prefix + "")); + assertFalse(match(USER_ID, prefix + " ")); + assertFalse(match(USER_ID, prefix + "yy")); + assertFalse(match("", prefix + "")); + assertFalse(match("", prefix + "uni")); + } + + @Test + public void testSubstringPlain() throws Exception { + substringTests(""); + } + + @Test + public void testSubstringAsterisk() throws Exception { + substringTests("*"); + } + + @Test + public void testExplicitFingerprint() throws Exception { + assertFalse(match("John Fade ", "0xfade")); + assertFalse(match("John Fade <0xfade@example.com>", "0xfade")); + assertFalse(match("", "0xfade")); + } + + @Test + public void testImplicitFingerprint() throws Exception { + assertTrue(match("John Fade ", "fade")); + assertTrue(match("John Fade <0xfade@example.com>", "fade")); + assertTrue(match("John Fade ", "FADE")); + assertTrue(match("John Fade <0xfade@example.com>", "FADE")); + } + + @Test + public void testZeroX() throws Exception { + assertTrue(match("John Fade <0xfade@example.com>", "0x")); + assertTrue(match("John Fade <0xfade@example.com>", "*0x")); + assertTrue(match("John Fade <0xfade@example.com>", "*0xfade")); + assertTrue(match("John Fade <0xfade@example.com>", "*0xFADE")); + assertTrue(match("John Fade <0xfade@example.com>", "@0xfade")); + assertFalse(match("John Fade <0xfade@example.com>", "@0xFADE")); + assertFalse(match("", "0x")); + } +} diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 35f29aa07..ab0f1a2c0 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -99,7 +99,7 @@ Export-Package: org.eclipse.jgit.annotations;version="5.6.0", org.eclipse.jgit.treewalk, org.eclipse.jgit.transport, org.eclipse.jgit.submodule", - org.eclipse.jgit.lib.internal;version="5.6.0";x-internal:=true, + org.eclipse.jgit.lib.internal;version="5.6.0";x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.merge;version="5.6.0"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java index e3233590f..f28334cb8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java @@ -92,6 +92,7 @@ import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -192,14 +193,87 @@ class BouncyCastleGpgKeyLocator { } } - private boolean containsSigningKey(String userId) { - return userId.toLowerCase(Locale.ROOT) - .contains(signingKey.toLowerCase(Locale.ROOT)); + /** + * Checks whether a given OpenPGP {@code userId} matches a given + * {@code signingKeySpec}, which is supposed to have one of the formats + * defined by GPG. + *

+ * Not all formats are supported; only formats starting with '=', '<', + * '@', and '*' are handled. Any other format results in a case-insensitive + * substring match. + *

+ * + * @param userId + * of a key + * @param signingKeySpec + * GPG key identification + * @return whether the {@code userId} matches + * @see GPG + * Documentation: How to Specify a User ID + */ + static boolean containsSigningKey(String userId, String signingKeySpec) { + if (StringUtils.isEmptyOrNull(userId) + || StringUtils.isEmptyOrNull(signingKeySpec)) { + return false; + } + String toMatch = signingKeySpec; + if (toMatch.startsWith("0x") && toMatch.trim().length() > 2) { //$NON-NLS-1$ + return false; // Explicit fingerprint + } + int command = toMatch.charAt(0); + switch (command) { + case '=': + case '<': + case '@': + case '*': + toMatch = toMatch.substring(1); + if (toMatch.isEmpty()) { + return false; + } + break; + default: + break; + } + switch (command) { + case '=': + return userId.equals(toMatch); + case '<': { + int begin = userId.indexOf('<'); + int end = userId.indexOf('>', begin + 1); + int stop = toMatch.indexOf('>'); + return begin >= 0 && end > begin + 1 && stop > 0 + && userId.substring(begin + 1, end) + .equals(toMatch.substring(0, stop)); + } + case '@': { + int begin = userId.indexOf('<'); + int end = userId.indexOf('>', begin + 1); + return begin >= 0 && end > begin + 1 + && userId.substring(begin + 1, end).contains(toMatch); + } + default: + if (toMatch.trim().isEmpty()) { + return false; + } + return userId.toLowerCase(Locale.ROOT) + .contains(toMatch.toLowerCase(Locale.ROOT)); + } + } + + private String toFingerprint(String keyId) { + if (keyId.startsWith("0x")) { //$NON-NLS-1$ + return keyId.substring(2); + } + return keyId; } private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob) throws IOException { - String keyId = signingKey.toLowerCase(Locale.ROOT); + String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); + if (keyId.isEmpty()) { + return null; + } for (KeyInformation keyInfo : keyBlob.getKeyInformation()) { String fingerprint = Hex.toHexString(keyInfo.getFingerprint()) .toLowerCase(Locale.ROOT); @@ -213,7 +287,7 @@ class BouncyCastleGpgKeyLocator { private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob) throws IOException { for (UserID userID : keyBlob.getUserIds()) { - if (containsSigningKey(userID.getUserIDAsString())) { + if (containsSigningKey(userID.getUserIDAsString(), signingKey)) { return getSigningPublicKey(keyBlob); } } @@ -446,7 +520,7 @@ class BouncyCastleGpgKeyLocator { PGPUtil.getDecoderStream(new BufferedInputStream(in)), new JcaKeyFingerprintCalculator()); - String keyId = signingkey.toLowerCase(Locale.ROOT); + String keyId = toFingerprint(signingkey).toLowerCase(Locale.ROOT); Iterator keyrings = pgpSec.getKeyRings(); while (keyrings.hasNext()) { PGPSecretKeyRing keyRing = keyrings.next(); @@ -464,7 +538,7 @@ class BouncyCastleGpgKeyLocator { Iterator userIDs = key.getUserIDs(); while (userIDs.hasNext()) { String userId = userIDs.next(); - if (containsSigningKey(userId)) { + if (containsSigningKey(userId, signingKey)) { return key; } } @@ -492,7 +566,7 @@ class BouncyCastleGpgKeyLocator { new BufferedInputStream(in), new JcaKeyFingerprintCalculator()); - String keyId = signingKey.toLowerCase(Locale.ROOT); + String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); Iterator keyrings = pgpPub.getKeyRings(); while (keyrings.hasNext()) { PGPPublicKeyRing keyRing = keyrings.next(); @@ -509,7 +583,7 @@ class BouncyCastleGpgKeyLocator { Iterator userIDs = key.getUserIDs(); while (userIDs.hasNext()) { String userId = userIDs.next(); - if (containsSigningKey(userId)) { + if (containsSigningKey(userId, signingKey)) { return key; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java index cfe0931b4..cfa67eefd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java @@ -115,7 +115,7 @@ public class BouncyCastleGpgSigner extends GpgSigner { NoSuchAlgorithmException, NoSuchProviderException, PGPException, URISyntaxException { if (gpgSigningKey == null || gpgSigningKey.isEmpty()) { - gpgSigningKey = committer.getEmailAddress(); + gpgSigningKey = '<' + committer.getEmailAddress() + '>'; } BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator(