View Javadoc
1   /*
2    * Copyright (C) 2018, Salesforce.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.lib.internal;
44  
45  import static java.nio.file.Files.exists;
46  import static java.nio.file.Files.newInputStream;
47  
48  import java.io.BufferedInputStream;
49  import java.io.File;
50  import java.io.IOException;
51  import java.io.InputStream;
52  import java.net.URISyntaxException;
53  import java.nio.file.DirectoryStream;
54  import java.nio.file.Files;
55  import java.nio.file.InvalidPathException;
56  import java.nio.file.Path;
57  import java.nio.file.Paths;
58  import java.security.NoSuchAlgorithmException;
59  import java.security.NoSuchProviderException;
60  import java.text.MessageFormat;
61  import java.util.Iterator;
62  import java.util.Locale;
63  import java.util.stream.Collectors;
64  import java.util.stream.Stream;
65  
66  import org.bouncycastle.gpg.SExprParser;
67  import org.bouncycastle.gpg.keybox.BlobType;
68  import org.bouncycastle.gpg.keybox.KeyBlob;
69  import org.bouncycastle.gpg.keybox.KeyBox;
70  import org.bouncycastle.gpg.keybox.KeyInformation;
71  import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
72  import org.bouncycastle.gpg.keybox.UserID;
73  import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder;
74  import org.bouncycastle.openpgp.PGPException;
75  import org.bouncycastle.openpgp.PGPKeyFlags;
76  import org.bouncycastle.openpgp.PGPPublicKey;
77  import org.bouncycastle.openpgp.PGPPublicKeyRing;
78  import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
79  import org.bouncycastle.openpgp.PGPSecretKey;
80  import org.bouncycastle.openpgp.PGPSecretKeyRing;
81  import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
82  import org.bouncycastle.openpgp.PGPSignature;
83  import org.bouncycastle.openpgp.PGPUtil;
84  import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
85  import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
86  import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
87  import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
88  import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
89  import org.bouncycastle.util.encoders.Hex;
90  import org.eclipse.jgit.annotations.NonNull;
91  import org.eclipse.jgit.api.errors.CanceledException;
92  import org.eclipse.jgit.errors.UnsupportedCredentialItem;
93  import org.eclipse.jgit.internal.JGitText;
94  import org.eclipse.jgit.util.FS;
95  import org.eclipse.jgit.util.StringUtils;
96  import org.eclipse.jgit.util.SystemReader;
97  import org.slf4j.Logger;
98  import org.slf4j.LoggerFactory;
99  
100 /**
101  * Locates GPG keys from either <code>~/.gnupg/private-keys-v1.d</code> or
102  * <code>~/.gnupg/secring.gpg</code>
103  */
104 class BouncyCastleGpgKeyLocator {
105 
106 	/** Thrown if a keybox file exists but doesn't contain an OpenPGP key. */
107 	private static class NoOpenPgpKeyException extends Exception {
108 
109 		private static final long serialVersionUID = 1L;
110 
111 	}
112 
113 	private static final Logger log = LoggerFactory
114 			.getLogger(BouncyCastleGpgKeyLocator.class);
115 
116 	private static final Path GPG_DIRECTORY = findGpgDirectory();
117 
118 	private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
119 			.resolve("pubring.kbx"); //$NON-NLS-1$
120 
121 	private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
122 			.resolve("private-keys-v1.d"); //$NON-NLS-1$
123 
124 	private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY
125 			.resolve("pubring.gpg"); //$NON-NLS-1$
126 
127 	private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
128 			.resolve("secring.gpg"); //$NON-NLS-1$
129 
130 	private final String signingKey;
131 
132 	private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt;
133 
134 	private static Path findGpgDirectory() {
135 		SystemReader system = SystemReader.getInstance();
136 		if (system.isWindows()) {
137 			// On Windows prefer %APPDATA%\gnupg if it exists, even if Cygwin is
138 			// used.
139 			String appData = system.getenv("APPDATA"); //$NON-NLS-1$
140 			if (appData != null && !appData.isEmpty()) {
141 				try {
142 					Path directory = Paths.get(appData).resolve("gnupg"); //$NON-NLS-1$
143 					if (Files.isDirectory(directory)) {
144 						return directory;
145 					}
146 				} catch (SecurityException | InvalidPathException e) {
147 					// Ignore and return the default location below.
148 				}
149 			}
150 		}
151 		// All systems, including Cygwin and even Windows if
152 		// %APPDATA%\gnupg doesn't exist: ~/.gnupg
153 		File home = FS.DETECTED.userHome();
154 		if (home == null) {
155 			// Oops. What now?
156 			home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
157 		}
158 		return home.toPath().resolve(".gnupg"); //$NON-NLS-1$
159 	}
160 
161 	/**
162 	 * Create a new key locator for the specified signing key.
163 	 * <p>
164 	 * The signing key must either be a hex representation of a specific key or
165 	 * a user identity substring (eg., email address). All keys in the KeyBox
166 	 * will be looked up in the order as returned by the KeyBox. A key id will
167 	 * be searched before attempting to find a key by user id.
168 	 * </p>
169 	 *
170 	 * @param signingKey
171 	 *            the signing key to search for
172 	 * @param passphrasePrompt
173 	 *            the provider to use when asking for key passphrase
174 	 */
175 	public BouncyCastleGpgKeyLocator(String signingKey,
176 			@NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
177 		this.signingKey = signingKey;
178 		this.passphrasePrompt = passphrasePrompt;
179 	}
180 
181 	private PGPSecretKey attemptParseSecretKey(Path keyFile,
182 			PGPDigestCalculatorProvider calculatorProvider,
183 			PBEProtectionRemoverFactory passphraseProvider,
184 			PGPPublicKey publicKey) {
185 		try (InputStream in = newInputStream(keyFile)) {
186 			return new SExprParser(calculatorProvider).parseSecretKey(
187 					new BufferedInputStream(in), passphraseProvider, publicKey);
188 		} catch (IOException | PGPException | ClassCastException e) {
189 			if (log.isDebugEnabled())
190 				log.debug("Ignoring unreadable file '{}': {}", keyFile, //$NON-NLS-1$
191 						e.getMessage(), e);
192 			return null;
193 		}
194 	}
195 
196 	/**
197 	 * Checks whether a given OpenPGP {@code userId} matches a given
198 	 * {@code signingKeySpec}, which is supposed to have one of the formats
199 	 * defined by GPG.
200 	 * <p>
201 	 * Not all formats are supported; only formats starting with '=', '&lt;',
202 	 * '@', and '*' are handled. Any other format results in a case-insensitive
203 	 * substring match.
204 	 * </p>
205 	 *
206 	 * @param userId
207 	 *            of a key
208 	 * @param signingKeySpec
209 	 *            GPG key identification
210 	 * @return whether the {@code userId} matches
211 	 * @see <a href=
212 	 *      "https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html">GPG
213 	 *      Documentation: How to Specify a User ID</a>
214 	 */
215 	static boolean containsSigningKey(String userId, String signingKeySpec) {
216 		if (StringUtils.isEmptyOrNull(userId)
217 				|| StringUtils.isEmptyOrNull(signingKeySpec)) {
218 			return false;
219 		}
220 		String toMatch = signingKeySpec;
221 		if (toMatch.startsWith("0x") && toMatch.trim().length() > 2) { //$NON-NLS-1$
222 			return false; // Explicit fingerprint
223 		}
224 		int command = toMatch.charAt(0);
225 		switch (command) {
226 		case '=':
227 		case '<':
228 		case '@':
229 		case '*':
230 			toMatch = toMatch.substring(1);
231 			if (toMatch.isEmpty()) {
232 				return false;
233 			}
234 			break;
235 		default:
236 			break;
237 		}
238 		switch (command) {
239 		case '=':
240 			return userId.equals(toMatch);
241 		case '<': {
242 			int begin = userId.indexOf('<');
243 			int end = userId.indexOf('>', begin + 1);
244 			int stop = toMatch.indexOf('>');
245 			return begin >= 0 && end > begin + 1 && stop > 0
246 					&& userId.substring(begin + 1, end)
247 							.equals(toMatch.substring(0, stop));
248 		}
249 		case '@': {
250 			int begin = userId.indexOf('<');
251 			int end = userId.indexOf('>', begin + 1);
252 			return begin >= 0 && end > begin + 1
253 					&& userId.substring(begin + 1, end).contains(toMatch);
254 		}
255 		default:
256 			if (toMatch.trim().isEmpty()) {
257 				return false;
258 			}
259 			return userId.toLowerCase(Locale.ROOT)
260 					.contains(toMatch.toLowerCase(Locale.ROOT));
261 		}
262 	}
263 
264 	private String toFingerprint(String keyId) {
265 		if (keyId.startsWith("0x")) { //$NON-NLS-1$
266 			return keyId.substring(2);
267 		}
268 		return keyId;
269 	}
270 
271 	private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
272 			throws IOException {
273 		String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
274 		if (keyId.isEmpty()) {
275 			return null;
276 		}
277 		for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
278 			String fingerprint = Hex.toHexString(keyInfo.getFingerprint())
279 					.toLowerCase(Locale.ROOT);
280 			if (fingerprint.endsWith(keyId)) {
281 				return getPublicKey(keyBlob, keyInfo.getFingerprint());
282 			}
283 		}
284 		return null;
285 	}
286 
287 	private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
288 			throws IOException {
289 		for (UserID userID : keyBlob.getUserIds()) {
290 			if (containsSigningKey(userID.getUserIDAsString(), signingKey)) {
291 				return getSigningPublicKey(keyBlob);
292 			}
293 		}
294 		return null;
295 	}
296 
297 	/**
298 	 * Finds a public key associated with the signing key.
299 	 *
300 	 * @param keyboxFile
301 	 *            the KeyBox file
302 	 * @return publicKey the public key (maybe <code>null</code>)
303 	 * @throws IOException
304 	 *             in case of problems reading the file
305 	 * @throws NoSuchAlgorithmException
306 	 * @throws NoSuchProviderException
307 	 * @throws NoOpenPgpKeyException
308 	 *             if the file does not contain any OpenPGP key
309 	 */
310 	private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
311 			throws IOException, NoSuchAlgorithmException,
312 			NoSuchProviderException, NoOpenPgpKeyException {
313 		KeyBox keyBox = readKeyBoxFile(keyboxFile);
314 		boolean hasOpenPgpKey = false;
315 		for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
316 			if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
317 				hasOpenPgpKey = true;
318 				PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
319 				if (key != null) {
320 					return key;
321 				}
322 				key = findPublicKeyByUserId(keyBlob);
323 				if (key != null) {
324 					return key;
325 				}
326 			}
327 		}
328 		if (!hasOpenPgpKey) {
329 			throw new NoOpenPgpKeyException();
330 		}
331 		return null;
332 	}
333 
334 	/**
335 	 * If there is a private key directory containing keys, use pubring.kbx or
336 	 * pubring.gpg to find the public key; then try to find the secret key in
337 	 * the directory.
338 	 * <p>
339 	 * If there is no private key directory (or it doesn't contain any keys),
340 	 * try to find the key in secring.gpg directly.
341 	 * </p>
342 	 *
343 	 * @return the secret key
344 	 * @throws IOException
345 	 *             in case of issues reading key files
346 	 * @throws NoSuchAlgorithmException
347 	 * @throws NoSuchProviderException
348 	 * @throws PGPException
349 	 *             in case of issues finding a key, including no key found
350 	 * @throws CanceledException
351 	 * @throws URISyntaxException
352 	 * @throws UnsupportedCredentialItem
353 	 */
354 	@NonNull
355 	public BouncyCastleGpgKey findSecretKey() throws IOException,
356 			NoSuchAlgorithmException, NoSuchProviderException, PGPException,
357 			CanceledException, UnsupportedCredentialItem, URISyntaxException {
358 		BouncyCastleGpgKey key;
359 		PGPPublicKey publicKey = null;
360 		if (hasKeyFiles(USER_SECRET_KEY_DIR)) {
361 			// Use pubring.kbx or pubring.gpg to find the public key, then try
362 			// the key files in the directory. If the public key was found in
363 			// pubring.gpg also try secring.gpg to find the secret key.
364 			if (exists(USER_KEYBOX_PATH)) {
365 				try {
366 					publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH);
367 					if (publicKey != null) {
368 						key = findSecretKeyForKeyBoxPublicKey(publicKey,
369 								USER_KEYBOX_PATH);
370 						if (key != null) {
371 							return key;
372 						}
373 						throw new PGPException(MessageFormat.format(
374 								JGitText.get().gpgNoSecretKeyForPublicKey,
375 								Long.toHexString(publicKey.getKeyID())));
376 					}
377 					throw new PGPException(MessageFormat.format(
378 							JGitText.get().gpgNoPublicKeyFound, signingKey));
379 				} catch (NoOpenPgpKeyException e) {
380 					// There are no OpenPGP keys in the keybox at all: try the
381 					// pubring.gpg, if it exists.
382 					if (log.isDebugEnabled()) {
383 						log.debug("{} does not contain any OpenPGP keys", //$NON-NLS-1$
384 								USER_KEYBOX_PATH);
385 					}
386 				}
387 			}
388 			if (exists(USER_PGP_PUBRING_FILE)) {
389 				publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE);
390 				if (publicKey != null) {
391 					// GPG < 2.1 may have both; the agent using the directory
392 					// and gpg using secring.gpg. GPG >= 2.1 delegates all
393 					// secret key handling to the agent and doesn't use
394 					// secring.gpg at all, even if it exists. Which means for us
395 					// we have to try both since we don't know which GPG version
396 					// the user has.
397 					key = findSecretKeyForKeyBoxPublicKey(publicKey,
398 							USER_PGP_PUBRING_FILE);
399 					if (key != null) {
400 						return key;
401 					}
402 				}
403 			}
404 			if (publicKey == null) {
405 				throw new PGPException(MessageFormat.format(
406 						JGitText.get().gpgNoPublicKeyFound, signingKey));
407 			}
408 			// We found a public key, but didn't find the secret key in the
409 			// private key directory. Go try the secring.gpg.
410 		}
411 		boolean hasSecring = false;
412 		if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
413 			hasSecring = true;
414 			key = loadKeyFromSecring(USER_PGP_LEGACY_SECRING_FILE);
415 			if (key != null) {
416 				return key;
417 			}
418 		}
419 		if (publicKey != null) {
420 			throw new PGPException(MessageFormat.format(
421 					JGitText.get().gpgNoSecretKeyForPublicKey,
422 					Long.toHexString(publicKey.getKeyID())));
423 		} else if (hasSecring) {
424 			// publicKey == null: user has _only_ pubring.gpg/secring.gpg.
425 			throw new PGPException(MessageFormat.format(
426 					JGitText.get().gpgNoKeyInLegacySecring, signingKey));
427 		} else {
428 			throw new PGPException(JGitText.get().gpgNoKeyring);
429 		}
430 	}
431 
432 	private boolean hasKeyFiles(Path dir) {
433 		try (DirectoryStream<Path> contents = Files.newDirectoryStream(dir,
434 				"*.key")) { //$NON-NLS-1$
435 			return contents.iterator().hasNext();
436 		} catch (IOException e) {
437 			// Not a directory, or something else
438 			return false;
439 		}
440 	}
441 
442 	private BouncyCastleGpgKey loadKeyFromSecring(Path secring)
443 			throws IOException, PGPException {
444 		PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
445 				secring);
446 
447 		if (secretKey != null) {
448 			if (!secretKey.isSigningKey()) {
449 				throw new PGPException(MessageFormat
450 						.format(JGitText.get().gpgNotASigningKey, signingKey));
451 			}
452 			return new BouncyCastleGpgKey(secretKey, secring);
453 		}
454 		return null;
455 	}
456 
457 	private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
458 			PGPPublicKey publicKey, Path userKeyboxPath)
459 			throws PGPException, CanceledException, UnsupportedCredentialItem,
460 			URISyntaxException {
461 		/*
462 		 * this is somewhat brute-force but there doesn't seem to be another
463 		 * way; we have to walk all private key files we find and try to open
464 		 * them
465 		 */
466 
467 		PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
468 				.build();
469 
470 		PBEProtectionRemoverFactory passphraseProvider = new JcePBEProtectionRemoverFactory(
471 				passphrasePrompt.getPassphrase(publicKey.getFingerprint(),
472 						userKeyboxPath));
473 
474 		try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
475 			for (Path keyFile : keyFiles.filter(Files::isRegularFile)
476 					.collect(Collectors.toList())) {
477 				PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
478 						calculatorProvider, passphraseProvider, publicKey);
479 				if (secretKey != null) {
480 					if (!secretKey.isSigningKey()) {
481 						throw new PGPException(MessageFormat.format(
482 								JGitText.get().gpgNotASigningKey, signingKey));
483 					}
484 					return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
485 				}
486 			}
487 
488 			passphrasePrompt.clear();
489 			return null;
490 		} catch (RuntimeException e) {
491 			passphrasePrompt.clear();
492 			throw e;
493 		} catch (IOException e) {
494 			passphrasePrompt.clear();
495 			throw new PGPException(MessageFormat.format(
496 					JGitText.get().gpgFailedToParseSecretKey,
497 					USER_SECRET_KEY_DIR.toAbsolutePath()), e);
498 		}
499 	}
500 
501 	/**
502 	 * Return the first suitable key for signing in the key ring collection. For
503 	 * this case we only expect there to be one key available for signing.
504 	 * </p>
505 	 *
506 	 * @param signingkey
507 	 * @param secringFile
508 	 *
509 	 * @return the first suitable PGP secret key found for signing
510 	 * @throws IOException
511 	 *             on I/O related errors
512 	 * @throws PGPException
513 	 *             on BouncyCastle errors
514 	 */
515 	private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
516 			Path secringFile) throws IOException, PGPException {
517 
518 		try (InputStream in = newInputStream(secringFile)) {
519 			PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
520 					PGPUtil.getDecoderStream(new BufferedInputStream(in)),
521 					new JcaKeyFingerprintCalculator());
522 
523 			String keyId = toFingerprint(signingkey).toLowerCase(Locale.ROOT);
524 			Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings();
525 			while (keyrings.hasNext()) {
526 				PGPSecretKeyRing keyRing = keyrings.next();
527 				Iterator<PGPSecretKey> keys = keyRing.getSecretKeys();
528 				while (keys.hasNext()) {
529 					PGPSecretKey key = keys.next();
530 					// try key id
531 					String fingerprint = Hex
532 							.toHexString(key.getPublicKey().getFingerprint())
533 							.toLowerCase(Locale.ROOT);
534 					if (fingerprint.endsWith(keyId)) {
535 						return key;
536 					}
537 					// try user id
538 					Iterator<String> userIDs = key.getUserIDs();
539 					while (userIDs.hasNext()) {
540 						String userId = userIDs.next();
541 						if (containsSigningKey(userId, signingKey)) {
542 							return key;
543 						}
544 					}
545 				}
546 			}
547 		}
548 		return null;
549 	}
550 
551 	/**
552 	 * Return the first public key matching the key id ({@link #signingKey}.
553 	 *
554 	 * @param pubringFile
555 	 *
556 	 * @return the PGP public key, or {@code null} if none found
557 	 * @throws IOException
558 	 *             on I/O related errors
559 	 * @throws PGPException
560 	 *             on BouncyCastle errors
561 	 */
562 	private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
563 			throws IOException, PGPException {
564 		try (InputStream in = newInputStream(pubringFile)) {
565 			PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
566 					new BufferedInputStream(in),
567 					new JcaKeyFingerprintCalculator());
568 
569 			String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
570 			Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
571 			while (keyrings.hasNext()) {
572 				PGPPublicKeyRing keyRing = keyrings.next();
573 				Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
574 				while (keys.hasNext()) {
575 					PGPPublicKey key = keys.next();
576 					// try key id
577 					String fingerprint = Hex.toHexString(key.getFingerprint())
578 							.toLowerCase(Locale.ROOT);
579 					if (fingerprint.endsWith(keyId)) {
580 						return key;
581 					}
582 					// try user id
583 					Iterator<String> userIDs = key.getUserIDs();
584 					while (userIDs.hasNext()) {
585 						String userId = userIDs.next();
586 						if (containsSigningKey(userId, signingKey)) {
587 							return key;
588 						}
589 					}
590 				}
591 			}
592 		}
593 		return null;
594 	}
595 
596 	private PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
597 			throws IOException {
598 		return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing()
599 				.getPublicKey(fingerprint);
600 	}
601 
602 	private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException {
603 		PGPPublicKey masterKey = null;
604 		Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob)
605 				.getPGPPublicKeyRing().getPublicKeys();
606 		while (keys.hasNext()) {
607 			PGPPublicKey key = keys.next();
608 			// only consider keys that have the [S] usage flag set
609 			if (isSigningKey(key)) {
610 				if (key.isMasterKey()) {
611 					masterKey = key;
612 				} else {
613 					return key;
614 				}
615 			}
616 		}
617 		// return the master key if no other signing key was found or null if
618 		// the master key did not have the signing flag set
619 		return masterKey;
620 	}
621 
622 	private boolean isSigningKey(PGPPublicKey key) {
623 		Iterator signatures = key.getSignatures();
624 		while (signatures.hasNext()) {
625 			PGPSignature sig = (PGPSignature) signatures.next();
626 			if ((sig.getHashedSubPackets().getKeyFlags()
627 					& PGPKeyFlags.CAN_SIGN) > 0) {
628 				return true;
629 			}
630 		}
631 		return false;
632 	}
633 
634 	private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
635 			NoSuchAlgorithmException, NoSuchProviderException,
636 			NoOpenPgpKeyException {
637 		if (keyboxFile.toFile().length() == 0) {
638 			throw new NoOpenPgpKeyException();
639 		}
640 		KeyBox keyBox;
641 		try (InputStream in = new BufferedInputStream(
642 				newInputStream(keyboxFile))) {
643 			keyBox = new JcaKeyBoxBuilder().build(in);
644 		}
645 		return keyBox;
646 	}
647 }