GPG: also consider pubring.gpg when looking for keys

The algorithm for finding keys was already improved in commit db0eb9f8,
but that wasn't quite correct yet.

If there is no pubring.kbx but a private-keys-v1.d directory and a
pubring.gpg, GPG also uses pubring.gpg in combination with the
private-keys-v1.d directory. GPG has three ways to locate public and
private key pairs:

* pubring.kbx and private-keys-v1.d (GPG >= 2.1)
* pubring.gpg and private-keys-v1.d (GPG >= 2.1)
* pubring.gpg and secring.gpg (GPG < 2.1)

See [1] and [2]. pubring.kbx may not exist if the user migrated from
an older GPG installation and didn't run the agent. Since we don't
know which GPG version the user has we must try secring.gpg also if
we found the public key in pubring.gpg, but didn't find the secret
key in the private key directory. Note that GPG < 2.1 also may have
a private key directory, used by the agent. But it may also _not_ have
that directory.

[1] https://lists.gnupg.org/pipermail/gnupg-users/2015-December/054881.html
[2] https://www.gnupg.org/faq/whats-new-in-2.1.html#nosecring

Bug: 549439
Change-Id: I6088014b16c585b6a3408bb31dba3c116e6b583d
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2019-08-15 11:46:46 +02:00
parent db0eb9f8ae
commit 39e83a6583
1 changed files with 128 additions and 28 deletions

View File

