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