warn) {
if (!StringUtils.isEmptyOrNull(dir)) {
try {
Path directory = toPath.apply(dir);
if (Files.isDirectory(directory)) {
return directory;
}
} catch (SecurityException | InvalidPathException e) {
// Ignore, warn, and try other known directories
}
if (warn != null) {
warn.accept(dir);
}
}
return null;
}
/**
* Create a new key locator for the specified signing key.
*
* The signing key must either be a hex representation of a specific key or
* a user identity substring (eg., email address). All keys in the KeyBox
* will be looked up in the order as returned by the KeyBox. A key id will
* be searched before attempting to find a key by user id.
*
*
* @param signingKey
* the signing key to search for
* @param passphrasePrompt
* the provider to use when asking for key passphrase
*/
public BouncyCastleGpgKeyLocator(String signingKey,
@NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
this.signingKey = signingKey;
this.passphrasePrompt = passphrasePrompt;
}
private PGPSecretKey attemptParseSecretKey(Path keyFile,
PGPDigestCalculatorProvider calculatorProvider,
SecretKeys.PassphraseSupplier passphraseSupplier,
PGPPublicKey publicKey)
throws IOException, PGPException, CanceledException,
UnsupportedCredentialItem, URISyntaxException {
try (InputStream in = newInputStream(keyFile)) {
return SecretKeys.readSecretKey(in, calculatorProvider,
passphraseSupplier, publicKey);
}
}
/**
* 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)
.equalsIgnoreCase(toMatch.substring(0, stop));
}
case '@': {
int begin = userId.indexOf('<');
int end = userId.indexOf('>', begin + 1);
return begin >= 0 && end > begin + 1
&& containsIgnoreCase(userId.substring(begin + 1, end),
toMatch);
}
default:
if (toMatch.trim().isEmpty()) {
return false;
}
return containsIgnoreCase(userId, toMatch);
}
}
private static boolean containsIgnoreCase(String a, String b) {
int alength = a.length();
int blength = b.length();
for (int i = 0; i + blength <= alength; i++) {
if (a.regionMatches(true, i, b, 0, blength)) {
return true;
}
}
return false;
}
private static String toFingerprint(String keyId) {
if (keyId.startsWith("0x")) { //$NON-NLS-1$
return keyId.substring(2);
}
return keyId;
}
static BouncyCastleGpgPublicKey findPublicKey(String fingerprint,
String keySpec)
throws IOException, PGPException {
BouncyCastleGpgPublicKey result = findPublicKeyInPubring(
USER_PGP_PUBRING_FILE,
fingerprint, keySpec);
if (result == null && exists(USER_KEYBOX_PATH)) {
try {
result = findPublicKeyInKeyBox(USER_KEYBOX_PATH, fingerprint,
keySpec);
} catch (NoSuchAlgorithmException | NoSuchProviderException
| IOException | NoOpenPgpKeyException e) {
log.error(e.getMessage(), e);
}
}
return result;
}
private static PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob,
String keyId)
throws IOException {
if (keyId.isEmpty()) {
return null;
}
for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
String fingerprint = Hex.toHexString(keyInfo.getFingerprint())
.toLowerCase(Locale.ROOT);
if (fingerprint.endsWith(keyId)) {
return getPublicKey(keyBlob, keyInfo.getFingerprint());
}
}
return null;
}
private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob,
String keySpec)
throws IOException {
for (UserID userID : keyBlob.getUserIds()) {
if (containsSigningKey(userID.getUserIDAsString(), keySpec)) {
return getSigningPublicKey(keyBlob);
}
}
return null;
}
/**
* Finds a public key associated with the signing key.
*
* @param keyboxFile
* the KeyBox file
* @param keyId
* to look for, may be null
* @param keySpec
* to look for
* @return publicKey the public key (maybe null
)
* @throws IOException
* in case of problems reading the file
* @throws NoSuchAlgorithmException
* if an algorithm isn't available
* @throws NoSuchProviderException
* if a provider isn't available
* @throws NoOpenPgpKeyException
* if the file does not contain any OpenPGP key
*/
private static BouncyCastleGpgPublicKey findPublicKeyInKeyBox(
Path keyboxFile,
String keyId, String keySpec)
throws IOException, NoSuchAlgorithmException,
NoSuchProviderException, NoOpenPgpKeyException {
KeyBox keyBox = readKeyBoxFile(keyboxFile);
String id = keyId != null ? keyId
: toFingerprint(keySpec).toLowerCase(Locale.ROOT);
boolean hasOpenPgpKey = false;
for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
hasOpenPgpKey = true;
PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id);
if (key != null) {
if (!isSigningKey(key)) {
return null;
}
return new BouncyCastleGpgPublicKey(key, true,
toStrings(keyBlob.getUserIds()));
}
key = findPublicKeyByUserId(keyBlob, keySpec);
if (key != null) {
return new BouncyCastleGpgPublicKey(key, true,
toStrings(keyBlob.getUserIds()));
}
}
}
if (!hasOpenPgpKey) {
throw new NoOpenPgpKeyException();
}
return null;
}
private static List toStrings(List userIds) {
if (userIds == null) {
return Collections.emptyList();
}
return userIds.stream().map(UserID::getUserIDAsString)
.collect(Collectors.toList());
}
/**
* 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.
*
* If there is no private key directory (or it doesn't contain any keys),
* try to find the key in secring.gpg directly.
*
*
* @return the secret key
* @throws IOException
* in case of issues reading key files
* @throws NoSuchAlgorithmException
* algorithm is not available
* @throws NoSuchProviderException
* provider is not available
* @throws PGPException
* in case of issues finding a key, including no key found
* @throws CanceledException
* operation was cancelled
* @throws URISyntaxException
* URI is invalid
* @throws UnsupportedCredentialItem
* credential item is not supported
*/
@NonNull
public BouncyCastleGpgKey findSecretKey() throws IOException,
NoSuchAlgorithmException, NoSuchProviderException, PGPException,
CanceledException, UnsupportedCredentialItem, URISyntaxException {
BouncyCastleGpgKey key;
BouncyCastleGpgPublicKey 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, null,
signingKey);
if (publicKey != null) {
key = findSecretKeyForKeyBoxPublicKey(
publicKey.getPublicKey(), USER_KEYBOX_PATH);
if (key != null) {
return key;
}
throw new PGPException(MessageFormat.format(
BCText.get().gpgNoSecretKeyForPublicKey,
Long.toHexString(
publicKey.getPublicKey().getKeyID())));
}
throw new PGPException(MessageFormat.format(
BCText.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, null,
signingKey);
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.getPublicKey(),
USER_PGP_PUBRING_FILE);
if (key != null) {
return key;
}
}
}
if (publicKey == null) {
throw new PGPException(MessageFormat.format(
BCText.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(
BCText.get().gpgNoSecretKeyForPublicKey,
Long.toHexString(publicKey.getPublicKey().getKeyID())));
} else if (hasSecring) {
// publicKey == null: user has _only_ pubring.gpg/secring.gpg.
throw new PGPException(MessageFormat.format(
BCText.get().gpgNoKeyInLegacySecring, signingKey));
} else {
throw new PGPException(BCText.get().gpgNoKeyring);
}
}
private boolean hasKeyFiles(Path dir) {
try (DirectoryStream contents = Files.newDirectoryStream(dir,
"*.key")) { //$NON-NLS-1$
return contents.iterator().hasNext();
} catch (IOException e) {
// Not a directory, or something else
return false;
}
}
private BouncyCastleGpgKey loadKeyFromSecring(Path secring)
throws IOException, PGPException {
PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
secring);
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
throw new PGPException(MessageFormat
.format(BCText.get().gpgNotASigningKey, signingKey));
}
return new BouncyCastleGpgKey(secretKey, secring);
}
return null;
}
private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
PGPPublicKey publicKey, Path userKeyboxPath)
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
byte[] keyGrip = null;
try {
keyGrip = KeyGrip.getKeyGrip(publicKey);
} catch (PGPException e) {
throw new PGPException(
MessageFormat.format(BCText.get().gpgNoKeygrip,
Hex.toHexString(publicKey.getFingerprint())),
e);
}
String filename = Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT)
+ ".key"; //$NON-NLS-1$
Path keyFile = USER_SECRET_KEY_DIR.resolve(filename);
if (!Files.exists(keyFile)) {
return null;
}
boolean clearPrompt = false;
try {
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
clearPrompt = true;
PGPSecretKey secretKey = null;
try {
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
() -> passphrasePrompt.getPassphrase(
publicKey.getFingerprint(), userKeyboxPath),
publicKey);
} catch (PGPException e) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
keyFile.toAbsolutePath()), e);
}
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgNotASigningKey, signingKey));
}
clearPrompt = false;
return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
}
return null;
} catch (RuntimeException e) {
throw e;
} catch (FileNotFoundException | NoSuchFileException e) {
clearPrompt = false;
return null;
} catch (IOException e) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
keyFile.toAbsolutePath()), e);
} finally {
if (clearPrompt) {
passphrasePrompt.clear();
}
}
}
/**
* Return the first suitable key for signing in the key ring collection. For
* this case we only expect there to be one key available for signing.
*
* @param signingKeyName
* the signing key
* @param secringFile
* the secring file
*
* @return the first suitable PGP secret key found for signing
* @throws IOException
* on I/O related errors
* @throws PGPException
* on BouncyCastle errors
*/
private PGPSecretKey findSecretKeyInLegacySecring(String signingKeyName,
Path secringFile) throws IOException, PGPException {
try (InputStream in = newInputStream(secringFile)) {
PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
PGPUtil.getDecoderStream(new BufferedInputStream(in)),
new JcaKeyFingerprintCalculator());
String keyId = toFingerprint(signingKeyName).toLowerCase(Locale.ROOT);
Iterator keyrings = pgpSec.getKeyRings();
while (keyrings.hasNext()) {
PGPSecretKeyRing keyRing = keyrings.next();
Iterator keys = keyRing.getSecretKeys();
while (keys.hasNext()) {
PGPSecretKey key = keys.next();
// try key id
String fingerprint = Hex
.toHexString(key.getPublicKey().getFingerprint())
.toLowerCase(Locale.ROOT);
if (fingerprint.endsWith(keyId)) {
return key;
}
// try user id
Iterator userIDs = key.getUserIDs();
while (userIDs.hasNext()) {
String userId = userIDs.next();
if (containsSigningKey(userId, signingKey)) {
return key;
}
}
}
}
}
return null;
}
/**
* Return the first public key matching the key id ({@link #signingKey}.
*
* @param pubringFile
* to search
* @param keyId
* to look for, may be null
* @param keySpec
* to look for
*
* @return the PGP public key, or {@code null} if none found
* @throws IOException
* on I/O related errors
* @throws PGPException
* on BouncyCastle errors
*/
private static BouncyCastleGpgPublicKey findPublicKeyInPubring(
Path pubringFile,
String keyId, String keySpec)
throws IOException, PGPException {
try (InputStream in = newInputStream(pubringFile)) {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
new BufferedInputStream(in),
new JcaKeyFingerprintCalculator());
String id = keyId != null ? keyId
: toFingerprint(keySpec).toLowerCase(Locale.ROOT);
Iterator keyrings = pgpPub.getKeyRings();
BouncyCastleGpgPublicKey candidate = null;
while (keyrings.hasNext()) {
PGPPublicKeyRing keyRing = keyrings.next();
BouncyCastleGpgPublicKey newCandidate = findPublicKeyInPubring(
keyRing, id, keySpec);
if (newCandidate != null) {
if (newCandidate.isExactMatch()) {
return newCandidate;
} else if (candidate == null) {
candidate = newCandidate;
}
}
}
return candidate;
} catch (FileNotFoundException | NoSuchFileException e) {
return null;
}
}
private static BouncyCastleGpgPublicKey findPublicKeyInPubring(
PGPPublicKeyRing keyRing, String keyId, String keySpec) {
Iterator keys = keyRing.getPublicKeys();
if (!keys.hasNext()) {
return null;
}
PGPPublicKey masterKey = keys.next();
String fingerprint = Hex.toHexString(masterKey.getFingerprint())
.toLowerCase(Locale.ROOT);
boolean masterFingerprintMatch = false;
boolean userIdMatch = false;
List userIds = new ArrayList<>();
masterKey.getUserIDs().forEachRemaining(userIds::add);
if (fingerprint.endsWith(keyId)) {
masterFingerprintMatch = true;
} else {
// Check the user IDs
for (String userId : userIds) {
if (containsSigningKey(userId, keySpec)) {
userIdMatch = true;
break;
}
}
}
if (masterFingerprintMatch) {
if (isSigningKey(masterKey)) {
return new BouncyCastleGpgPublicKey(masterKey, true, userIds);
}
}
// Check subkeys -- they have no user ids, so only check for a
// fingerprint match (unless the master key matched).
PGPPublicKey candidate = null;
while (keys.hasNext()) {
PGPPublicKey subKey = keys.next();
if (!isSigningKey(subKey)) {
continue;
}
if (masterFingerprintMatch) {
candidate = subKey;
break;
}
fingerprint = Hex.toHexString(subKey.getFingerprint())
.toLowerCase(Locale.ROOT);
if (fingerprint.endsWith(keyId)) {
return new BouncyCastleGpgPublicKey(subKey, true, userIds);
}
if (candidate == null) {
candidate = subKey;
}
}
if (candidate != null && (masterFingerprintMatch || userIdMatch)) {
return new BouncyCastleGpgPublicKey(candidate, false, userIds);
}
return null;
}
private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
throws IOException {
return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing()
.getPublicKey(fingerprint);
}
private static PGPPublicKey getSigningPublicKey(KeyBlob blob)
throws IOException {
PGPPublicKey masterKey = null;
Iterator keys = ((PublicKeyRingBlob) blob)
.getPGPPublicKeyRing().getPublicKeys();
while (keys.hasNext()) {
PGPPublicKey key = keys.next();
// only consider keys that have the [S] usage flag set
if (isSigningKey(key)) {
if (key.isMasterKey()) {
masterKey = key;
} else {
return key;
}
}
}
// return the master key if no other signing key was found or null if
// the master key did not have the signing flag set
return masterKey;
}
private static boolean isSigningKey(PGPPublicKey key) {
Iterator signatures = key.getSignatures();
while (signatures.hasNext()) {
PGPSignature sig = (PGPSignature) signatures.next();
if ((sig.getHashedSubPackets().getKeyFlags()
& PGPKeyFlags.CAN_SIGN) > 0) {
return true;
}
}
return false;
}
private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
NoSuchAlgorithmException, NoSuchProviderException,
NoOpenPgpKeyException {
if (keyboxFile.toFile().length() == 0) {
throw new NoOpenPgpKeyException();
}
KeyBox keyBox;
try (InputStream in = new BufferedInputStream(
newInputStream(keyboxFile))) {
keyBox = new JcaKeyBoxBuilder().build(in);
}
return keyBox;
}
}
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
Content-Type: text/plain; charset=UTF-8
Content-Length: 3312
Content-Disposition: inline; filename="BouncyCastleGpgKeyPassphrasePrompt.java"
Last-Modified: Thu, 14 Aug 2025 05:02:44 GMT
Expires: Thu, 14 Aug 2025 05:07:44 GMT
ETag: "463b661127aabd3184813c5e90b59853dd01adfb"
/*-
* Copyright (C) 2019, 2020 Salesforce and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.text.MessageFormat;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem.InformationalMessage;
import org.eclipse.jgit.transport.CredentialItem.Password;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
/**
* Prompts for a passphrase and caches it until {@link #clear() cleared}.
*
* Implements {@link AutoCloseable} so it can be used within a
* try-with-resources block.
*
*/
class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
private Password passphrase;
private CredentialsProvider credentialsProvider;
public BouncyCastleGpgKeyPassphrasePrompt(
CredentialsProvider credentialsProvider) {
this.credentialsProvider = credentialsProvider;
}
/**
* Clears any cached passphrase
*/
public void clear() {
if (passphrase != null) {
passphrase.clear();
passphrase = null;
}
}
@Override
public void close() {
clear();
}
private URIish createURI(Path keyLocation) throws URISyntaxException {
return new URIish(keyLocation.toUri().toString());
}
/**
* Prompts use for a passphrase unless one was cached from a previous
* prompt.
*
* @param keyFingerprint
* the fingerprint to show to the user during prompting
* @param keyLocation
* the location the key was loaded from
* @return the passphrase (maybe null
)
* @throws PGPException
* if a PGP problem occurred
* @throws CanceledException
* in case passphrase was not entered by user
* @throws URISyntaxException
* if the URI isn't parseable
* @throws UnsupportedCredentialItem
* if a credential item isn't supported
*/
public char[] getPassphrase(byte[] keyFingerprint, Path keyLocation)
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
if (passphrase == null) {
passphrase = new Password(BCText.get().credentialPassphrase);
}
if (credentialsProvider == null) {
throw new PGPException(BCText.get().gpgNoCredentialsProvider);
}
if (passphrase.getValue() == null
&& !credentialsProvider.get(createURI(keyLocation),
new InformationalMessage(
MessageFormat.format(BCText.get().gpgKeyInfo,
Hex.toHexString(keyFingerprint))),
passphrase)) {
throw new CanceledException(BCText.get().gpgSigningCancelled);
}
return passphrase.getValue();
}
/**
* Determines whether a passphrase was already obtained.
*
* @return {@code true} if a passphrase is already set, {@code false}
* otherwise
*/
public boolean hasPassphrase() {
return passphrase != null && passphrase.getValue() != null;
}
}
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
Content-Type: text/plain; charset=UTF-8
Content-Length: 973
Content-Disposition: inline; filename="BouncyCastleGpgPublicKey.java"
Last-Modified: Thu, 14 Aug 2025 05:02:44 GMT
Expires: Thu, 14 Aug 2025 05:07:44 GMT
ETag: "9ec5b455304277a110f12d23f70e6d93dc80a73c"
/*
* Copyright (C) 2024 Thomas Wolf and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal;
import java.util.List;
import org.bouncycastle.openpgp.PGPPublicKey;
/**
* Container for GPG public keys.
*/
class BouncyCastleGpgPublicKey {
private final PGPPublicKey publicKey;
private final boolean exactMatch;
private final List userIds;
BouncyCastleGpgPublicKey(PGPPublicKey publicKey, boolean exactMatch,
List userIds) {
this.publicKey = publicKey;
this.exactMatch = exactMatch;
this.userIds = userIds;
}
PGPPublicKey getPublicKey() {
return publicKey;
}
boolean isExactMatch() {
return exactMatch;
}
List getUserIds() {
return userIds;
}
}
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
Content-Type: text/plain; charset=UTF-8
Content-Length: 8639
Content-Disposition: inline; filename="BouncyCastleGpgSignatureVerifier.java"
Last-Modified: Thu, 14 Aug 2025 05:02:44 GMT
Expires: Thu, 14 Aug 2025 05:07:44 GMT
ETag: "5a3d43ba5434ea4a76d9ad3074fd81d050179d38"
/*
* Copyright (C) 2021, Thomas Wolf and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import org.bouncycastle.bcpg.sig.IssuerFingerprint;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.lib.GpgConfig;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.SignatureVerifier;
import org.eclipse.jgit.util.LRUMap;
import org.eclipse.jgit.util.StringUtils;
/**
* A {@link SignatureVerifier} to verify GPG signatures using BouncyCastle.
*/
public class BouncyCastleGpgSignatureVerifier
implements SignatureVerifier {
private static final String NAME = "bc"; //$NON-NLS-1$
// To support more efficient signature verification of multiple objects we
// cache public keys once found in a LRU cache.
private static final Object NO_KEY = new Object();
private LRUMap byFingerprint = new LRUMap<>(16, 200);
private LRUMap bySigner = new LRUMap<>(16, 200);
@Override
public String getName() {
return NAME;
}
static PGPSignature parseSignature(InputStream in)
throws IOException, PGPException {
try (InputStream sigIn = PGPUtil.getDecoderStream(in)) {
JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(sigIn);
Object obj = pgpFactory.nextObject();
if (obj instanceof PGPCompressedData) {
obj = new JcaPGPObjectFactory(
((PGPCompressedData) obj).getDataStream()).nextObject();
}
if (obj instanceof PGPSignatureList) {
return ((PGPSignatureList) obj).get(0);
}
return null;
}
}
@Override
public SignatureVerification verify(Repository repository, GpgConfig config,
byte[] data, byte[] signatureData) throws IOException {
PGPSignature signature = null;
String fingerprint = null;
String signer = null;
String keyId = null;
try (InputStream sigIn = new ByteArrayInputStream(signatureData)) {
signature = parseSignature(sigIn);
if (signature != null) {
// Try to figure out something to find the public key with.
if (signature.hasSubpackets()) {
PGPSignatureSubpacketVector packets = signature
.getHashedSubPackets();
IssuerFingerprint fingerprintPacket = packets
.getIssuerFingerprint();
if (fingerprintPacket != null) {
fingerprint = Hex
.toHexString(fingerprintPacket.getFingerprint())
.toLowerCase(Locale.ROOT);
}
signer = packets.getSignerUserID();
if (signer != null) {
signer = BouncyCastleGpgSigner.extractSignerId(signer);
}
}
keyId = Long.toUnsignedString(signature.getKeyID(), 16)
.toLowerCase(Locale.ROOT);
} else {
throw new JGitInternalException(BCText.get().nonSignatureError);
}
} catch (NumberFormatException | PGPException e) {
throw new JGitInternalException(BCText.get().signatureParseError,
e);
}
Date signatureCreatedAt = signature.getCreationTime();
if (fingerprint == null && signer == null && keyId == null) {
return new SignatureVerification(NAME, signatureCreatedAt,
null, null, null, false, false, TrustLevel.UNKNOWN,
BCText.get().signatureNoKeyInfo);
}
if (fingerprint != null && keyId != null
&& !fingerprint.endsWith(keyId)) {
return new SignatureVerification(NAME, signatureCreatedAt,
signer, fingerprint, signer, false, false,
TrustLevel.UNKNOWN,
MessageFormat.format(BCText.get().signatureInconsistent,
keyId, fingerprint));
}
if (fingerprint == null && keyId != null) {
fingerprint = keyId;
}
// Try to find the public key
String keySpec = '<' + signer + '>';
Object cached = null;
BouncyCastleGpgPublicKey publicKey = null;
try {
cached = byFingerprint.get(fingerprint);
if (cached != null) {
if (cached instanceof BouncyCastleGpgPublicKey) {
publicKey = (BouncyCastleGpgPublicKey) cached;
}
} else if (!StringUtils.isEmptyOrNull(signer)) {
cached = bySigner.get(signer);
if (cached != null) {
if (cached instanceof BouncyCastleGpgPublicKey) {
publicKey = (BouncyCastleGpgPublicKey) cached;
}
}
}
if (cached == null) {
publicKey = BouncyCastleGpgKeyLocator.findPublicKey(fingerprint,
keySpec);
}
} catch (IOException | PGPException e) {
throw new JGitInternalException(
BCText.get().signatureKeyLookupError, e);
}
if (publicKey == null) {
if (cached == null) {
byFingerprint.put(fingerprint, NO_KEY);
byFingerprint.put(keyId, NO_KEY);
if (signer != null) {
bySigner.put(signer, NO_KEY);
}
}
return new SignatureVerification(NAME, signatureCreatedAt,
signer, fingerprint, signer, false, false,
TrustLevel.UNKNOWN, BCText.get().signatureNoPublicKey);
}
if (fingerprint != null && !publicKey.isExactMatch()) {
// We did find _some_ signing key for the signer, but it doesn't
// match the given fingerprint.
return new SignatureVerification(NAME, signatureCreatedAt,
signer, fingerprint, signer, false, false,
TrustLevel.UNKNOWN,
MessageFormat.format(BCText.get().signatureNoSigningKey,
fingerprint));
}
if (cached == null) {
byFingerprint.put(fingerprint, publicKey);
byFingerprint.put(keyId, publicKey);
if (signer != null) {
bySigner.put(signer, publicKey);
}
}
String user = null;
List userIds = publicKey.getUserIds();
if (userIds != null && !userIds.isEmpty()) {
if (!StringUtils.isEmptyOrNull(signer)) {
for (String userId : publicKey.getUserIds()) {
if (BouncyCastleGpgKeyLocator.containsSigningKey(userId,
keySpec)) {
user = userId;
break;
}
}
}
if (user == null) {
user = userIds.get(0);
}
} else if (signer != null) {
user = signer;
}
PGPPublicKey pubKey = publicKey.getPublicKey();
boolean expired = false;
long validFor = pubKey.getValidSeconds();
if (validFor > 0 && signatureCreatedAt != null) {
Instant expiredAt = pubKey.getCreationTime().toInstant()
.plusSeconds(validFor);
expired = expiredAt.isBefore(signatureCreatedAt.toInstant());
}
// Trust data is not defined in OpenPGP; the format is implementation
// specific. We don't use the GPG trustdb but simply the trust packet
// on the public key, if present. Even if present, it may or may not
// be set.
byte[] trustData = pubKey.getTrustData();
TrustLevel trust = parseGpgTrustPacket(trustData);
boolean verified = false;
try {
signature.init(
new JcaPGPContentVerifierBuilderProvider(),
pubKey);
signature.update(data);
verified = signature.verify();
} catch (PGPException e) {
throw new JGitInternalException(
BCText.get().signatureVerificationError, e);
}
return new SignatureVerification(NAME, signatureCreatedAt, signer,
fingerprint, user, verified, expired, trust, null);
}
private TrustLevel parseGpgTrustPacket(byte[] packet) {
if (packet == null || packet.length < 6) {
// A GPG trust packet has at least 6 bytes.
return TrustLevel.UNKNOWN;
}
if (packet[2] != 'g' || packet[3] != 'p' || packet[4] != 'g') {
// Not a GPG trust packet
return TrustLevel.UNKNOWN;
}
int trust = packet[0] & 0x0F;
switch (trust) {
case 0: // No determined/set
case 1: // Trust expired; i.e., calculation outdated or key expired
case 2: // Undefined: not enough information to set
return TrustLevel.UNKNOWN;
case 3:
return TrustLevel.NEVER;
case 4:
return TrustLevel.MARGINAL;
case 5:
return TrustLevel.FULL;
case 6:
return TrustLevel.ULTIMATE;
default:
return TrustLevel.UNKNOWN;
}
}
@Override
public void clear() {
byFingerprint.clear();
bySigner.clear();
}
}
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
Content-Type: text/plain; charset=UTF-8
Content-Length: 977
Content-Disposition: inline; filename="BouncyCastleGpgSignatureVerifierFactory.java"
Last-Modified: Thu, 14 Aug 2025 05:02:44 GMT
Expires: Thu, 14 Aug 2025 05:07:44 GMT
ETag: "566ad1bf916b7385ad74ab9a90747a24d220e4c4"
/*
* Copyright (C) 2021, 2024 Thomas Wolf and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal;
import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
import org.eclipse.jgit.lib.SignatureVerifier;
import org.eclipse.jgit.lib.SignatureVerifierFactory;
/**
* A {@link SignatureVerifierFactory} that creates {@link SignatureVerifier}
* instances that verify GPG signatures using BouncyCastle and that do cache
* public keys.
*/
public final class BouncyCastleGpgSignatureVerifierFactory
implements SignatureVerifierFactory {
@Override
public GpgFormat getType() {
return GpgFormat.OPENPGP;
}
@Override
public SignatureVerifier create() {
return new BouncyCastleGpgSignatureVerifier();
}
}
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
Content-Type: text/plain; charset=UTF-8
Content-Length: 6540
Content-Disposition: inline; filename="BouncyCastleGpgSigner.java"
Last-Modified: Thu, 14 Aug 2025 05:02:44 GMT
Expires: Thu, 14 Aug 2025 05:07:44 GMT
ETag: "adac9b199d5a4af6b8c2327a4a2431755aeef44d"
/*
* Copyright (C) 2018, 2024, Salesforce and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Iterator;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.lib.GpgConfig;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.Signer;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.util.StringUtils;
/**
* GPG Signer using the BouncyCastle library.
*/
public class BouncyCastleGpgSigner implements Signer {
private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
PersonIdent committer,
BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt)
throws CanceledException, UnsupportedCredentialItem, IOException,
NoSuchAlgorithmException, NoSuchProviderException, PGPException,
URISyntaxException {
if (gpgSigningKey == null || gpgSigningKey.isEmpty()) {
gpgSigningKey = '<' + committer.getEmailAddress() + '>';
}
BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator(
gpgSigningKey, passphrasePrompt);
return keyHelper.findSecretKey();
}
@Override
public GpgSignature sign(Repository repository, GpgConfig config,
byte[] data, PersonIdent committer, String signingKey,
CredentialsProvider credentialsProvider) throws CanceledException,
IOException, UnsupportedSigningFormatException {
String gpgSigningKey = signingKey;
if (gpgSigningKey == null) {
gpgSigningKey = config.getSigningKey();
}
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
committer, passphrasePrompt);
PGPSecretKey secretKey = gpgKey.getSecretKey();
if (secretKey == null) {
throw new JGitInternalException(
BCText.get().unableToSignCommitNoSecretKey);
}
JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder();
PGPPrivateKey privateKey = null;
if (!passphrasePrompt.hasPassphrase()) {
// Either the key is not encrypted, or it was read from the
// legacy secring.gpg. Try getting the private key without
// passphrase first.
try {
privateKey = secretKey.extractPrivateKey(
decryptorBuilder.build(new char[0]));
} catch (PGPException e) {
// Ignore and try again with passphrase below
}
}
if (privateKey == null) {
// Try using a passphrase
char[] passphrase = passphrasePrompt.getPassphrase(
secretKey.getPublicKey().getFingerprint(),
gpgKey.getOrigin());
privateKey = secretKey
.extractPrivateKey(decryptorBuilder.build(passphrase));
}
PGPPublicKey publicKey = secretKey.getPublicKey();
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
new JcaPGPContentSignerBuilder(
publicKey.getAlgorithm(),
HashAlgorithmTags.SHA256),
publicKey);
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
subpackets.setIssuerFingerprint(false, publicKey);
// Also add the signer's user ID. Note that GPG uses only the e-mail
// address part.
String userId = committer.getEmailAddress();
Iterator userIds = publicKey.getUserIDs();
if (userIds.hasNext()) {
String keyUserId = userIds.next();
if (!StringUtils.isEmptyOrNull(keyUserId)
&& (userId == null || !keyUserId.contains(userId))) {
// Not the committer's key?
userId = extractSignerId(keyUserId);
}
}
if (userId != null) {
subpackets.addSignerUserID(false, userId);
}
signatureGenerator
.setHashedSubpackets(subpackets.generate());
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (BCPGOutputStream out = new BCPGOutputStream(
new ArmoredOutputStream(buffer))) {
signatureGenerator.update(data);
signatureGenerator.generate().encode(out);
}
return new GpgSignature(buffer.toByteArray());
} catch (PGPException | NoSuchAlgorithmException
| NoSuchProviderException | URISyntaxException e) {
throw new JGitInternalException(e.getMessage(), e);
}
}
@Override
public boolean canLocateSigningKey(Repository repository, GpgConfig config,
PersonIdent committer, String signingKey,
CredentialsProvider credentialsProvider) throws CanceledException {
String gpgSigningKey = signingKey;
if (gpgSigningKey == null) {
gpgSigningKey = config.getSigningKey();
}
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
committer, passphrasePrompt);
return gpgKey != null;
} catch (CanceledException e) {
throw e;
} catch (Exception e) {
return false;
}
}
static String extractSignerId(String pgpUserId) {
int from = pgpUserId.indexOf('<');
if (from >= 0) {
int to = pgpUserId.indexOf('>', from + 1);
if (to > from + 1) {
return pgpUserId.substring(from + 1, to);
}
}
return pgpUserId;
}
}
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
Content-Type: text/plain; charset=UTF-8
Content-Length: 828
Content-Disposition: inline; filename="BouncyCastleGpgSignerFactory.java"
Last-Modified: Thu, 14 Aug 2025 05:02:44 GMT
Expires: Thu, 14 Aug 2025 05:07:44 GMT
ETag: "92ab65d7e44ff36f0acb5142b47f70af75bab539"
/*
* Copyright (C) 2021, 2024 Thomas Wolf and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal;
import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
import org.eclipse.jgit.lib.Signer;
import org.eclipse.jgit.lib.SignerFactory;
/**
* Factory for creating a {@link Signer} for OPENPGP signatures based on Bouncy
* Castle.
*/
public final class BouncyCastleGpgSignerFactory implements SignerFactory {
@Override
public GpgFormat getType() {
return GpgFormat.OPENPGP;
}
@Override
public Signer create() {
return new BouncyCastleGpgSigner();
}
}
Content-Type: text/plain; charset=UTF-8
Content-Length: 828
Content-Disposition: inline; filename="BouncyCastleGpgSignerFactory.java"
Last-Modified: Thu, 14 Aug 2025 05:02:44 GMT
Expires: Thu, 14 Aug 2025 05:07:44 GMT
ETag: "0968b6fcfe8d9dd3257a36788eb7b4ca735e81e7"
/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/
/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/