@ -50,6 +50,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
@ -72,6 +73,8 @@
import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
@ -115,6 +118,9 @@ private static class NoOpenPgpKeyException extends Exception {
private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
.resolve("private-keys-v1.d"); //$NON-NLS-1$
private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY
.resolve("pubring.gpg"); //$NON-NLS-1$
private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
.resolve("secring.gpg"); //$NON-NLS-1$
@ -250,8 +256,13 @@ private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
}
/**
* Use pubring.kbx when available, if not fallback to secring.gpg or secret
* key path provided to parse and return secret key
* If there is a private key directory containing keys, use pubring.kbx or
* pubring.gpg to find the public key; then try to find the secret key in
* the directory.
* <p>
* If there is no private key directory (or it doesn't contain any keys),
* try to find the key in secring.gpg directly.
* </p>
*
* @return the secret key
* @throws IOException
@ -259,51 +270,97 @@ private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @throws PGPException
* in case of issues finding a key
* in case of issues finding a key, including no key found
* @throws CanceledException
* @throws URISyntaxException
* @throws UnsupportedCredentialItem
*/
@NonNull
public BouncyCastleGpgKey findSecretKey() throws IOException,
NoSuchAlgorithmException, NoSuchProviderException, PGPException,
CanceledException, UnsupportedCredentialItem, URISyntaxException {
BouncyCastleGpgKey key;
if (exists(USER_KEYBOX_PATH)) {
try {
key = loadKeyFromKeybox(USER_KEYBOX_PATH);
if (key != null) {
return key;
}
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoPublicKeyFound, signingKey));
} catch (NoOpenPgpKeyException e) {
// Ignore and try the secring.gpg, if it exists.
if (log.isDebugEnabled()) {
log.debug("{} does not contain any OpenPGP keys", //$NON-NLS-1$
USER_KEYBOX_PATH);
PGPPublicKey publicKey = null;
if (hasKeyFiles(USER_SECRET_KEY_DIR)) {
// Use pubring.kbx or pubring.gpg to find the public key, then try
// the key files in the directory. If the public key was found in
// pubring.gpg also try secring.gpg to find the secret key.
if (exists(USER_KEYBOX_PATH)) {
try {
publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH);
if (publicKey != null) {
key = findSecretKeyForKeyBoxPublicKey(publicKey,
USER_KEYBOX_PATH);
if (key != null) {
return key;
}
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoSecretKeyForPublicKey,
Long.toHexString(publicKey.getKeyID())));
}
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoPublicKeyFound, signingKey));
} catch (NoOpenPgpKeyException e) {
// There are no OpenPGP keys in the keybox at all: try the
// pubring.gpg, if it exists.
if (log.isDebugEnabled()) {
log.debug("{} does not contain any OpenPGP keys", //$NON-NLS-1$
USER_KEYBOX_PATH);
}
}
}
if (exists(USER_PGP_PUBRING_FILE)) {
publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE);
if (publicKey != null) {
// GPG < 2.1 may have both; the agent using the directory
// and gpg using secring.gpg. GPG >= 2.1 delegates all
// secret key handling to the agent and doesn't use
// secring.gpg at all, even if it exists. Which means for us
// we have to try both since we don't know which GPG version
// the user has.
key = findSecretKeyForKeyBoxPublicKey(publicKey,
USER_PGP_PUBRING_FILE);
if (key != null) {
return key;
}
}
}
if (publicKey == null) {
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoPublicKeyFound, signingKey));
}
// We found a public key, but didn't find the secret key in the
// private key directory. Go try the secring.gpg.
}
boolean hasSecring = false;
if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
hasSecring = true;
key = loadKeyFromSecring(USER_PGP_LEGACY_SECRING_FILE);
if (key != null) {
return key;
}
}
if (publicKey != null) {
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoSecretKeyForPublicKey,
Long.toHexString(publicKey.getKeyID())));
} else if (hasSecring) {
// publicKey == null: user has _only_ pubring.gpg/secring.gpg.
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoKeyInLegacySecring, signingKey));
} else {
throw new PGPException(JGitText.get().gpgNoKeyring);
}
throw new PGPException(JGitText.get().gpgNoKeyring);
}
private BouncyCastleGpgKey loadKeyFromKeybox(Path keybox)
throws NoOpenPgpKeyException, NoSuchAlgorithmException,
NoSuchProviderException, IOException, CanceledException,
UnsupportedCredentialItem, PGPException, URISyntaxException {
PGPPublicKey publicKey = findPublicKeyInKeyBox(keybox);
if (publicKey != null) {
return findSecretKeyForKeyBoxPublicKey(publicKey, keybox);
private boolean hasKeyFiles(Path dir) {
try (DirectoryStream<Path> contents = Files.newDirectoryStream(dir,
"*.key")) { //$NON-NLS-1$
return contents.iterator().hasNext();
} catch (IOException e) {
// Not a directory, or something else
return false;
}
return null;
}
private BouncyCastleGpgKey loadKeyFromSecring(Path secring)
@ -353,9 +410,7 @@ private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
}
passphrasePrompt.clear();
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoSecretKeyForPublicKey,
Long.toHexString(publicKey.getKeyID())));
return null;
} catch (RuntimeException e) {
passphrasePrompt.clear();
throw e;
@ -417,6 +472,51 @@ private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
return null;
}
/**
* Return the first public key matching the key id ({@link #signingKey}.
*
* @param pubringFile
*
* @return the PGP public key, or {@code null} if none found
* @throws IOException
* on I/O related errors
* @throws PGPException
* on BouncyCastle errors
*/
private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
throws IOException, PGPException {
try (InputStream in = newInputStream(pubringFile)) {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
new BufferedInputStream(in),
new JcaKeyFingerprintCalculator());
String keyId = signingKey.toLowerCase(Locale.ROOT);
Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
while (keyrings.hasNext()) {
PGPPublicKeyRing keyRing = keyrings.next();
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
while (keys.hasNext()) {
PGPPublicKey key = keys.next();
// try key id
String fingerprint = Hex.toHexString(key.getFingerprint())
.toLowerCase(Locale.ROOT);
if (fingerprint.endsWith(keyId)) {
return key;
}
// try user id
Iterator<String> userIDs = key.getUserIDs();
while (userIDs.hasNext()) {
String userId = userIDs.next();
if (containsSigningKey(userId)) {
return key;
}
}
}
}
}
return null;
}
private PGPPublicKey getFirstPublicKey(KeyBlob keyBlob) throws IOException {
return ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing()
.getPublicKey();