Browse Source

GPG: implement more OpenPGP UserId matching formats

Instead of just looking for a substring match of user.signingKey
in a key's user ID implement the GPG matching formats[1] for:

'=' Full exact match
'<' Full exact match of the e-mail address
'@' Substring match within the e-mail address only
'*' General case-insensitive substring match (default)

When user.signingKey is not set, the committer's e-mail address is
used by default. In that case, use '<', i.e., require an exact match
on the OpenPGP e-mail address.

Also handle the optional "0x" prefix for (partial) key fingerprints.

[1] https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html

Bug: 550335
Change-Id: I6ce482a099ff1a0dc9de45435cd4d3ec5b504f12
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
tags/v5.6.0.201911271000-m3
Thomas Wolf 4 years ago
parent
commit
6a39da37fe

+ 1
- 0
org.eclipse.jgit.test/META-INF/MANIFEST.MF View File

@@ -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)",

+ 167
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocatorTest.java View File

@@ -0,0 +1,167 @@
/*
* Copyright (C) 2019, Thomas Wolf <thomas.wolf@paranor.ch>
* 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 <heinrichh@uni-duesseldorf.de>";

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 <heinrichh@uni-duesseldorf.de>"));
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, "<heinrichh@uni-duesseldorf.de>"));
assertTrue(match(USER_ID + " ", "<heinrichh@uni-duesseldorf.de>"));
assertFalse(match(USER_ID, "<>"));
assertFalse(match(USER_ID, "<h>"));
assertFalse(match(USER_ID, "<heinrichh>"));
assertFalse(match(USER_ID, "<uni-duesseldorf>"));
assertFalse(match(USER_ID, "<h@u>"));
assertFalse(match(USER_ID, "<HeinrichH@uni-duesseldorf.de>"));
assertFalse(match(USER_ID.substring(0, USER_ID.length() - 1),
"<heinrichh@uni-duesseldorf.de>"));
assertFalse(match("", "<>"));
assertFalse(match("", "<heinrichh@uni-duesseldorf.de>"));
}

@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 <j.fade@example.com>", "0xfade"));
assertFalse(match("John Fade <0xfade@example.com>", "0xfade"));
assertFalse(match("", "0xfade"));
}

@Test
public void testImplicitFingerprint() throws Exception {
assertTrue(match("John Fade <j.fade@example.com>", "fade"));
assertTrue(match("John Fade <0xfade@example.com>", "fade"));
assertTrue(match("John Fade <j.fade@example.com>", "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"));
}
}

+ 1
- 1
org.eclipse.jgit/META-INF/MANIFEST.MF View File

@@ -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,

+ 83
- 9
org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java View File

@@ -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.
* <p>
* Not all formats are supported; only formats starting with '=', '&lt;',
* '@', and '*' are handled. Any other format results in a case-insensitive
* substring match.
* </p>
*
* @param userId
* of a key
* @param signingKeySpec
* GPG key identification
* @return whether the {@code userId} matches
* @see <a href=
* "https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html">GPG
* Documentation: How to Specify a User ID</a>
*/
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<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings();
while (keyrings.hasNext()) {
PGPSecretKeyRing keyRing = keyrings.next();
@@ -464,7 +538,7 @@ class BouncyCastleGpgKeyLocator {
Iterator<String> 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<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
while (keyrings.hasNext()) {
PGPPublicKeyRing keyRing = keyrings.next();
@@ -509,7 +583,7 @@ class BouncyCastleGpgKeyLocator {
Iterator<String> userIDs = key.getUserIDs();
while (userIDs.hasNext()) {
String userId = userIDs.next();
if (containsSigningKey(userId)) {
if (containsSigningKey(userId, signingKey)) {
return key;
}
}

+ 1
- 1
org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java View File

@@ -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(

Loading…
Cancel
Save